Rust

About

Difficulty
  • Rust is known for being very  complex, but it's as fast as C++; it’s safe and memory-safe, but complex with a steep learning curve.

  • 'memory safe', 'borrow checker'.

  • "Too much type struggle."

  • "Go hides the complexity for you. Rust helps you understand it by exposing it."

Adoption
  • It's typically used for Databases, Compilers, Operating Systems, CLI Programs, WebAssembly, Server-Side Apps, Embedded Apps, Game Engines.

  • Linux.

    • It is slowly being converted from C to Rust.

Installation

Installation
Requirements

Problems with MinGW

  • I tested tons of things.

  • Deleted the .rustup folder from users/caior/ and reinstalled Rust using rustup.

    • It worked, but I’m not sure if that alone solved it.

Build for GCC
  1. MSYS2 .

    • "MSYS2 is a collection of tools and libraries providing you with an easy-to-use environment for building, installing and running native Windows software."

    • "MSYS2 provides up-to-date native builds for GCC, mingw-w64, CPython, CMake, Meson, OpenSSL, FFmpeg, Rust, Ruby, etc."

    • "You will probably want to install some tools like the mingw-w64 GCC to start compiling projects. Run the following command:"

      • In the MSYS2 terminal:

        • pacman -S mingw-w64-x86_64-gcc .

        • pacman -S mingw-w64-ucrt-x86_64-gcc .

          • Didn’t work, possibly because it installs the UCRT version.

    • Query of installations:

      • pacman -Q

  2. MinGW-w64 via Chocolatey .

    • choco install mingw

  • "~Check which gcc is being used":

    • Get-Command x86_64-w64-mingw32-gcc

MSVCRT or UCRT runtime library?
  • Traditionally, the MinGW-w64 compiler used MSVCRT  as the runtime library, which is available on all Windows versions.

  • Since Windows 10, Universal C Runtime ( UCRT ) is available as an alternative to MSVCRT.

  • Universal C Runtime can also be installed on earlier Windows versions (see: Update for Universal C Runtime in Windows ).

  • Unless you are targeting older versions of Windows, UCRT as a runtime library is the better choice, as it was written to better support recent Windows versions and provide better standards conformance (see also: Upgrade your code to the Universal CRT ).

Cargo

Documentation
  • cargo doc

    • Builds the documentation for the local package and all dependencies. The output is placed in target/doc  in rustdoc’s usual format.

Packages

  • It's a bundle of one or more crates that provides a set of functionality.

  • A package contains a Cargo.toml  file that describes how to build those crates.

Creating a Package
  • cargo new NAME

    • Compiles to a binary.

    • main.rs  is used as the package root.

  • cargo new --lib NAME

    • Compiles to a library.

    • lib.rs  is used as the package root.

Compilation
  • cargo build

    • Compile and generate an executable.

  • ./executable_name

    • Run the executable.

  • cargo check

    • Compile without generating an executable.

    • Faster than cargo build .

  • cargo run

    • Compile, generate, and run the executable.

Cargo.toml
  • .

Cargo.lock
  • When you build a project for the first time, Cargo figures out all the versions of the dependencies that fit the criteria and then writes them to the Cargo.lock  file. When you build your project again, Cargo will see that the Cargo.lock  file exists and will use the versions specified there rather than figuring out versions again. This lets you have a reproducible build automatically. In other words, your project will remain at 0.8.5 until you explicitly upgrade, thanks to the Cargo.lock  file.

  • Because the Cargo.lock  file is important for reproducible builds, it’s often checked into source control with the rest of the code in your project.

Crates

  • It's a tree of modules.

Binary Crate
  • Must have a function called main  that defines what happens when the executable runs.

Library Crate
  • Doesn't have a main  function, and they don’t compile to an executable.

  • They define functionality intended to be shared with multiple projects.

  • Usually, when people say “crate”, they mean 'library crate'.

