Interactive Rust Cheatsheet

An interactive guide to Rust concepts, data structures, and concurrency patterns.

0. Rust Fundamentals

This section covers the foundational concepts of Rust programming, essential for building any application.

Packages & Imports (use)

Rust code is organized into crates and modules. Use use to bring items into scope.

use std::io; // Bring the `io` module into scope
use std::collections::HashMap; // Bring a specific struct into scope

fn main() {
    println!("Hello, Rust!");
    let mut map = HashMap::new();
    map.insert("key", "value");
    println!("{:?}", map);
}

Basic Data Types

Rust is statically typed and has several built-in types.

  • Boolean: bool (true or false).
  • Numeric Types:
    • Integers: i8, i16, i32, i64, i128 (signed); u8, u16, u32, u64, u128 (unsigned); isize, usize (pointer-sized).
    • Floating-point: f32, f64.
  • Character: char (Unicode scalar value, 4 bytes).
  • Tuple: Fixed-size collection of different types.
  • Array: Fixed-size collection of same type.
  • Slice: Dynamic-size view into a collection.

Variables & Mutability

Variables are immutable by default. Use mut to make them mutable.

fn main() {
    let x = 5; // Immutable variable
    println!("The value of x is: {}", x);

    let mut y = 10; // Mutable variable
    println!("The value of y is: {}", y);
    y = 15;
    println!("The new value of y is: {}", y);

    const MAX_POINTS: u32 = 100_000; // Constants (must be annotated)
    println!("Max points: {}", MAX_POINTS);
}

Shadowing

You can declare a new variable with the same name as a previous variable, shadowing it.

fn main() {
    let x = 5;
    let x = x + 1; // x is shadowed by a new x
    let x = x * 2;
    println!("The value of x is: {}", x); // Output: 12
}

Input/Output

Using std::io for console I/O.

use std::io;

fn main() {
    println!("Hello, Rust!");
    println!("Please enter your name:");

    let mut name = String::new(); // Create a mutable, empty String
    io::stdin()
        .read_line(&mut name) // Read line into `name`
        .expect("Failed to read line");

    let name = name.trim(); // Shadow `name` with a trimmed version

    println!("Hello, {}!", name);
}

Operators

Rust supports standard arithmetic, comparison, logical, and bitwise operators.

  • **Arithmetic**: +, -, *, /, %
  • **Comparison**: ==, !=, <, >, <=, >=
  • **Logical**: && (AND), || (OR), ! (NOT)
  • **Bitwise**: &, |, ^, <<, >>

Control Flow

Statements that control the order of execution.

If-Else

fn main() {
    let number = 7;
    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }

    // `if` is an expression
    let condition = true;
    let num = if condition { 5 } else { 6 };
    println!("The value of num is: {}", num);
}

Match (Pattern Matching)

Powerful control flow operator for handling different cases of a value.

