Skip to content

Instantly share code, notes, and snippets.

@hyojjxipitug
Last active March 12, 2026 10:27
Show Gist options
  • Select an option

  • Save hyojjxipitug/f056e6aa57643d0501c9577df7eef58b to your computer and use it in GitHub Desktop.

Select an option

Save hyojjxipitug/f056e6aa57643d0501c9577df7eef58b to your computer and use it in GitHub Desktop.

Rust personal CheatSheet

TODO: recap early sessions in the training

Intro

This document lists my personal notes for further reference taken during my Rust training on Ûdemy (Link)

Variable types

TODO complete this section

  • debug formatting: println!("{my_value:?}")

Integer

  • stored on the stack
  • implement copy trait, therefore the following creates two different values on the stack
let a: i32 = 5;
let mut b: i32 = a; // copies the original, we have now two different variables stored on the stack
b += 5;

println!("{a} {b}");

String vs. str

  • String - A dynamic piece of text stored on the heap at runtime

  • str - A hardcoded, read-only piece of text encoded in the binary

  • do not mistake &str

  • uses the String namespace

  • is stored on the heap

  • only a reference (a pointer) is stored in the stack

  • allocates some extra memory that can be used during string manipulation

let one: String = String::new();
let mut two: String = String::from("Two");
two.push_str(" Three");
println!("{two}");

Ownership

Base

  • a variable is more appropriately called a binding in Rust
  • The binding linked to a value is responsible for the clean up of the value upon leaving its scope
  • There can be only one owner at a time
  • the drop function is called upon cleaning of variable, i.e. when it goes out of scope. This only works for heap allocated variables
  • the clone method allows deep copy of object when allocated on the heap
  • you cannot take ownership to a value stored in another structure like an array or a tuple as it expects to remain owner of it. For example, the following is not allowed:
fn main() {
    let my_array = [String::from("one"), String::from("two"), String::from("three")];
    let a = my_array[0]; // not allowed; either borrow a reference or make a copy with clone()
}

Move

  • transfer of ownership from one owner to another
  • transfer happens if when copying variable that doesn't implement the copy trait. For example a String stored on the heap:
let person = String::from("Ben");
let employee = person; // the ownership has been transfered to the employee variable 

Borrowing & Reference

  • have a reference to a value without ownership
  • use the borrow operator &
fn main() {
    let my_stack_value: i32 = 2;
    let my_int_ref: &i32 = &my_stack_value; // note the data type

    let heap_value: String = String::from("horse");
    let heap_reference: &String = &heap_value;
}
  • reference like these are guaranteed to always be valid (References never outlive their referent)
  • de-reference operator: * (accesses the data at the reference)
let heap_reference: &String = &heap_value;
println!("{}", *heap_reference);
  • Rust ensures that references implement the display traits by using the display trait of the actual value behind the reference
  • The immutable references (not the mutable to avoid breaking the rule of single mutable reference to a value at the same time) implement the copy trait
let ref1: &str = "This is a test";
let ref2: &str = ref1; // we have now two different references on the stack pointing to the same encoded string in the binary
println!("{ref1} {ref2}");
  • a mutable reference is automatically dereferenced when useed:
  let mut value = String::from("test");
  let ref = &mut value;
  ref.push_str(" extension");
  • to borrow a mutable reference, the original variablee must also be mutable
  • there can be multiple immutable reference to a same value at the same time
  • However there can only be one single mutable reference to a value at a time. After a mutable reference has been created, it is not permitted to create even an immutable reference anymore
    • small caveat: the compiler is smart enough to detect if the mutable reference not used after some point and thereefore allows the creation of an immutable ref
  • a dangling reference is a reference to a value that has been cleared/deallocated. This is forbideen by the compiler. For example, the following is not allowed:
fn main() {}

fn create_city() -> &String {
    let city = String::from("Berlin");
    return &city;
}

Function parameters

  • by default, parameters are immutables. To change this:
fn main() {
    let my_string = String::from("This is a test");
    print_my_string(my_string);
}

fn print_my_string(mut value: String) {
    value.push_str(" [String extension]");
    println!("Your value is: {value}");
}
  • depending on the datatype, a function parameter will use a copy of the variable passed (if it implements the copy trait) or will move ownership