Crate Root
  • The crate root  is a source file that the Rust compiler starts from and makes up the root module of your crate.

    • Usually symbolized by main.rs  or lib.rs , depending if the Package is bin or lib.

  • Every package needs a Crate Root.

  • In the crate root file, you can declare new modules, like mod garden; .

  • The compiler will look for the module’s code in these places:

    • Inline, within curly brackets that replace the semicolon following mod garden

    • In the file src/garden.rs

    • In the file src/garden/mod.rs

Submodules
  • In any file other than the crate root, you can declare submodules. For example, you might declare mod vegetables;  in src/garden.rs .

  • The compiler will look for the submodule’s code within the directory named for the parent module in these places:

    • Inline, directly following mod vegetables , within curly brackets instead of the semicolon

    • In the file src/garden/vegetables.rs

    • In the file src/garden/vegetables/mod.rs

Modules and Use

  • Let you control organization, scope, and path privacy

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}
Super
  • Refers to the parent module.

Paths

  • A way of naming an item, such as a struct, function, or module.

Debug

Comments
  • .

Proper printing
  • [#derive(Debug)]  + "println!({:?}, x)"

    #[derive(Debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    
    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.height
        }
    }
    
    fn main() {
        let rect1 = Rectangle {
            width: 30,
            height: 50,
        };
    
        println!(
            "The area of the rectangle is {} square pixels.",
            rect1.area()
        );
    }
    
VSCode Debug
  • LLDB extension installed.

  • launch.json file:

    • .

    • .

      • Used from the Native Debug Extension.

Error Handling
  • panic!

    • Crashes the program.

    • Avoid using it.

  • assert!()

    • Panics if the given condition is false.

  • .unwrap()

    • .

  • .expect()

    • .

LSP
Tests
  • #[cfg(test)]

    • Used to mark code blocks  that should be compiled only during testing .

    • Modules marked with this attribute are not compiled into the binary.

    • Where used :

      • Usually in modules ( mod ) or other sections of code that should only be included in the binary during testing.

    • Behavior :

      • Any code within a block marked with #[cfg(test)]  will be ignored during normal compilation (not included in the final binary) and only compiled/executed during tests.

  • #[test]

    • Used to mark a function  as a test.

    • Functions marked with this attribute are not compiled into the binary.

    • Where used :

      • Directly before a function to be executed by Rust’s test framework.

    • Behavior :

      • The test framework recognizes functions with #[test]  as individual tests and executes them when running cargo test .

  • cargo test

    • Runs all tests.

Imports

Memory

  • Does not have a Garbage Collector.

Ownership

Borrow Checker

Lifetime