fn main() {
    let coin = Coin::Quarter;
    match coin {
        Coin::Penny => println!("Lucky penny!"),
        Coin::Nickel => println!("Five cents!"),
        Coin::Dime => println!("Ten cents!"),
        Coin::Quarter => println!("Twenty-five cents!"),
    }

    // Match with enum that holds data
    let msg = Message::Write(String::from("hello"));
    match msg {
        Message::Quit => println!("The Quit variant has no data."),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b),
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Loops

Rust has loop, while, and for loops.

fn main() {
    // `loop` loop (infinite loop, can return a value)
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {}", result); // Output: 20

    // `while` loop
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");

    // `for` loop (iterating over collections)
    let a = [10, 20, 30, 40, 50];
    for element in a.iter() {
        println!("the value is: {}", element);
    }

    // For loop with range
    for number in (1..4).rev() { // 3, 2, 1
        println!("{}!", number);
    }
}

Functions

Functions are declared with the fn keyword. Return values are the last expression in the function body (without a semicolon).

fn main() {
    another_function(5, 6);
    let x = five();
    println!("The value of x is: {}", x);
    let y = plus_one(5);
    println!("The value of y is: {}", y);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

fn five() -> i32 {
    5 // This is an expression, not a statement (no semicolon)
}

fn plus_one(x: i32) -> i32 {
    x + 1 // This is an expression, not a statement
}

1. Ownership & Borrowing

Rust's core memory safety features. Ownership rules are checked at compile time.

Ownership Rules

  • Each value in Rust has a variable that's called its *owner*.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped (memory freed).
fn main() {
    let s1 = String::from("hello"); // s1 owns "hello"
    let s2 = s1; // s1 is moved to s2; s1 is no longer valid
    // println!("{}", s1); // Compile-time error: borrow of moved value: `s1`

    let s3 = s2.clone(); // Deep copy, s2 and s3 are separate owners
    println!("s2: {}, s3: {}", s2, s3);

    takes_ownership(s2); // s2's value moves into the function, s2 is no longer valid
    // println!("{}", s2); // Compile-time error

    let x = 5; // Integers implement `Copy` trait, so they are copied
    let y = x;
    println!("x: {}, y: {}", x, y); // Both x and y are valid

    makes_copy(x); // x's value is copied into the function
    println!("x: {}", x); // x is still valid
}

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

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

Borrowing (References)

  • Allows you to use values without taking ownership.
  • References are immutable by default. Use &mut for mutable references.
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // Pass a reference to s1
    println!("The length of '{}' is {}.", s1, len); // s1 is still valid

    let mut s = String::from("hello");
    change(&mut s); // Pass a mutable reference
    println!("Modified string: {}", s);
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // s goes out of scope. Nothing is dropped.

fn change(some_string: &mut String) { // some_string is a mutable reference
    some_string.push_str(", world");
}

Rules of References

  • At any given time, you can have *either* one mutable reference *or* any number of immutable references.
  • References must always be valid (no dangling references).
fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // Immutable reference
    let r2 = &s; // Another immutable reference
    println!("{}, {}", r1, r2);
    // r1 and r2 go out of scope here, so we can create a mutable reference below

    let r3 = &mut s; // One mutable reference
    r3.push_str(" world");
    println!("{}", r3);

    // This would be a compile-time error:
    // let r4 = &s; // Cannot have immutable reference while mutable reference (r3) is active
    // println!("{}", r4);
}

Slices

References to contiguous sequence of elements in a collection, without taking ownership.

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5]; // Slice from index 0 to 5 (exclusive)
    let world = &s[6..11]; // Slice from index 6 to 11 (exclusive)

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

    let full_slice = &s[..]; // Slice of the entire string
    println!("{}", full_slice);

    let mut a = [1, 2, 3, 4, 5];
    let slice = &mut a[1..4]; // Mutable slice of an array
    slice[0] = 99;
    println!("{:?}", a); // Output: [1, 99, 3, 4, 5]
}

2. Data Structures

Rust's standard library provides robust and efficient data structures.

Vectors (Vec<T>)

  • Description: A growable list of values of the same type. Stored contiguously in memory.
  • Performance: Access by index: $O(1)$, Push/Pop (end): $O(1)$ amortized, Insert/Delete (middle/beginning): $O(N)$.
  • Thread Safety: Not inherently thread-safe for concurrent mutable access. Use Arc<Mutex<Vec<T>>> for shared mutable state.
fn main() {
    let mut v: Vec<i32> = Vec::new(); // Create an empty vector
    v.push(5);
    v.push(6);
    v.push(7);
    println!("{:?}", v); // Output: [5, 6, 7]

    let v2 = vec![1, 2, 3]; // Macro for creating a vector with initial values
    println!("{:?}", v2);

    let third: &i32 = &v[2]; // Access element by index
    println!("The third element is {}", third);

    // Iterating
    for i in &mut v {
        *i += 50; // Dereference to modify value
    }
    println!("{:?}", v);
}