fn main() {
    let my_value: i32 = 155;
    print_my_value(my_value); // in this case the value is copied
    
    let my_string = String::from("This is a test");
    print_my_string(my_string); // ownership moved from the variable to the parameter.
    println!("my_string is not valid anymore: {my_string}");
}

fn print_my_value(value: i32) {
    println!("Your value is {value}");
}

fn print_my_string(value: String) {
    println!("Your value is {value}");
}
  • To only borrow a reference, change parameter declaration like this:
fn show_meal(meal: &String) { // fn show_meal(meal: &mut String) {...} if you want to modify it along the way
    println!("Meal: {meal}");
}

Return values

  • moves ownership from the function local variable to the caller via the return (implicit or explicit)
fn main() {
    let cake = bake_cake();
    println!("I have a cake: {cake}");
}

fn bake_cake() -> String {
    // let cake = String::from("Chocolate mousse");
    // return cake;
    
    String::from("Chocolate mousse")
}

Slices

  • a slice is a reference to a portion/sequence of a collection (e.g. array) up to 100% of the collection
fn main() {
    let name: String = String::from("Hyojj Xipitug");
    let firstname: &str = &name[0..5]; // From index 0 included up to index 5 NOT included

    println!("{firstname}");
}
  • slice boundaries can be skipped if we want to start fro, the beginning and/or finish at the end
fn main() {
    let action_hero = String::from("Arnold Schwarzenegger");

    let first_name = &action_hero[..6];
    println!("His first name is {first_name}.");
    
    let last_name = &action_hero[7..];
    println!("His last name is {last_name}.");
    
    let full_name = &action_hero[..];
    println!("His full name is {full_name}.");
}
  • deref coercion is the mechanism that Rust uses to deref an argument as long as possible until it reaches a valid type. This makes the following code valid:
fn do_hero_stuff(hero_name: &str) { // &String -> String -> &str
    println!("{hero_name} saves the day!");
}

fn main() {
    let action_hero = String::from("Arnold Schwarzenegger");
    do_hero_stuff(&action_hero); //deref coercion

    let another_hero = "Sylvester Stallone";
    do_hero_stuff(another_hero);
}
  • It is therefore more flexible to have &str as argument type
  • same logic of slicing with arrays:
fn main() {
    let values: [i32; 6] = [4, 8, 15, 16, 23, 42];

    let my_slice: &[i32] = &values[0..3]; // note that the type doesn't mention the length here
    println!("{my_slice:?}");
}
  • using slice allows more flexibility than the stricter reference:
fn main() {
    let values: [i32; 6] = [4, 8, 15, 16, 23, 42];

    let my_slice: &[i32] = &values[..]; // compare this type... (array slice)
    println!("{my_slice:?}");
    
    let my_slice: &[i32; 6] = &values; // ...with this type (reference to a full array, length included)
    println!("{my_slice:?}");
}
  • Rust does not allow mutable slice of string but well of arrays

Object Oriented Programming

  • Class == Struct in Rust
  • 3 types of struct:
    • Named fields structs
    • Tuple-like structs
    • Unit-like structs

Named fields struct example:

fn main() {
    struct CoffeeDrink {
        price: f64,
        name: String,
        is_hot: bool,
    }

    let mocha = CoffeeDrink {
        name: String::from("Mocha"),
        price: 4.99,
        is_hot: true,
    };

    println!("{}", mocha.name);
}
  • from an ownership point of view, the struct instance is the owner of its fields which are in turn owners of their respective values
[...]
let favorite_coffee = mocha.name; // /!\ this takes ownership of the inner field: movement of ownership
println!("{}", mocha.name); // will fail because not owner anymore
  • when declared mutable, a struct instance's fields are all mutable
  • Shorthand syntax to instantiate struct:
fn make_coffee(name: String, price: f64, is_hot: bool) -> CoffeeDrink {
    CoffeeDrink { 
        price, 
        name, 
        is_hot, 
    }
}
  • Struct update syntax: shortcut to populate part of the fields from another instance of the same struct (concerned fields must implement copy trait; reason explained below). The fields that are mentioned before the .. syntax are not overriden