Syntax with '
&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Usage and need
  • The return type needs a generic lifetime parameter on it because Rust can’t tell whether the reference being returned refers to x  or y .

    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    
  • This tells Rust that the string slice returned from the function will live at least as long as lifetime 'a

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    
  • Example:

    use std::fmt::Display;
    
    fn longest_with_an_announcement<'a, T>(
        x: &'a str,
        y: &'a str,
        ann: T,
    ) -> &'a str
    where
        T: Display,
    {
        println!("Announcement! {ann}");
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    
Requirements
  • The return lifetime of the function must be one of the lifetimes of its parameters.

    • Does not work :

    fn longest<'a>(x: &str, y: &str) -> &'a str {
        let result = String::from("really long string");
        result.as_str()
    }
    
    • result  stops existing when the function scope ends, so its lifetime cannot be used.

  • Lifetime annotations become part of the function’s contract, similar to types in its signature.

Conclusion
  • "The Lifetime is NOT  changed. It only creates relationships between the lifetimes of multiple references."

  • The generic lifetime 'a  will get the concrete lifetime that equals the shorter of the lifetimes  of x  and y .

  • The returned reference will also be valid for the shorter lifetime  of x  and y .

  • "Is the smallest lifetime still valid?"
    ``

Symbols

  • Borrow

    • &

  • Namespace

    • ::

    algo::algo
    ::path       // Path relative to the crate root, implicitly.
    self::path   // Path relative to the crate root, explicitly.
    super::path      // Path relative to the parent of the current module.
    
    • The ::  syntax is used for both associated functions and module namespaces.

Attributes

  • External attribute

    • #[meta]

  • Internal attribute

    • #![meta]

  • Invoke macro

    • something!()

  • Macro substitution

    • $something

  • Macro capture

    • $something:kind

Derive Macros

Definition
  • #[derive]  is a convenient way to apply macros that automatically implement traits for an entire struct (or enum). When you use #[derive(...)] , the compiler automatically generates the implementation for you. Without derive, you’d need to write it manually.

derive(debug)
  • Automatic implementation of the Debug  trait, using #[derive(Debug)] :

    #[derive(Debug)] // Compiler implements Debug
    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 3, y: 4 };
        println!("{:?}", p); // Output: Point { x: 3, y: 4 }
    }
    
  • Manual implementation of the Debug  trait, without #[derive(Debug)] :

    use std::fmt;
    
    struct Point {
        x: i32,
        y: i32,
    }
    
    // Manual implementation of the Debug trait
    impl fmt::Debug for Point {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
        }
    }
    
    fn main() {
        let p = Point { x: 3, y: 4 };
        println!("{:?}", p); // Output: Point { x: 3, y: 4 }
    }
    

Operations

Numeric Operations

Arithmetic
Compound
  • All operators allow Operation + Assignment:

    • counter += 1

Logical Comparisons

Basic
  • ==

  • !=

  • >

  • <

  • AND

    • &&

  • OR

    • ||

  • NOT

Bitwise
  • AND

    • &

  • OR

    • |

  • XOR

    • ^

  • L SHIFT

    • <<

  • R SHIFT

    • >>

Control Flow

Conditionals

Examples
  • Works :

    if number < 10 {
        println!("algo");
    } else if number < 22 {
        println!("outra coisa");
    } else {
        println!("outra coisinha");
    }
    
  • Does not work :

    let number = 3;
    
    if number {
        println!("number was three");
    }
    
    • Rust will not automatically try to convert non-Boolean types to a Boolean.

Ternary
let number: i32 = if 2 < 3 { 5 } else { 1 }
  • If the if  and else  arms have value types that are incompatible, you'll get an error.

Loops

Loop
loop {
    println!("again!");
    if \condicional {
        break;
    }
}
let mut counter = 0;
let result  = loop {

    counter += 1;

    if counter == 10 {
        break counter;
    }
};
  • break

    • Exits the loop.

  • continue

    • Skip over any remaining code in this iteration of the loop and go to the next iteration.

  • Labels

    • You can optionally specify a loop label  on a loop that you can then use with break  or continue .

      let mut count = 0;
      'counting_up: loop {
          println!("count = {count}");
          let mut remaining = 10;
      
          loop {
              println!("remaining = {remaining}");
              if remaining == 9 {
                  break;
              }
              if count == 2 {
                  break 'counting_up;
              }
              remaining -= 1;
          }
      
          count += 1;
      }
      println!("End count = {count}");
      
While
let mut number = 3;

while number != 0 {
    number -= 1;
}
For
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
    println!("the value is {}", element);
}
// Creates a sequence of numbers from 1 to 3.
for number in (1..4) {
    println!("the value is {}", element);
}
// Same thing
for number in 1..4 {
    println!("the value is {}", element);
}

Keywords

let

Characteristics
  • Does not work :

    • x = y = 6  and have both x  and y  have the value 6 .

Typing
  • You cannot change the variable's type.

    let mut spaces = "   ";
    spaces = spaces.len();
    
    • This will cause an error because spaces  attempts to change type from &str  to usize .