Strings (String and &str)

  • Description: String is a growable, heap-allocated, UTF-8 encoded string. &str is a string slice (immutable view) into a String or string literal.
  • Performance: String append: $O(1)$ amortized, Concatenation: $O(N)$. &str operations: $O(1)$ (slice creation), $O(N)$ (iteration).
  • Thread Safety: String is not inherently thread-safe for concurrent mutable access. &str (immutable) is safe to share.
fn main() {
    let mut s = String::new(); // Empty mutable String
    s.push_str("hello"); // Append a string slice
    s.push(' '); // Append a character
    s.push_str("world");
    println!("{}", s); // Output: hello world

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");
    let s = format!("{}-{}-{}", s1, s2, s3); // Efficient concatenation
    println!("{}", s);

    let hello = "Здравствуйте"; // String literal (&str)
    for c in hello.chars() { // Iterate over Unicode characters
        print!("{} ", c);
    }
    println!();
}

Hash Maps (HashMap<K, V>)

  • Description: Stores key-value pairs using a hash table. Keys must be unique.
  • Performance: Insert/Lookup/Delete: $O(1)$ on average, $O(N)$ worst case (hash collisions).
  • Thread Safety: Not inherently thread-safe for concurrent mutable access. Use Arc<Mutex<HashMap<K, V>>> or DashMap (a concurrent hash map crate).
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    println!("{:?}", scores);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name); // Get a reference to the value
    println!("Blue team score: {:?}", score);

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

    // Insert only if key not present
    scores.entry(String::from("Blue")).or_insert(25);
    scores.entry(String::from("Green")).or_insert(25);
    println!("{:?}", scores);
}

Structs

  • Description: Custom data types that let you name and package together multiple related values.
  • Performance: Accessing fields: $O(1)$.
  • Thread Safety: Fields are not inherently thread-safe.
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

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

    let mut user2 = User {
        email: String::from("another@example.com"),
        username: String::from("anotherusername567"),
        active: true,
        sign_in_count: 1,
    };
    user2.email = String::from("newemail@example.com"); // Mutate field
    println!("User 2 new email: {}", user2.email);

    // Struct Update Syntax
    let user3 = User {
        email: String::from("third@example.com"),
        ..user2 // Take remaining fields from user2 (moves non-Copy types)
    };
    // println!("{}", user2.username); // Error: user2.username moved to user3
    println!("User 3 username: {}", user3.username);
}

Enums

  • Description: Allow you to define a type by enumerating its possible variants. Variants can optionally hold data.
  • Performance: Pattern matching is highly optimized by the compiler.
  • Thread Safety: Enums themselves are safe. Data held within enum variants follows standard ownership/borrowing rules.
enum IpAddrKind {
    V4,
    V6,
}

enum IpAddr { // Enum with data
    V4(u8, u8, u8, u8),
    V6(String),
}

enum Message { // More complex enum variants
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    let msg = Message::Write(String::from("hello"));
    // Use match to handle enum variants (see Control Flow section for example)
}

Comparison of Rust Built-in Data Structures

Data Structure Description Fixed/Dynamic Size Access (Index/Key) Insertion (Avg) Deletion (Avg) Ordered Unique Elements Thread Safety (Concurrent Mod.)
ArrayFixed-size sequence of elementsFixed$O(1)$N/AN/AYes (index order)NoNo (requires external sync)
Vec<T>Growable list (dynamic array)Dynamic$O(1)$$O(1)$ amortized (push), $O(N)$ (insert middle)$O(N)$Yes (insertion order)NoNo (requires external sync)
StringGrowable, heap-allocated UTF-8 textDynamic$O(1)$ (byte index), $O(N)$ (char index)$O(1)$ amortized (push_str)$O(N)$Yes (byte order)N/ANo (requires external sync)
&strImmutable string sliceFixed (view)$O(1)$ (byte index), $O(N)$ (char index)N/AN/AYes (byte order)N/AYes (immutable)
HashMap<K, V>Unordered key-value pairs (Hash Table)Dynamic$O(1)$ (avg), $O(N)$ (worst)$O(1)$ (avg), $O(N)$ (worst)$O(1)$ (avg), $O(N)$ (worst)NoKeys: YesNo (requires external sync)
StructCustom composite data typeFixed (fields)$O(1)$ (field access)N/AN/AYes (field declaration order)N/AFields not inherently safe (requires external sync)
EnumType with enumerated variantsN/AN/AN/AN/AN/AN/AVariants follow data rules

