Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

About

My personal notes I took when reading the book.

Variables

Type annotation

  • type is either inferred from the context or explicitly specified by the developer
// inferred type
let x = 42;
let y: u32 = x;

// explicit type 
// let <variable_name>: <type> = <expression>;
let y: u32 = 48;

Initialization

  • a varible does need to be initialized until it is used
let x: u32;

Function’s parameters

  • function’s parameters are variables too
  • type annotation of parameters is mandatory
fn add_one(x: u32) -> u32 {
    x + 1
}

Mutability

  • by default the variables are immutable unless you make it mutable with mut
fn main () {
    let x = 5;
    let mut y = 6;
    println! {"Variable x equals to {x} and cannot be changed."};
    println! {"Variable y equals to {y} and can be changed."};
    y = 8;
    println!("New value of variable y equals to {y}.")
}

Constants

  • The type of the value must be annotated.
  • Can be declared in any scope, including the global scope.
  • It may be set only to a constant expression, not the result of a value that could only be computed at runtime.
  • The naming convention: all uppercase with underscores between words.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Shadowing

  • A variable can be shadowed by using the same variable’s name and use it with let.
  • The second variable overshadows the first variable until either it itself is shadowed or the scope ends.
fn main() {
    let x = 5;
    println!("The value of x is {x}.");

    let x = x + 2;
    println!("The new value of x is {x} and overshadows the previous value.");

    {
        let x = x * 2;
        println!("The value of x in inner scope is {x}.");
    }

    println!("The value of x outside the inner scope is again {x}");

}
  • Shadowing differs from marking a variable as mut:
    • as the value cannot be changed without using the let
    • the variable is still immutable after transformation with let
  • Shadowing can also change the type of the value
fn main() {
    let spaces = "   ";
    println!(" Variable spaces is a string.");
    let spaces = spaces.len();
    println!("The type of variable spaces is now a number and spaces equals to {spaces}.");
}

Numbers

Literals

  • Literal is a notation representing a fixed value in the code
  • You can use underscores in literals, i.e. 98222 is the same as 98_222
  • Type annotation for literal can be provided as a suffix, i.e. 23u32 is 23 that is explicitly typed as u32

Integer literals:

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b’A’

Arithmetic operators

  • + for addition
  • - for subtraction
  • * for multiplication
  • / for division
  • % for remainder

Note:

  • division of integer data types: 5 / 3 results in 1

No Automatic Type Coercion

  • Rust is statically typed language (i.e. it must know the data type already during the compilation)
  • Any conversion from one type to another has to be done explicitly
// this will produce compilation error
let b: u8 = 100;
let a: u32 = b;

// or this will produce compilation error
fn compute(a: u32, b: u32) -> u32 {
    let multiplier: u8 = 4;
    a + b * multiplier
}

Scalar Primitive Types

  • represent a single value

Integer

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
Architecture-dependentisizeusize
  • signed can represent negative number whereas unsigned is always positive.
  • isize and usize depend on architecture: either 32-bit or 64-bit. (e.g. usage is when indexing a collection)
  • you can check the MAX or MIN value of each data type by using u8::MIN or u8::MAX:
fn main() {
    let x = i8::MAX;
    println!("The maximum value of data type i8 is {x}.");
}

Floating point number

  • all floating point types are signed.
  • Rust has two floating point types: f32 and f64, the latter one being default
fn main() {
    let x = 3.2; // will default to f64
    let y: f32 = 4.6; // is explicitly set to f32
}

Char, Bool & Unit

Boolean

  • The boolean type is specified using bool.
  • Booleans are one byte in size.
fn main() {
    // boolean data type inferred 
    let y = false;

    // explicit type annotation
    let x: bool = true; 
}

Character

  • The character type is specified using char.
  • Characters are 4 bytes in size.
  • char is specified with single quotation marks.
fn main() {
    // character data type inferred 
    let c = 'z';
    let heart_eyed_cat = '😻';

    // with explicit type annotation
    let z: char = 'ℤ'; 
}