Shadowing
  • Shadowing is different from marking a variable as mut  because we’ll get a compile-time error if we accidentally try to reassign to this variable without using the let  keyword. By using let , we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

  • Allowed :

    • Keeps the type:

      let x = 5;
      let x = x + 1;
      
    • Changes the type:

      let spaces = "   ";
      let spaces = spaces.len();
      

const

Differences from let
  • You aren’t allowed to use mut  with constants.

    • Constants aren’t just immutable by default—they’re always immutable.

  • The type of the value must  be annotated.

  • Constants can be declared in any scope, including the global scope, which makes them useful for values that many parts of code need to know about.

  • The last difference is that constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.

  • Rust’s naming convention for constants is to use all uppercase with underscores between words.

  • Constants are valid for the entire time a program runs, within the scope in which they were declared.

  • Naming hardcoded values used throughout your program as constants is useful in conveying the meaning of that value to future maintainers of the code. It also helps to have only one place in your code you would need to change if the hardcoded value needed to be updated in the future.

Data Types

Type conversion
  • You need to specify the type with : .

  • String to Int :

    let a: u32 = "42".parse().expect("Falha!");
    
  • &str to String :

    let a = "texto";      // &str
    let b = a.to_string(); // String
    
    let a = String::from("texto");
    
Kinds of types
  • Compound Types:

    • Tuples and Arrays.

  • Scalar Types:

    • Everything else.

Boolean

fn main() {
    let t = true;
    let f: bool = false;
}

Integers

  • .

  • " arch " varies depending on the system architecture (32-bit or 64-bit).

    • The primary situation in which you’d use isize  or usize  is when indexing some sort of collection.

  • Signed  and unsigned  refer to whether it’s possible for the number to be negative.

Number Literals
  • .

  • Number literals can also use _  as a visual separator to make the number easier to read.

1_000
1000

Floats

  • All floating-point types are signed.

  • The default type is f64  because on modern CPUs, it’s roughly the same speed as f32  but is capable of more precision.

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

Char

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}
  • "Your human intuition for what a “character” is may not match up with what a char  is in Rust."

Strings

  • Explanation .

    • At the end of the video there is a summary.

  • No null terminator.

  • All strings are valid UTF-8.

  • Immutable by default.

Append
  • Example:

  • With format!

    let s1 = String::from("hello ");
    let s2 = String::from("world");
    let s3 = format!("{}{}", s1, s2);
    
    • This macro uses references so that this call doesn’t take ownership of any  of its parameters.

  • With concatenation:

    let s1 = String::from("hello ");
    let s2 = String::from("world");
    let s3 = s1 + &s2;  // s1 is moved.
    
  • With .push  or .push_str :

    let mut s = String::from("hello");
    s.push_str("bar");   // Receives a string slice.
    s.push('!');         // Receives a char.
    
Accessing characters
for c in "hello world".chars() {
    println!("{c}");
}
Placeholders
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
Literals
  • Raw strings :

    • They are the same:

    let text = "he said \"goodbye\" and left";
    let text = r#"he said "goodbye" and left"#;
    
    • This is used to avoid having to use \  every time you want to use quotes inside strings.

    • Very useful for RegEx.

  • Byte strings :

    let http_ok = b"HTTP/1.1 200 OK\r\n";
    let http_ok: &[u8; 17] = b"HTTP/1.1 200 OK\r\n";
    
    • Creates a 'slice of bytes'.

    • Useful when dealing with network protocols that expect a byte sequence, such as HTTP.

Types

String (String)
let my_string: String = String::from("hello!");
  • Owned type.

  • **Use:

    • Create and modify strings.

      • Read files.

      • User inputs.

&str (string slice)
let my_string: &str = &my_string; 
  • Borrowed type.

    • Not the owner, just has access.

  • Read-only.

  • It's a pointer to the start of the string and the length of the string.

  • **Use:

    • Read and analyze an existing string without changing it.

      • Parse a string

      • Search for a substring.

  • Static:

    let my_string: &str = "hello world";
    let my_string: &'static str = "hello world";
    
    • Both represent the same thing.

    • The 'static' indicates the pointed value is guaranteed to be available for the entire runtime of the program.