3. Concurrency (Threads, Channels, Shared State)

Rust's concurrency model emphasizes safety through its ownership system, preventing data races at compile time.

Threads (std::thread)

Create new OS threads using thread::spawn.

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| { // Closure runs in new thread
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap(); // Wait for the spawned thread to finish
    println!("Main thread finished.");
}

Message Passing (Channels - std::sync::mpsc)

Communicating between threads safely using Multiple Producer, Single Consumer (MPSC) channels.

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel(); // Create a new channel: (transmitter, receiver)

    thread::spawn(move || { // Move tx into the spawned thread
        let val = String::from("hi");
        tx.send(val).unwrap(); // Send value through the channel
        // println!("val is {}", val); // Error: val moved to tx.send()
    });

    let received = rx.recv().unwrap(); // Block until a value is received
    println!("Got: {}", received);

    // Multiple messages
    let (tx2, rx2) = mpsc::channel();
    let tx3 = mpsc::Sender::clone(&tx2); // Clone transmitter for multiple producers

    thread::spawn(move || {
        let msgs = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];
        for msg in msgs {
            tx2.send(msg).unwrap();
            thread::sleep(Duration::from_millis(100));
        }
    });

    thread::spawn(move || {
        let msgs = vec![
            String::from("and"),
            String::from("even"),
            String::from("more"),
            String::from("messages"),
        ];
        for msg in msgs {
            tx3.send(msg).unwrap();
            thread::sleep(Duration::from_millis(50));
        }
    });

    for received in rx2 { // rx2 acts as an iterator
        println!("Got: {}", received);
    }
}

Shared State Concurrency (Mutex<T> and Arc<T>)

Protecting shared mutable data with a mutex, shared across threads using an atomic reference counter.

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    // Arc enables multiple ownership over shared data
    // Mutex provides mutual exclusion for safe mutable access
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter); // Clone Arc for each thread
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap(); // Acquire lock, blocks if already locked
            *num += 1; // Mutate the protected data
            // Lock is automatically released when `num` goes out of scope
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap()); // Final count should be 10
}

4. Traits

Traits define shared behavior in an abstract way. They are similar to interfaces in other languages.

Defining a Trait

pub trait Summary {
    fn summarize(&self) -> String; // Required method
    
    // Default implementation (optional)
    fn summarize_author(&self) -> String {
        String::from("(Read more...)")
    }
}

Implementing a Trait for a Type

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

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

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

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    };
    println!("Tweet summary: {}", tweet.summarize());

    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup!"),
        location: String::from("Pittsburgh, PA"),
        author: String::from("Iceburgh"),
        content: String::from("The Pittsburgh Penguins once again won the Stanley Cup."),
    };
    println!("Article summary: {}", article.summarize());
    println!("Article author summary: {}", article.summarize_author()); // Using default implementation
}

// Trait as a parameter (Trait Bound Syntax)
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// Trait as a parameter (impl Trait syntax - syntactic sugar for trait bounds)
pub fn notify_sugar(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// Trait as a return type (only if returning single concrete type)
// pub fn returns_summarizable() -> impl Summary {
//     Tweet { /* ... */ }
// }

Trait Objects (Dynamic Dispatch)

Allows working with values of different types that implement the same trait, using runtime polymorphism.

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>, // Vector of trait objects
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        println!("Drawing a Button ({}x{}) with label: {}", self.width, self.height, self.label);
    }
}

pub struct SelectBox {
    pub width: u32,
    pub height: u32,
    pub options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        println!("Drawing a SelectBox ({}x{}) with options: {:?}", self.width, self.height, self.options);
    }
}

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 20,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