struct CoffeeDrink {
    price: f64,
    name: String,
    is_hot: bool,
}

fn main() {
    let beverage = make_coffee(String::from("Latte"), 4.99, true);
    println!("{}", beverage.name);
    
    let caramel_machiato = CoffeeDrink {
        name: String::from("Caramel Machiato"),
        ..beverage // <= Magic happens here...
    };
    println!("{}", caramel_machiato.name);
}

fn make_coffee(name: String, price: f64, is_hot: bool) -> CoffeeDrink {
    CoffeeDrink { 
        price, 
        name, 
        is_hot, 
    }
}
  • the struct update can lead to ownership issue. If one the field does not implement the copy trait, it will lead to an ownership move instead. If need be, use clone() method
  • Here is how to derive (implement a default version) the debug trait (Trait is akin to interface in other languages):
#[derive(Debug)]
struct CoffeeDrink {
    price: f64,
    name: String,
    is_hot: bool,
}

fn main() {
    let beverage = 
    CoffeeDrink { 
        price: 4.99, 
        name: String::from("Latte"), 
        is_hot: true, 
    };
    println!("{:#?}", beverage);
}
  • Method implementation (⚠️ the self parameter can be a struct value or a reference; the later one is more flexible although the first one can be useful as well). Methods are implemented in a separated block from the datastructure declaration. There can be any arbitrary number of implementation blocks
#[derive(Debug)]
struct Song {
    title: String,
    release_year: u32,
    duration_sec: u32,
}

impl Song {
    fn display_song_info(&self) { // equivalent to self: &Self where Self is the current struct we are implementing
        println!("Song title is {}", self.title);
    }
}

fn main() {
    let song = Song{
        duration_sec: 100,
        release_year: 2026,
        title: String::from("Pif paf pouf"),
    };

    song.display_song_info();
}
  • A bit more convoluted example:
#[derive(Debug)]
struct Song {
    title: String,
    release_year: u32,
    duration_sec: u32,
}

impl Song {
    fn display_song_info(&mut self) { // the type 'Self' is a shortcut to mention the struct we are implementing
        println!("Song title is {}, released in {} and its duration is {}", self.title, self.release_year, self.duration_sec);
    }

    fn increase_duration(&mut self, extra_duration: u32) {
        self.duration_sec += extra_duration;
    }

    fn is_longer_than(&self, other: &Self) -> bool {
        self.duration_sec > other.duration_sec
    }
}

fn main() {
    let mut song = Song{
        duration_sec: 100,
        release_year: 2026,
        title: String::from("Pif paf pouf"),
    };
    
    song.display_song_info();
    song.increase_duration(15);
    song.display_song_info();
    
    let another = Song {
        duration_sec: 200,
        release_year: 2026,
        title: String::from("Wouapdouwap"),
    };
    println!("song longer than another? {}", song.is_longer_than(&another));
}
  • Associated functions are functions associated to a type (equivalent in other languages: static methods): simply do not mention self in the parameter list. They are invoked with :: syntax
impl Song {
  [...]
    fn new(title: String, release_year: u32, duration_sec: u32) -> Self {
        Self {
            title,
            release_year,
            duration_sec,
        }
    }
}

fn main() {
    let song = Song::new(String::from("Pif paf pouf"), 2026, 100);
    song.display_song_info();
}
  • builder pattern. Allows methods chaining in an elegant manner
#[derive(Debug)]
struct Computer {
    cpu: String,
    memory: u32,
    hard_drive_capacity: u32,
}

impl Computer {
    fn new(cpu: String, memory: u32, hard_drive_capacity: u32,) -> Self {
        Self {
            cpu,
            memory,
            hard_drive_capacity,
        }
    }

    fn upgrade_cpu(&mut self, cpu: String) -> &mut Self{
        self.cpu = cpu;
        self
    }

    fn upgrade_memory(&mut self, memory: u32) -> &mut Self{
        self.memory = memory;
        self
    }

    fn upgrade_hard_drive_capacity(&mut self, hard_drive_capacity: u32) -> &mut Self{
        self.hard_drive_capacity = hard_drive_capacity;
        self
    }
}