Box<str> (boxed string slice)
  • Owned, non-growable, heap-allocated string slice.

  • "Freeze a string to prevent further modification".

let my_string: String = String::from("this is a long string");
let my_boxed_str: Box<str> = my_string.into_boxed_str();
  • This drops the capacity information, reducing memory usage.

Rc<str> (Reference counted string slice)
  • Shared, immutable string slice.

  • Not thread-safe.

  • "~duplicate an object without duplicating it in memory", something like that.

Arc<str> (Atomic reference counted string slice)
  • Same as Rc<str> , but thread-safe.

  • Useful when you want to use the string across different threads.

Vec<u8> (Vector of UTF-8 bytes)
  • It's basically the same thing as String .

  • Useful when dealing with non-UTF-8 encoded strings.

Specialized string types, less known

&mut str (mutable reference to a sequence of string bytes)
  • Normally avoided in idiomatic Rust code because it's complex and poses potential problems ensuring the content remains valid UTF-8.

Cow<'a, str> (copy on write)
  • I didn't quite understand it.

Interoperability

  • "Helps connect Rust code with other languages".

OsString, OsStr
  • Can contain any byte sequence, not just UTF-8.

Path
  • Deals with paths.

  • Used to inspect filesystem paths.

PathBuf
  • "mutable and owned version of a path".

CString, CStr
  • Useful when interacting with C code that expects a null terminator.

Tuples

  • Tuples have a fixed  length: once declared, they cannot grow or shrink in size.

  • The types of the different values in the tuple don’t have to be the same.

Creation
let tup: (i32, f64, u8) = (500, 6.4, 1);
Access
  • Via Index:

    let x: (i32, f64, u8) = (500, 6.4, 1);
    
    let five_hundred = x.0;
    
    let six_point_four = x.1;
    
    let one = x.2;
    
  • Via Destructuring:

    let tup = (500, 6.4, 1);
    
    let (x, y, z) = tup;
    
    println!("The value of y is: {y}");
    

Arrays

  • Every element of an array must have the same type.

  • Arrays have a fixed  length.

Creation
  • Defining values manually:

    let a = [1, 2, 3, 4, 5];
    let b: [i32; 5] = [1, 2, 3, 4, 5];
        // [Type; number_of_elements]
    
  • Creating with identical elements:

    let a = [3; 5];
    
    // Same thing.
    let b = [3, 3, 3, 3, 3];  
    
Access
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
  • Out of Bounds :

    • If the index is out of bounds, Rust will panic.

    • In many low-level languages, this kind of check is not done, and when you provide an incorrect index, invalid memory can be accessed. Rust protects you against this kind of error by immediately exiting instead of allowing the memory access and continuing.

Data Types: Collections

  • The data these collections point to is stored on the heap, which means the amount of data does not need to be known at compile time and can grow or shrink as the program runs.

Vectors

  • Is a similar collection type to the Array, provided by the standard library, that is allowed to grow or shrink in size.

Creation
let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);
let v = vec![1, 2, 3];
Access
  • Without handling Out of Bounds:

    let v = vec![1, 2, 3, 4, 5];
    
    let third: &i32 = &v[2];
    println!("The third element is {third}");
    
  • Handling Out of Bounds:

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
    
  • Getting all elements:

    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
    
  • Mutating with the *  dereference operator.

    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
    

Hash Maps

  • You need to import the collection.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {
    println!("{key}: {value}");
}

println!("{scores:?}");

Functions

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}
main
  • "You’ve already seen one of the most important functions in the language: the main  function, which is the entry point of many programs".

Where to put functions
  • Rust doesn’t care where you define your functions, only that they’re defined somewhere in a scope that can be seen by the caller.

    • Nice.

Declaration
  • You must  declare the type of each parameter.