5. Error Handling (Result & Option)

Rust emphasizes explicit error handling using enums, rather than exceptions.

panic! (Unrecoverable Errors)

Used for unrecoverable errors, typically indicating a bug in your code.

fn main() {
    // panic!("crash and burn"); // This will cause the program to crash

    let v = vec![1, 2, 3];
    // v[99]; // This would panic at runtime if not caught by bounds checks
}

Option<T> (Absence of a Value)

An enum that represents the possibility of a value being present or absent. Used for situations where a value might or might not exist.

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    // Using `match` with Option
    let x = 5;
    let y: Option<i32> = Some(5);

    match y {
        Some(i) => println!("Value is: {}", i),
        None => println!("No value"),
    }

    // Using `if let` for concise matching
    if let Some(value) = some_number {
        println!("The value is: {}", value);
    } else {
        println!("No value present.");
    }

    // `unwrap()` and `expect()` (use with caution, can panic!)
    let value = some_number.unwrap(); // Panics if None
    let value = some_string.expect("String should be present"); // Panics with custom message if None
}

Result<T, E> (Recoverable Errors)

An enum that represents the possibility of either success (Ok(T)) or failure (Err(E)). Used for operations that might fail in a way you want to handle.

use std::fs::File;
use std::io::ErrorKind; // For specific error kinds

fn main() {
    let f = File::open("hello.txt"); // Returns a Result<File, io::Error>

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
    println!("File opened/created successfully: {:?}", f);

    // Using `unwrap()` and `expect()` with Result (use with caution!)
    // let f = File::open("another.txt").unwrap(); // Panics on Err
    // let f = File::open("another.txt").expect("Failed to open another.txt"); // Panics with custom message on Err

    // Using `?` operator for propagating errors
    // (Can only be used in functions that return Result)
    // fn read_username_from_file() -> Result<String, io::Error> {
    //     let mut f = File::open("username.txt")?; // Propagates error if any
    //     let mut s = String::new();
    //     f.read_to_string(&mut s)?; // Propagates error if any
    //     Ok(s)
    // }
}

6. Lifetimes

Lifetimes are a Rust concept that ensures references are always valid. They are a compile-time concept and don't affect runtime performance.

  • **Purpose**: Prevent dangling references (references that point to invalid memory).
  • **Syntax**: Lifetime annotations start with an apostrophe ('), e.g., 'a.
  • **Lifetime Elision Rules**: The compiler can often infer lifetimes, so you don't always need to explicitly write them.

Function Lifetimes

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);

    let string3 = String::from("long string is long");
    {
        let string4 = String::from("xyz");
        let result = longest(string3.as_str(), string4.as_str());
        println!("The longest string is {}", result);
    }
}

// Function signature with lifetime annotations
// 'a indicates that the returned reference will live as long as the shortest of the two input references.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Struct Lifetimes

If a struct holds references, you must specify lifetime parameters for those references.

struct ImportantExcerpt<'a> {
    part: &'a str, // This struct holds a reference with lifetime 'a
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };
    println!("Excerpt: {}", i.part);
}

7. Modules & Crates

Rust's code organization system for managing projects and dependencies.

  • **Crate**: The smallest unit of code that the Rust compiler considers. Can be a binary (executable) or a library.
  • **Module**: Organizes code within a crate for readability and reuse. Defines privacy (public/private).
  • **Cargo.toml**: The manifest file for a Rust project, defining dependencies and metadata.
  • **cargo new**: Creates a new Rust project (crate).
  • **cargo build**: Compiles the project.
  • **cargo run**: Compiles and runs the project.
  • **cargo test**: Runs tests.

Module Example

// src/main.rs
mod front_of_house { // Define a module
    pub mod hosting { // Public submodule
        pub fn add_to_waitlist() { // Public function
            println!("Added to waitlist!");
        }
    }
    mod serving { // Private submodule
        fn take_order() {}
    }
}