Functions

Function

  • any function used in main function must defined in the program (after or before main function)
  • type annotation of parameters is mandatory in function’s signature (i.e. what precedes function’s body).
  • functions can return a value if defined (by declaring their type):
    • the return value can be explicitly defined with return statement or
    • the last expression in a function is implicitly returned.
// `fn` <function_name> ( <input params> ) -> <return_type> { <body> }
fn main() {
    let x: i32 =  sum(5, 6);
    let y = number();

    println!("The number is {x}");  
    println!("The number is {}", y); 
}

fn sum(a: i32, b: i32) -> i32 {

    // the last expression in a function is implicitly returned
     a + b  
    }

fn number() -> i32 {

    // the explicitly defined return value 
    return 3 + 5;  
}

Statements & Expressions

Statement

  • instructions that perform an action and do not return a value
  • ends with semicolon ;
  • examples:
    • creating a variable and value assignment let x = 6
    • function definition itself is a statement
fn main() {
    let y = 6;
}

Expression

  • expressions evaluate to a resultant value
  • expression can be part of a statement e.g. can be assigned as a value to a variable
  • expression does not have a semicolon at the end of line
  • examples:
    • number 6 in statement let x = 6 is an expression that evaluates to 6
    • calling function is an expression
    • if itself is an expression
fn main() {
    let y = {
        let x = 3;
        x + 1     // an expression
    };
}

Control Flow

If, else if, else

  • if is an expression, it can be used for instance in let statement
  • a condition must be of type bool, a boolean.
  • the values potentially resulting from each arm of the if expression must be the same type
fn main() {
    let condition = true;
    let number = if condition { 8 } else { 6 }; // both 5 and 6 must be of the same type
    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else {
        println!("number is not divisible by 4 or 3");
    }
}

Comparison Operators

  • == equal to
  • != not equal to
  • < less than
  • > greater than
  • <= less than or equal to
  • >= greater than or equal to

Recursion

  • the case in which the function calls itself with a smaller/simpler input
  • the recursive function must contain a condition to stop calling itself
fn main() {
    let number: u32 = 5;
    let result: u32 = factorial(number);
    println!("Factorial of {number} is {result}.");
  }

// recursive function returning factorial for given parameter
fn factorial(a: u32) -> u32 {
 if a == 0 {
    1
 } else {
    a * factorial1(a-1)
 }
}

Loop

  • Executes a block of code over and over again until stopped by brake
  • break can be acompanied with the value to be returned out of the loop
  • continue can be used to skip any remaining code in the current loop iteration and go to the next iteration
fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}
  • break and continue are always applied to the innermost loop unless loop label is used
fn main() {
    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

  • While the condition is true the loop runs
fn main() {
    let number: u32 = 5;
    let result: u32 = factorial(number);
    println!("Factorial of {number} is {result}.");
  }

// function with while loop returning factorial for given parameter
fn factorial(a: u32) -> u32 {
    let mut result: u32 = 1;
    let mut i: u32 = 1;
    while i <= a {
        result *= i;
        i += 1;
    }
    result
}

For

  • executes a block of code for each element in an iterator (range of values)
  • ranges:
    • 1..5: A (half-open) range. It includes all numbers from 1 to 4. It doesn’t include the last value, 5.
    • 1..=5: An inclusive range. It includes all numbers from 1 to 5. It includes the last value, 5.
    • 1..: An open-ended range. It includes all numbers from 1 to infinity (well, until the maximum value of the integer type).
    • ..5: A range that starts at the minimum value for the integer type and ends at 4. It doesn’t include the last value, 5.
    • ..=5: A range that starts at the minimum value for the integer type and ends at 5. It includes the last value, 5.
//using an array as an iterator
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a { //using a collection
        println!("the value is: {element}");
    }
}
//using reversed range as iterator
fn main() {
    for number in (1..4).rev() { 
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}
fn main() {
    let number: u32 = 5;
    let result: u32 = factorial(number);
    println!("Factorial of {number} is {result}.");
  }

// function with for loop returning factorial for given parameter
fn factorial3(a: u32) -> u32 {
    let mut result: u32 = 1;
    for i in 1..=a {
        result *= i;
    }
    result
}

Ownership

Variable scope

fn main() {
        {                      // s is not valid here, since it's not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s

    }                      // this scope is now over, and s is no longer valid

}

Ownership rules

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

String type

  • Unlike string literals (&str), String can grow so its size is not known at compile time.
  • The String contents are stored on the heap. The memory allocator finds space on the heap and returns a pointer to it.
  • The String value itself (pointer + length + capacity) is fixed-size and stored on the stack (i.e. stack representaion of a String which owns the bytes on the heap)
  • When a String goes out of scope, Rust automatically calls drop to free the heap memory
  • See Figure 4-1 in the book.
fn main() {
    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }   // this scope is now over, and s is no longer valid
}