Return
fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}
  • If there is a return, the return type must be specified.

  • Using the return  keyword is not necessary.

  • If return  is omitted, do not use a ;  on the return line.

Getters
  • Often, but not always, when we give a method the same name as a field we want it to only return the value in the field and do nothing else. Methods like this are called getters , and Rust does not implement them automatically for struct fields.

  • Getters are useful because you can make the field private but the method public, and thus enable read-only access to that field as part of the type’s public API.

Closures
  • Definition :

    • They are anonymous functions that capture and use variables from the scope where they were defined.

  • GDScript :

    • Exactly the same as GDScript. In GDScript, anonymous functions are all closures.

  • Uses || .

  • Example:

    let mut list = vec![1, 2, 3];
    
    println!("Before defining closure: {list:?}");
    
    let mut borrows_mutably = || list.push(7);
    
    borrows_mutably();
    
    println!("After calling closure: {list:?}");
    
    • Output:

      Before defining closure: [1, 2, 3]
      After calling closure: [1, 2, 3, 7]
      

Structs

  • Good explanatory video .

  • Unlike with tuples, in a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.

Creation
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
  • Tuple Structs :

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    
    fn main() {
        let black = Color(0, 0, 0);
        let origin = Point(0, 0, 0);
    }
    
Creating methods
  • The struct methods' first parameter is always self , which represents the instance of the struct the method is being called on.

    • The &self  is actually short for self: &Self .

      • The type Self  is an alias for the type that the impl  block is for.

    • Methods can take ownership of self , borrow self  immutably, as we’ve done here, or borrow self  mutably, just as they can any other parameter.

    • We chose &self  here, as we don’t want to take ownership, and we just want to read the data in the struct, not write to it.

  • All functions defined within an impl  block are called associated functions  because they’re associated with the type named after the impl .

    • Everything within this impl  block will be associated with the Rectangle  type.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}
  • Using multiple impl

    • There’s no reason to separate these methods into multiple impl  blocks here, but this is valid syntax:

    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.height
        }
    }
    
    impl Rectangle {
        fn can_hold(&self, other: &Rectangle) -> bool {
            self.width > other.width && self.height > other.height
        }
    }
    
Creating an instance
  • Directly :

    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
    
  • "Builder" :

    fn build_user(email: String, username: String) -> User {
        User {
            active: true,
            username: username,  // can be written just 'username'
            email: email,        // can be written just 'email'
            sign_in_count: 1,
        }
    }
    
  • Constructors :

    • Associated functions that aren’t methods are often used for constructors that will return a new instance of the struct.

      • These are often called new , but new  isn’t a special name and isn’t built into the language.

      • For example, we could choose to provide an associated function named square  that would have one dimension parameter and use that as both width and height, thus making it easier to create a square Rectangle  rather than having to specify the same value twice

    impl Rectangle {
        fn square(size: u32) -> Self {
            Self {
                width: size,
                height: size,
            }
        }
    }
    
    • The Self  keywords in the return type and in the body of the function are aliases for the type that appears after the impl  keyword, which in this case is Rectangle .

      let sq = Rectangle::square(3);
      
Access
fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}
  • Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable.

  • No need for -> access operator :

    • Rust automatically adds in & , &mut , or *  so object  matches the signature of the method.

    (&p1).distance(&p2);
    
    // Same thing
    p1.distance(&p2);
    

Traits

Implementing a Trait for a Struct
  • The functions inside the Trait may have no implementation or a default implementation that can be overridden by the implementing type.

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Traits as types and Trait Bounds
  • Ex1 : "Accepts anything that implements Summary".

    // Syntax sugar for the version below.
    pub fn notify(item: &impl Summary) {
        println!("Breaking news! {}", item.summarize());
    }
    
    // Same thing, represented via a "trait bound".
    pub fn notify<T: Summary>(item: &T) {
        println!("Breaking news! {}", item.summarize());
    }
    
  • Ex2 :

    pub fn notify(item1: &impl Summary, item2: &impl Summary) {
    }
    
    // Same thing, but the "trait bound" infers directly that the types of 'item1' and 'item2' must be the same.
    pub fn notify<T: Summary>(item1: &T, item2: &T) {
    }
    
  • Ex3 :

    pub fn notify(item: &(impl Summary + Display)) {
    }
    
    // Same thing, but using "trait bound".
    pub fn notify<T: Summary + Display>(item: &T) {
    }
    
  • Ex4 : Use of where , with the sole purpose of avoiding a very long signature.

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
}