fn main() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();

    // Using `use` to bring into scope
    use crate::front_of_house::hosting;
    hosting::add_to_waitlist();
}

Adding a Dependency (Cargo.toml)

# Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5" # Example dependency

Then, in your Rust code:

use rand::Rng; // Use a function from the 'rand' crate

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
    println!("Secret number: {}", secret_number);
}

8. Testing

Rust has a built-in testing framework. Test code lives in functions annotated with #[test].

  • **Unit Tests**: Typically placed in the same file as the code they're testing, within a mod tests module annotated with #[cfg(test)].
  • **Integration Tests**: Placed in the tests directory at the root of your project.
// src/lib.rs (or src/main.rs for unit tests)
pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)] // Only compile when running tests
mod tests {
    use super::*; // Bring outer items into scope

    #[test] // Marks a function as a test
    fn it_works() {
        assert_eq!(4, add_two(2)); // Assertion macro
    }

    #[test]
    #[should_panic(expected = "less than or equal to 100")] // Expect a panic with specific message
    fn another_test() {
        // This test will pass if the code inside panics with the expected message
        panic_if_too_high(101);
    }

    fn panic_if_too_high(x: i32) {
        if x > 100 {
            panic!("Value {} is too high, must be less than or equal to 100", x);
        }
    }

    #[test]
    #[ignore = "reason for ignoring"] // Ignores this test by default
    fn expensive_test() {
        // This test won't run with `cargo test`
        // Run with `cargo test -- --ignored`
    }
}

// To run tests:
// cargo test
// cargo test -- --show-output // Show println! output from tests
// cargo test it_works // Run specific test
// cargo test -- --test-threads=1 // Run tests sequentially

9. Memory Management (Ownership, Borrowing, Drop Trait)

Rust's memory management is handled by its ownership system at compile time, eliminating garbage collectors and manual memory deallocation.

  • **Ownership**: (See section 1) Each value has a single owner. When the owner goes out of scope, the value is dropped.
  • **Borrowing**: (See section 1) References allow temporary access to owned data without transferring ownership.
  • **Drop Trait**: Allows you to customize what happens when a value is about to go out of scope.

Drop Trait

Implement the Drop trait to define custom cleanup logic for your types.

struct CustomSmartPointer {
    data: String,
}

// Implement the Drop trait for CustomSmartPointer
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
    // 'c' and 'd' will be dropped when they go out of scope,
    // calling their `drop` implementations automatically.

    // You cannot explicitly call `drop()` directly to clean up:
    // c.drop(); // Compile-time error: explicit use of destructor method
    // Use `std::mem::drop` if you need to force an early drop:
    // std::mem::drop(c);
    // println!("CustomSmartPointer c dropped before the end of main.");
}

10. Design Patterns

Common solutions to recurring problems in software design, adapted for Rust's unique features like ownership, traits, and enums.

Singleton

Ensures a class has only one instance and provides a global point of access to it. In Rust, this often involves lazy_static or once_cell crates for thread-safe lazy initialization.

use lazy_static::lazy_static; // Requires `lazy_static = "1.4.0"` in Cargo.toml
use std::sync::Mutex;

struct AppConfig {
    pub setting_a: String,
    pub setting_b: u32,
}

lazy_static! {
    static ref CONFIG: Mutex<AppConfig> = Mutex::new(AppConfig {
        setting_a: String::from("default_a"),
        setting_b: 100,
    });
}

impl AppConfig {
    pub fn get_instance() -> &'static Mutex<AppConfig> {
        &CONFIG
    }

    pub fn show_settings(&self) {
        println!("Setting A: {}, Setting B: {}", self.setting_a, self.setting_b);
    }
}

fn main() {
    let config_instance = AppConfig::get_instance();
    let locked_config = config_instance.lock().unwrap();
    locked_config.show_settings();
    drop(locked_config); // Explicitly release lock

    // Modify settings (requires mutable access)
    let mut mutable_config = config_instance.lock().unwrap();
    mutable_config.setting_a = String::from("updated_a");
    mutable_config.setting_b = 200;
    drop(mutable_config);

    let final_config = config_instance.lock().unwrap();
    final_config.show_settings();
}