Move

  • A String assignment copies the stack data (pointer, length, capacity).
  • The heap data is not copied (stay where they are).
  • The original variable s1 becomes invalid and can no longer be used.
  • Ownership of the value moves to the new variable s2. This prevents double-free errors when values are dropped.
  • See Figure 4-4 in the book.
fn main() {
    let s1 = String::from("hello");

    // assigning s1 to s2
    let s2 = s1;  

    // s1 is invalidated and cannot be used here
}

Clone

  • clone performs a deep copy.
  • A new heap allocation is created and the data is copied.
  • Both variables own separate values.
  • Each value is dropped independently.
    let s1 = String::from("hello");

    // assigning s1 to s2 with clone
    let s2 = s1.clone();

    println!("s1 = {s1}, s2 = {s2}");

New value

  • When assigning a new value, the old value stored in the variable is dropped immediately.
  • The binding s stays in scope, but now owns the new value.
  • The new String allocates its own heap memory.
  • See Figure 4-5 in the book.
    let mut s = String::from("hello");

    // assigning new value to a variable
    s = String::from("ahoy");

    println!("{s}, world!");

Stack-only data

  • Types that implement the Copy trait are copied instead of moved.
  • Assignment creates a copy, so both variables remain valid.
  • These types are usually small, fixed-size values that do not require memory cleanup.
  • Examples:
    • integers
    • booleans
    • floating-point numbers
    • char
    • tuples containing only Copy types
    let x = 5;

    // assigning x to y
    let y = x;
    // x is not invalidated
    println!("x = {x}, y = {y}");

Ownership and functions

  • Passing a value to a function transfers ownership unless the type implements Copy.
  • Values that own heap data are dropped when their owner goes out of scope unless the ownership is transferred.
  • Ownership can be transferred into and out of functions through parameters and return values.

Passing value to a function

  • String is moved into the function.
  • i32 is copied because it implements Copy.
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // Because i32 implements the Copy trait,
                                    // x does NOT move into the function,
                                    // so it's okay to use x afterward.

} // Here, x goes out of scope, then s. However, because s's value was moved,
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.

Return Value

  • Returning a value moves ownership to the caller.
  • Function parameters and return values use the same move rules as assignments.
fn main() {
    let s1 = gives_ownership();        // gives_ownership moves its return
                                       // value into s1

    let s2 = String::from("hello");    // s2 comes into scope

    let s3 = takes_and_gives_back(s2); // s2 is moved into
                                       // takes_and_gives_back, which also
                                       // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {       // gives_ownership will move its
                                       // return value into the function
                                       // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                        // some_string is returned and
                                       // moves out to the calling
                                       // function
}

// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string comes into
    // scope

    a_string  // a_string is returned and moves out to the calling function
}

Reference & Borrowing