// Same thing.
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
Differences between Traits and Abstract Classes in C#
  • Multiple inheritance :

    • Traits in Rust allow multiple implementations . A type can implement several traits, while in C#, a class can only directly inherit from one abstract class (but can implement multiple interfaces).

  • No class hierarchy :

    • Rust has no class hierarchy. Traits are independent from each other and are not part of an inheritance structure like abstract classes in C#. This avoids the rigidity of single inheritance found in C#.

  • No state or fields :

    • Traits in Rust cannot contain fields (state) , while abstract classes in C# can. Traits define only behavior, without storing data.

  • Implementing traits for external types :

    • In Rust, you can implement traits for types defined outside your control (provided you defined the trait or the type). This is not allowed with abstract classes in C#.

  • Generics vs. dynamic typing :

    • Traits in Rust often use generics  to determine behavior at compile time. In C#, polymorphism via abstract classes is often based on dynamic typing  at runtime.

Enums

Similarities between Enums and Structs
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));
  • The name of each enum variant that we define also becomes a function that constructs an instance of the enum.

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    • Quit  has no data associated with it at all.

    • Move  has named fields, like a struct does.

    • Write  includes a single String .

    • ChangeColor  includes three i32  values.

  • We’re also able to define methods on enums using impl .

    impl Message {
        fn call(&self) {
            // method body
        }
    }
    
    let m = Message::Write(String::from("hello"));
    m.call();
    

Options

Definition
enum Option<T> {
    None,
    Some(T),
}
let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;
as_mut
  • The as_mut  method is a method of optional types , like Option<T> .

  • It is used to convert an Option<T>  into an Option<&mut T> . This allows accessing the contained value mutably without moving the value.

let mut opt: Option<String> = Some(String::from("Hello"));

if let Some(value) = opt.as_mut() {
    value.push_str(", world!"); // Modifies the contained value
}

println!("{:?}", opt); // Output: Some("Hello, world!")

Adapters for working with references

  • as_ref  converts from &Option<T>  to Option<&T> .

  • as_mut  converts from &mut Option<T>  to Option<&mut T> .

  • as_deref  converts from &Option<T>  to Option<&T::Target> .

  • as_deref_mut  converts from &mut Option<T>  to Option<&mut T::Target> .

  • as_pin_ref  converts from Pin<&Option<T>>  to Option<Pin<&T>> .

  • as_pin_mut  converts from Pin<&mut Option<T>>  to Option<Pin<&mut T>> .

Using ?

  • Without ? :

    fn add_last_numbers(stack: &mut Vec<i32>) -> Option<i32> {
        let a = stack.pop();
        let b = stack.pop();
    
        match (a, b) {
            (Some(x), Some(y)) => Some(x + y),
            _ => None,
        }
    }
    
    
  • With ? :

    fn add_last_numbers(stack: &mut Vec<i32>) -> Option<i32> {
        Some(stack.pop()? + stack.pop()?)
    }
    

Extracting the contained value

If let
let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}
Matching
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Result

  • Result<T, E>  is the type used for returning and propagating errors.

  • It is an enum with the variants, Ok(T) , representing success and containing a value, and Err(E) , representing error and containing an error value.

enum Result<T, E> {
   Ok(T),
   Err(E),
}
#[derive(Debug)]
enum Version { Version1, Version2 }