Factory Method

Provides an interface for creating objects, allowing different implementations to decide which concrete type to instantiate.

trait Product {
    fn get_name(&self) -> String;
}

struct ConcreteProductA;
impl Product for ConcreteProductA {
    fn get_name(&self) -> String {
        "Product A".to_string()
    }
}

struct ConcreteProductB;
impl Product for ConcreteProductB {
    fn get_name(&self) -> String {
        "Product B".to_string()
    }
}

trait Creator {
    fn create_product(&self) -> Box<dyn Product>; // Returns a trait object
}

struct ConcreteCreatorA;
impl Creator for ConcreteCreatorA {
    fn create_product(&self) -> Box<dyn Product> {
        Box::new(ConcreteProductA)
    }
}

struct ConcreteCreatorB;
impl Creator for ConcreteCreatorB {
    fn create_product(&self) -> Box<dyn Product> {
        Box::new(ConcreteProductB)
    }
}

fn main() {
    let creator_a = ConcreteCreatorA;
    let product_a = creator_a.create_product();
    println!("Created: {}", product_a.get_name());

    let creator_b = ConcreteCreatorB;
    let product_b = creator_b.create_product();
    println!("Created: {}", product_b.get_name());
}

Observer

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Often implemented using channels or shared state with mutexes in Rust.

use std::sync::{Arc, Mutex};

trait Observer {
    fn update(&self, message: &str);
}

struct Subject {
    observers: Mutex<Vec<Arc<dyn Observer + Send + Sync>>>, // Observers are trait objects
}

impl Subject {
    fn new() -> Self {
        Subject {
            observers: Mutex::new(Vec::new()),
        }
    }

    fn attach(&self, observer: Arc<dyn Observer + Send + Sync>) {
        self.observers.lock().unwrap().push(observer);
    }

    fn detach(&self, observer_to_remove: Arc<dyn Observer + Send + Sync>) {
        let mut observers = self.observers.lock().unwrap();
        observers.retain(|obs| !Arc::ptr_eq(obs, &observer_to_remove));
    }

    fn notify_observers(&self, message: &str) {
        for observer in self.observers.lock().unwrap().iter() {
            observer.update(message);
        }
    }
}

struct ConcreteObserver {
    name: String,
}

impl Observer for ConcreteObserver {
    fn update(&self, message: &str) {
        println!("{} received update: {}", self.name, message);
    }
}

fn main() {
    let subject = Subject::new();
    let obs1 = Arc::new(ConcreteObserver { name: "Observer 1".to_string() });
    let obs2 = Arc::new(ConcreteObserver { name: "Observer 2".to_string() });

    subject.attach(Arc::clone(&obs1));
    subject.attach(Arc::clone(&obs2));

    subject.notify_observers("A new event occurred!");
    subject.detach(obs1); // Detach obs1
    subject.notify_observers("Another event!");
}

Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

trait PaymentStrategy {
    fn pay(&self, amount: u32);
}

struct CreditCardPayment;
impl PaymentStrategy for CreditCardPayment {
    fn pay(&self, amount: u32) {
        println!("Paying {} using Credit Card.", amount);
    }
}

struct PayPalPayment;
impl PaymentStrategy for PayPalPayment {
    fn pay(&self, amount: u32) {
        println!("Paying {} using PayPal.", amount);
    }
}

struct ShoppingCart {
    strategy: Box<dyn PaymentStrategy>, // Stores a trait object
}

impl ShoppingCart {
    fn new(strategy: Box<dyn PaymentStrategy>) -> Self {
        ShoppingCart { strategy }
    }

    fn set_payment_strategy(&mut self, strategy: Box<dyn PaymentStrategy>) {
        self.strategy = strategy;
    }

    fn checkout(&self, amount: u32) {
        self.strategy.pay(amount);
    }
}