Reference

  • Allow to refer to the value without the change in the ownership -> reference borrows the value
  • References are immutable by default unless mut is used
  • Reference is guaranteed to point to a valid value of a particular type for the life of that reference
fn main() {
    let s1 = String::from("hello");

    // due to reference s1 will not be dropped after the function call
    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

// we can pass a reference to a function as a parameter, String ownership is not transferred
fn calculate_length(s: &String) -> usize {
    s.len()
}

Mutable reference

  • Reference can be mutable with mut
  • At a any point in time:
    • just one mutable reference can exist or
    • one or more immutable references can exist
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Dangling reference

  • Prevented by Rust compiler
fn main() {

    // will return error as there is no value to borrow from 
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

Tuple

Tuples

  • Group number of values with various types into one compound type.
  • Tuples have fixed length, i.e. once declared they cannot grow or shrink in size.
  • Tuple can be created as comma-separated list of values inside parentheses. Type annotation is optional.
  • An empty tuple is called unit.
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Destructuring

fn main () {
    let tup = (2, 4.3, "test");
    let (x, y, z) = tup;
    println!("Value of z is: {z}.");
}

Element Access

  • A tuple element can be accessed directly by using a period . followed by the index of the value we want to access (first index in a tuple is 0).
fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

Array

Arrays

  • Group number of values with the same types into one compound type.
  • Arrays have fixed length, i.e. once declared they cannot grow or shrink in size.
  • Array can be created as as a comma-separated list inside square brackets.
  • Type annotation is written using square brackets with the type of each element, a semicolon, and then the number of elements in the array.
fn main() {
    let a = [1, 2, 3, 4, 5];
    let b: [i32; 5] = [1, 2, 3, 4, 5]; // with explicit type annotation
    let c = [3; 5]; // equivalent to let c = [3, 3, 3, 3, 3];
}

Element Access

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

String

tbd

Slice

The slice type

  • Slice allows to reference a contiguous sequence of elements in collections (String, arrays, etc..)
  • Slice does not have ownership.

String slice

  • Starting index refers to the first position in the slice (indexing starts with 0) and is inclusive
  • Ending index refers to a position that is one more than the last position of the slice and is exclusive
  • Its type is &str and is immutable
let s = String::from("hello world");

    let hello = &s[0..5]; // "hello"
    let world = &s[6..11]; // "world"

    // "hello", 0 at the beginning can be ommitted
    let hello = &s[..5]; 

    // "world", going up to the last byte of the String
    let world = &s[6..]; 

    // "hello world", takes a slice of the entire String
    let hello_world = &s[..]; 

String literals

  • String literals are hardcoded in the binary
  • String literal is a slice pointing to that specific point in the binary
  • Its type is &str -> it is immutable reference
// string literal of type &str (string slice), it's immutable
let s = "Hello, world!";

Other slices - array

let a = [1, 2, 3, 4, 5];

// slice is of type &[i32]
let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

Struct

Defining struct

  • Definition outside of main function
  • key: value pair is called field
  • Struct’s data type is its name in the definition
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

Instantiating struct

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("[email protected]"),
        sign_in_count: 1,
    };

    // if the instance is mutable its values can be changed
    user1.email = String::from("[email protected]");
}

Function instantiating struct

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

Field Init Shorthand

  • nor need to repeat key: value if the function’s parameters and field’s names are the same
fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

Struct Update

  • new instance of struct includes values from another instance of the same type and updates the others
fn main() {
    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("[email protected]"),
        sign_in_count: user1.sign_in_count,
    };
}
  • it can be further simplified to:
fn main() {
    let user2 = User {
        email: String::from("[email protected]"),
        ..user1
    };
}
  • Struct update either moves/copies data depending on data types
  • In the example above user1 cannot be used anymore as its username changed the ownership due to its String data type