fn main() {
    let mut computer = Computer::new(String::from("M3"), 64, 1);
    computer
        .upgrade_cpu(String::from("M4 Pro"))
        .upgrade_memory(128)
        .upgrade_hard_drive_capacity(2);

    println!("{computer:#?}");
}

Tuple-like struct

  • fields are not named, only their positions matter
struct ShortDuration(u32, u32);

impl ShortDuration {
    fn diplay_info(&self) {
        println!("Workshift duration is {} hours, {} minutes", self.0, self.1);
    }
}

fn main() {
    let workshift = ShortDuration(7, 36);
    workshift.diplay_info();
}

Unit-like struct

  • Unit is an empty tuple
  • not often used, usually in some edge case design patterns. You may derive the debug trait as for structs
struct Empty;

fn main() {
    let my_empty = Empty;
}

Enums

  • not much to say, very comparable to equivalent in other languages
#[derive(Debug)]
enum CardSuit {
    Heart,
    Club,
    Diamond,
    Spade
}

fn main() {
    let first_card = CardSuit::Club;
    println!("The card's suit is {first_card:?}");
}
  • enum can be associated to value like so.
#[derive(Debug)]
enum PaymentType {
    CreditCard(String),
    DebitCard,
    PayPal,
}

fn main() {
    let visa = PaymentType::CreditCard(String::from("My account number here"));
}
  • there can be more than one associated value from different types
  • It is not mandatory for all variants to store the same data/datatype
  • associated data can alternatively be provided as a struct
#[derive(Debug)]
enum PaymentType {
    CreditCard(String),
    DebitCard,
    PayPal { username: String, password: String },
}

fn main() {
    let paypal = PaymentType::PayPal {
        password: String::from("password"),
        username: String::from("username"),
    };

    println!("{paypal:?}");
}
  • enum used with the match keyword:
enum OS {
    Windows,
    MacOS,
    Linux
}

fn main() {
    let computer = OS::MacOS;
    println!("MacOS was created {} years ago", years_since_release(&computer));
}

fn years_since_release (os: &OS) -> u32 {
    match os {
        OS::Windows => {
            println!("Some complex logic here...");
            39 // no semi-colon ; so it is considered an implicit return
        }
        OS::MacOS => 23, 
        OS::Linux => 34,
    }
}
  • it is possible to access the associated values of an enum in match structure like so:
enum LaundryCycle {
    Cold,
    Hot {temperature: u32},
    Delicate (String)
}

fn main() {
    let cold = LaundryCycle::Cold;
    wash_laundry(&cold);
    let silk = LaundryCycle::Delicate(String::from("silk"));
    wash_laundry(&silk);
}

fn wash_laundry(cycle: &LaundryCycle) {
    match cycle {
        LaundryCycle::Cold => {
            println!("Lavage à froid")
        }
        LaundryCycle::Hot {temperature} => {
            println!("Lavage à {temperature}°C")
        }
        LaundryCycle::Delicate (fabric_type) => {
            println!("Taking extra care of {fabric_type} fabric");
        }
    }
}
  • functions can be attached to an enum the same way it would to a struct. This leads to the following refactored code:
enum LaundryCycle {
    Cold,
    Hot {temperature: u32},
    Delicate (String)
}

impl LaundryCycle { 
    fn wash_laundry(&self) {
        match self {
            LaundryCycle::Cold => {
                println!("Lavage à froid")
            }
            LaundryCycle::Hot {temperature} => {
                println!("Lavage à {temperature}°C")
            }
            LaundryCycle::Delicate (fabric_type) => {
                println!("Taking extra care of {fabric_type} fabric");
            }
        }
    }
}

fn main() {
    LaundryCycle::Cold.wash_laundry();
    LaundryCycle::Delicate(String::from("silk")).wash_laundry();
    LaundryCycle::Hot { temperature: 80 }.wash_laundry();
}
  • match niceness
#[derive(Debug)]
enum OnlineOrderStatus {
    Ordered,
    Packed,
    Shipped,
    Delivered
}