fn main() {
    let mut cart = ShoppingCart::new(Box::new(CreditCardPayment));
    cart.checkout(100);

    cart.set_payment_strategy(Box::new(PayPalPayment));
    cart.checkout(50);
}

Decorator

Attaches additional responsibilities to an object dynamically.

trait Coffee {
    fn get_cost(&self) -> f64;
    fn get_ingredients(&self) -> String;
}

struct SimpleCoffee;
impl Coffee for SimpleCoffee {
    fn get_cost(&self) -> f64 { 5.0 }
    fn get_ingredients(&self) -> String { "Coffee".to_string() }
}

// Decorator struct (holds a Box<dyn Coffee>)
struct CoffeeDecorator {
    decorated_coffee: Box<dyn Coffee>,
}

// Concrete Decorators
struct MilkDecorator {
    base: CoffeeDecorator,
}

impl Coffee for MilkDecorator {
    fn get_cost(&self) -> f64 { self.base.decorated_coffee.get_cost() + 1.5 }
    fn get_ingredients(&self) -> String {
        format!("{}, Milk", self.base.decorated_coffee.get_ingredients())
    }
}

struct SugarDecorator {
    base: CoffeeDecorator,
}

impl Coffee for SugarDecorator {
    fn get_cost(&self) -> f64 { self.base.decorated_coffee.get_cost() + 0.5 }
    fn get_ingredients(&self) -> String {
        format!("{}, Sugar", self.base.decorated_coffee.get_ingredients())
    }
}

fn main() {
    let mut my_coffee: Box<dyn Coffee> = Box::new(SimpleCoffee);
    println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());

    my_coffee = Box::new(MilkDecorator { base: CoffeeDecorator { decorated_coffee: my_coffee } });
    println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());

    my_coffee = Box::new(SugarDecorator { base: CoffeeDecorator { decorated_coffee: my_coffee } });
    println!("Cost: {:.2}, Ingredients: {}", my_coffee.get_cost(), my_coffee.get_ingredients());
}

Visitor

Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

trait Visitor {
    fn visit_concrete_element_a(&self, element: &ConcreteElementA);
    fn visit_concrete_element_b(&self, element: &ConcreteElementB);
}

trait Element {
    fn accept(&self, visitor: &dyn Visitor);
}

struct ConcreteElementA;
impl ConcreteElementA {
    fn operation_a(&self) -> String { "ConcreteElementA".to_string() }
}
impl Element for ConcreteElementA {
    fn accept(&self, visitor: &dyn Visitor) { visitor.visit_concrete_element_a(self); }
}

struct ConcreteElementB;
impl ConcreteElementB {
    fn operation_b(&self) -> String { "ConcreteElementB".to_string() }
}
impl Element for ConcreteElementB {
    fn accept(&self, visitor: &dyn Visitor) { visitor.visit_concrete_element_b(self); }
}

struct ConcreteVisitor1;
impl Visitor for ConcreteVisitor1 {
    fn visit_concrete_element_a(&self, element: &ConcreteElementA) {
        println!("Visitor 1 processing {}", element.operation_a());
    }
    fn visit_concrete_element_b(&self, element: &ConcreteElementB) {
        println!("Visitor 1 processing {}", element.operation_b());
    }
}

struct ConcreteVisitor2;
impl Visitor for ConcreteVisitor2 {
    fn visit_concrete_element_a(&self, element: &ConcreteElementA) {
        println!("Visitor 2 processing {} differently.", element.operation_a());
    }
    fn visit_concrete_element_b(&self, element: &ConcreteElementB) {
        println!("Visitor 2 processing {} differently.", element.operation_b());
    }
}

fn main() {
    let elements: Vec<Box<dyn Element>> = vec![
        Box::new(ConcreteElementA),
        Box::new(ConcreteElementB),
    ];

    let visitor1 = ConcreteVisitor1;
    for elem in &elements {
        elem.accept(&visitor1);
    }

    let visitor2 = ConcreteVisitor2;
    for elem in &elements {
        elem.accept(&visitor2);
    }
}