Tuple structs

  • Similar to tuples but the tuple itself has a name
  • Each tuple struct is its own type
  • As normal tuples:
    • no names associated with their fields
    • they can be destructured
    • individual values can be accessed with . followed by the index
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Unit-like struct

  • similar to unit type () (empty tuple)
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Method syntax

  • Similar to functions:
    • Declared with fn
    • Can take parameters and have return value
    • Contain code
  • Defined in the context of struct
  • Their first parameter is always self which represents the instance of the struct the method is being called on
  • Methods can take ownership so you can define parameter:
    • self (takes ownership)
    • &self (reference borrowing)
    • &mut self (mutable reference)
  • Note: parameter &self is equivalent of self: &self
  • Defined within impl (implementation) block
#[derive(Debug)] // allows to print struct with println macro
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()
    );
}

Multiple parameters in method

  • other parameters can be defined after self parameter as in function
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

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

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Associated functions

  • All functions defined within impl block are associated functions
  • They are associated with the type named after impl
  • Associated function is not method if parameter self is not present (they do not need an instance to work with)
  • Often used for constructors returning a new instance of struct
  • self in the return type is alias for the type that follows impl
  • Associated function is called with ::
fn main() {
   let sq = Rectangle::square(3);
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

Enum

Enums definition

  • A value can be one of a possible set of values (variant)
  • All variants have the same custom data type
// enum definition
enum IpAddrKind {
    V4,
    V6,
}

Enum values

  • Single function can take any variant as a parameter due to their common data type.
// instantiating enum
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// fn taking any variant as a parameter
fn route(ip_kind: IppAddrKind) {}

// function call
route(IpAddrKind::V4);
route(IpAddrKind::V6);

Enum associated data

  • Each enum variant can have different types and amounts of associated data
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Methods

  • Similarly to struct we can define methods on enum by using impl
    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();

The option enum

  • Enum to encode the concept of a value being present or absent. Value is either
    • sone(value) - it exist or
    • none - it does not exist
  • Part of the standard library and included in the prelude (no need to bring it into scope explicitly)
  • Type annotation:
    • some(value) - data type can be inferred
    • none - type annotation must be explicit
enum Option<T> {
    None,
    Some(T),
}

Practicle example:

fn safe_divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    let result = safe_divide(10, 2);

    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Cannot divide by zero"),
    }
}

Match

Match

  • match control flow construct allows to compare a value against a series of patterns and execute code based on which pattern matches.
  • Pattern can be made up of literal values, variable names, wildcards, etc..
  • match compares resultant value of an expression against the pattern of each arm and executes the code if the pattern matches.
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
} 

Patterns that bind to values

  • match arms can bind to the parts of the values that match the pattern
#[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 => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}

The Option match pattern

  • match asrms must be exhaustive
    fn plus_one(x: Option<i32>) -> Option<i32> {
        match x {
            None => None, // leaving out this arm will return a compilation error "non-exhaustive" petterns
            Some(i) => Some(i + 1),  // i binds to the value contained in Some, e.g. i = 5
        }
    }

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

Catch-all patterns

  • Allows to take special actions for a few particular values and default action for all others
  • Catch-all pattern should be last as the patterns are evaluated in order
let dice_roll = 9;
    match dice_roll {
        3 => add_fancy_hat(),
        7 => remove_fancy_hat(),
        other => move_player(other), // catch-all pattern
    }

    fn add_fancy_hat() {}
    fn remove_fancy_hat() {}
    fn move_player(num_spaces: u8) {}
  • if catch-all value is not needed, we can use _ instead:
    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() {}
  • if there is no code to execute for given pattern we can use unit type ()
    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 & let…else

If let

  • if let syntax can be used for instance for a match that runs code when the value matches one pattern and then ignores all other values.
  • if let takes a pattern and an expression from match separated by an equal sign
  • trade off between conciseness and exhaustive check
    // match version
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }

    // if let syntax
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }

Let…else

  • Used when you expect a pattern to match otherwise you exit right away
fn print_length(name: Option<String>) {
    let Some(name) = name else {
        return;
    };

    println!("Length: {}", name.len());
}