fn parse_version(header: &[u8]) -> Result<Version, &'static str> {
    match header.get(0) {
        None => Err("invalid header length"),
        Some(&1) => Ok(Version::Version1),
        Some(&2) => Ok(Version::Version2),
        Some(_) => Err("invalid version"),
    }
}

let version = parse_version(&[1, 2, 3, 4]);
match version {
    Ok(v) => println!("working with version: {v:?}"),
    Err(e) => println!("error parsing header: {e:?}"),
}

The ?  operator

  • The operator is defined to perform an early return of a value out of the function

  • The ?  operator can only be used in functions whose return type is compatible with the value the ?  is used on.

    • We’re only allowed to use the ?  operator in a function that returns Result , Option , or another type that implements FromResidual .

    • By default, use Result .

  • Usage :

    use std::fs::File;
    use std::io::{self, Read};
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut username_file = File::open("hello.txt")?;
        let mut username = String::new();
        username_file.read_to_string(&mut username)?;
        Ok(username)
    }
    
    use std::fs::File;
    use std::io::{self, Read};
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut username = String::new();
    
        File::open("hello.txt")?.read_to_string(&mut username)?;
    
        Ok(username)
    }
    
  • Ex1 :

    • Without ? :

      use std::fs::File;
      use std::io::prelude::*;
      use std::io;
      
      struct Info {
          name: String,
          age: i32,
          rating: i32,
      }
      
      fn write_info(info: &Info) -> io::Result<()> {
          // Early return on error
          let mut file = match File::create("my_best_friends.txt") {
                 Err(e) => return Err(e),
                 Ok(f) => f,
          };
          if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
              return Err(e)
          }
          if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
              return Err(e)
          }
          if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
              return Err(e)
          }
          Ok(())
      }
      
    • With ? :

      use std::fs::File;
      use std::io::prelude::*;
      use std::io;
      
      struct Info {
          name: String,
          age: i32,
          rating: i32,
      }
      
      fn write_info(info: &Info) -> io::Result<()> {
          let mut file = File::create("my_best_friends.txt")?;
          // Early return on error
          file.write_all(format!("name: {}\n", info.name).as_bytes())?;
          file.write_all(format!("age: {}\n", info.age).as_bytes())?;
          file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
          Ok(())
      }
      

Unwrapping

Match

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}
  • Pattern :

    • The value Coin::Penny .

  • Separator :

    • =>  operator.

Examples
let x = 1;

match x {
    1 | 2 | 3 => println!("1 or 2 or 3");
    4..=8 => println!("from 4 to 8, including 8");
    'a'...='j' => println!("from a to j, including j");
    _ => println!("anything");
}
Exhaustiveness
  • If not all options are considered in the match, it will panic.

  • You can use the _  symbol to guarantee that all options are covered.

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => reroll(),
    }
    
    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn reroll() {}
    
  • You can use the unit value   ()  to indicate that nothing happens in a branch:

    let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        _ => (),
    }
    
    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    

If Let

  • You can handle values that match one pattern while ignoring the rest.

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}
  • What I understood is that if let  is basically treated like a new keyword, kinda....

  • I found it a bit confusing because it seems like I'm evaluating the return value of the operation let Some(max) , which doesn't make sense, considering let  doesn't return anything.

Iterator

  • It is a trait that requires implementing a single method: next .

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Game Dev

  • AreWeGameYet .

    • Getting started in GameDev with Rust.

    • Several useful resources.

    • List of engines.

Bevy

Impressions
  • Holy moly, Rust is intense for Game Dev.... It doesn't sound fun.

  • I give the same arguments for the Godot Rust binding.

  • I don't know if it's more correct to use an engine in Bevy, or a binding for Godot.

    • My impression is that Bevy would certainly be a more pleasant experience, despite having fewer features, probably.

    • It's hard to decide exactly that without a direct comparison between the two engines and Bevy is still in an experimental state.

Sources
About
Examples

Godot