impl OnlineOrderStatus {
    fn check(&self) {
        match self {
            OnlineOrderStatus::Ordered | OnlineOrderStatus::Packed => {
                println!("Still on our premises");
            }
            OnlineOrderStatus::Delivered => {
                println!("Your order has been delivered");
            }
            other => {
                println!("Your order has NOT been delivered: ({other:?})");
            }
        }
    }
}

fn main() {
    OnlineOrderStatus::Ordered.check();
    OnlineOrderStatus::Delivered.check();
    OnlineOrderStatus::Shipped.check();
}
  • match arms can be very specific in the way they match their values:
#[derive(Debug)]
enum Milk {
    LowFat(i32),
    WholeMilk,
}

impl Milk {
    // just for sport, we take ownership of the value this time
    // the value will therefore be cleaned at the end of the method
    fn drink(self) {
        match self {
            // will only match LowFat with exactly 2 associated value
            Milk::LowFat(2) => {
                println!("Very specific match arm");
            }
            Milk::LowFat(percent) => {
                // must come as second arm, otherwise it will always take precedence over the very specific arm
                println!("Drinking unspecific low fat milk ({percent}%)");
            }
            Milk::WholeMilk => {
                println!("Drinking whole milk");
            }
        }
    }
}

fn main() {
    Milk::WholeMilk.drink();
    Milk::LowFat(2).drink();
    Milk::LowFat(10).drink();
}
  • enums and if-let construction
#[derive(Debug)]
enum Milk {
    LowFat(i32),
    Whole,
    NonDairy {kind: String}
}

fn main() {
    let beverage = Milk::Whole;

    // if let <static hard-coded> = <dynamic value to compare against>
    // this way of doing allows to only verify one specific enum possibility
    // opposite to the match statement that checks everything
    if let Milk::Whole = beverage {
        println!("You have wole milk");
    }
}
  • Associated value can also be retrieved:
[...]
    if let Milk::LowFat(percent) = beverage {
        println!("Fat percentage : {percent}% ");
    }
  • let-else construct
#[derive(Debug)]
enum Milk {
    LowFat(i32),
    Whole,
    NonDairy {kind: String}
}

fn main() {
    let beverage = Milk::Whole;

    let Milk::LowFat(percent) = beverage else {
        // this block will only be executed if beverage is not LowFat
        return; // mandatory for the compiler, otherwise we could try to use percent later even if not defined
    };

    println!("{percent} retrieved in the let-else construct");
}
  • Option models situation where we can either have a valid value or none at all. Compare it to the Null value in other languages
fn main() {
    let a = Option::Some(5); // auto-detect the generic type based on the literal
    let b: Option<i8> = Option::Some(5); // one way to override the generic type
    let c = Option::<i8>::Some(5); // another way
    let d: Option<i32> = Option::None; // the None variant forces us to provide the generic type as it cannot infer it from the code
}
  • actual usage example
fn main() {
    let instruments = [
        String::from("Guitar"),
        String::from("Piano"),
        String::from("Bass"),
    ];

    // safe way to get an element without the risk of an out of bound exception
    // note that the generic type is Option<&String> to avoid taking array element ownership
    let bass: Option<&String> = instruments.get(2);
    println!("{bass:?}");

    let far_away: Option<&String> = instruments.get(200);
    println!("{far_away:?}");
}

Generics

  • a generic is a type argument == placeholder for future type
  • Behind the scene, the compiler generate all functions variants needed to compile. Therefore the types that can be handled must be known at compile time
fn identity<T> (value: T) -> T {
    value
}
  • Turbofish operator: ::<i32>, used to enforce the type to use with the generic
fn main() {
    let value = identity::<f64>(5);
    println!("{value}");
}

fn identity<T> (value: T) -> T {
    value
}
  • Generics work the same with structs:
struct TreasureChest<T> {
    captain: String,
    treasure: T,
}

impl TreasureChest<String> {
    // provide implementation specifically for the String generic variant
}

impl<T> TreasureChest<T> { // we need to mention the impl<T> otherwise the compiler would only try to resolve T as a type for a specific type (see above to understand the ambiguity)
    // provide an implementation for any type
}
  • Generic can also be used with enums
enum Cheesesteak<T> {
    Plain,
    Topping(T)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment