Table of Contents

0. Setup & Environment

Install Rust via rustup

The officially recommended way to install Rust is via rustup, the Rust toolchain installer. It handles rustc, cargo, and the standard library in one shot.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify the installation
rustc --version
cargo --version
Use rustup, not Homebrew. brew install rust works but gives you a static toolchain. rustup lets you switch channels (stable/beta/nightly), add components, and update in place. The Rust project officially recommends rustup for all platforms.

Toolchain Management

rustup lets you install and switch between toolchain channels without leaving your terminal.

rustup update                        # update stable to latest release
rustup toolchain install nightly     # install nightly channel
rustup default nightly               # switch default (or use per-project)
rustup show                          # list installed toolchains + active one

# Add components (linter + formatter are essential)
rustup component add clippy rustfmt
A toolchain is a versioned set of compiler + std lib + tools tied to a channel (stable, beta, nightly). You can pin a project to a specific toolchain by adding a rust-toolchain.toml at the project root.

Essential Tools

These four commands cover the majority of your day-to-day Rust workflow.

cargo clippy          # linter — catches common mistakes and style issues
cargo fmt             # auto-formatter (uses rustfmt under the hood)
cargo doc --open      # generate HTML docs for your crate and open in browser

# cargo-watch: re-runs a command on every file save
cargo install cargo-watch
cargo watch -x run    # re-run on save (use -x check for faster feedback)

Editor Setup

VS Code + rust-analyzer is the most widely used setup and the one officially recommended by the Rust team. rust-analyzer is a full language server — it gives you completions, inline type hints, go-to-definition, rename refactors, and auto-imports out of the box.

# Install rust-analyzer component via rustup (keeps it in sync with your toolchain)
rustup component add rust-analyzer

Then install the rust-analyzer extension in VS Code (extension ID: rust-lang.rust-analyzer). JetBrains RustRover is a strong alternative if you prefer a full IDE.

Quick Verify

Once installed, confirm everything works end-to-end with a new project.

cargo new ~/rust-refresher
cd ~/rust-refresher
cargo run
# Compiling rust-refresher v0.1.0
# Finished dev profile
# Running `target/debug/rust-refresher`
# Hello, world!
cargo new initialises a git repo by default. Add --vcs none if you're inside an existing repo and don't want a nested one.

1. Program Basics

Cargo

Cargo is Rust's build system and package manager. Every Rust project is a crate managed by Cargo.

# Create new binary project
cargo new my_app           # creates my_app/src/main.rs
cargo new my_lib --lib     # creates my_lib/src/lib.rs

# Build and run
cargo build                # debug build → target/debug/
cargo build --release      # optimized → target/release/
cargo run                  # build + run
cargo run -- arg1 arg2     # pass args to the binary

# Testing
cargo test                 # run all tests
cargo test my_fn           # run tests matching name
cargo test -- --nocapture  # show println! output in tests

# Code quality
cargo fmt                  # format with rustfmt
cargo clippy               # linter (catches common mistakes)
cargo clippy -- -D warnings  # fail on any warning

# Documentation
cargo doc --open           # build and open docs in browser

# Dependency management
cargo add serde            # add dependency (requires cargo-edit)
cargo add serde --features derive
cargo update               # update Cargo.lock
cargo tree                 # show dependency tree
cargo audit                # check for security vulnerabilities

Crate Structure

// src/main.rs — binary entry point
fn main() {
    println!("Hello, world!");
}

// src/lib.rs — library crate root
pub mod utils;        // declares module, looks for src/utils.rs or src/utils/mod.rs
pub mod models;

// src/utils.rs — inline module file
pub fn helper() -> &'static str { "help" }

// src/models/mod.rs — module as directory
pub mod user;
pub mod post;

// src/models/user.rs
pub struct User { pub name: String }

// Visibility
pub fn public_fn() {}        // visible everywhere
pub(crate) fn crate_fn() {}  // visible within this crate
pub(super) fn parent_fn() {} // visible in parent module
fn private_fn() {}           // visible only in this module

Cargo.toml

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <[email protected]>"]
description = "A sample application"

[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
thiserror = "1"

[dev-dependencies]       # only for tests and benchmarks
proptest = "1"
mockall = "0.12"

[build-dependencies]     # for build.rs scripts
cc = "1"

[features]
default = ["std"]
std = []
async = ["tokio"]

[[bin]]                  # multiple binaries
name = "server"
path = "src/bin/server.rs"

[workspace]              # monorepo workspace
members = ["crate-a", "crate-b"]
resolver = "2"           # required for feature unification in workspaces

[profile.release]
opt-level = 3
lto = true               # link-time optimization
codegen-units = 1        # slower compile, faster binary
strip = true             # strip symbols
Editions
Rust has three stable editions: 2015 (original), 2018 (module system overhaul, async/await), 2021 (resolver v2, IntoIterator for arrays, disjoint capture in closures). Always use edition = "2021" for new projects. Editions are per-crate and fully backward compatible — all crates interoperate regardless of edition.

2. Type System

Scalar Types

TypeSizeRange / Notes
i8 / u81 byte-128..127 / 0..255
i16 / u162 bytes-32768..32767 / 0..65535
i32 / u324 bytesDefault integer type
i64 / u648 bytesLarge integers
i128 / u12816 bytesVery large integers
isize / usizepointer-sizedUsed for indexing/offsets
f324 bytesSingle-precision float
f648 bytesDefault float type
bool1 bytetrue / false
char4 bytesUnicode scalar value (U+0000 to U+10FFFF)
// Literals
let x: i32 = 1_000_000;   // underscores for readability
let y = 0xFF_u8;           // hex with type suffix
let z = 0b1010_u8;         // binary
let o = 0o77_u32;          // octal
let f = 3.14_f64;

// Casting (explicit, never implicit)
let a: i32 = 42;
let b = a as f64;          // i32 → f64
let c = b as u8;           // truncates: 42.0 → 42
let d = -1_i32 as u32;     // wraps: 4294967295

// Overflow behavior
let e: u8 = 255u8.wrapping_add(1); // 0 (wrapping)
let f = 200u8.saturating_add(100); // 255 (saturating)
let g = 200u8.checked_add(100);    // None (overflow)
let (h, overflowed) = 200u8.overflowing_add(100); // (44, true)

// char
let heart: char = '♥';
let unicode: char = '\u{1F600}';   // emoji

Compound Types

// Tuples — fixed size, mixed types
let tup: (i32, f64, bool) = (42, 3.14, true);
let (x, y, z) = tup;          // destructuring
let first = tup.0;             // index access

// Arrays — fixed size, single type (stack-allocated)
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 100];          // [0, 0, ..., 0] (100 elements)
let len = arr.len();           // 5
let slice: &[i32] = &arr[1..3]; // slice: [2, 3]

// Slices — dynamically sized view into contiguous memory
fn sum(nums: &[i32]) -> i32 {  // takes any slice, not just arrays
    nums.iter().sum()
}
sum(&arr);
sum(&arr[1..]);
sum(&vec![1, 2, 3]);

// Unit type: () — zero-sized, "void" equivalent
fn do_nothing() {}             // returns ()
let unit: () = ();

Type Inference & Turbofish

// Rust infers types from usage context
let nums = vec![1, 2, 3];           // Vec<i32> inferred
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();

// Turbofish ::<> — supply type when inference fails
let parsed = "42".parse::<i32>().unwrap();
let collected = (0..5).collect::<Vec<_>>();
let v = Vec::<String>::new();

// Type aliases
type Result<T> = std::result::Result<T, AppError>;
type Meters = f64;
type Callback = Box<dyn Fn(i32) -> i32>;

// Newtype pattern — wrapper with zero runtime cost
struct Meters(f64);
struct Seconds(f64);
// Prevents passing Meters where Seconds is expected

impl Meters {
    fn value(&self) -> f64 { self.0 }
}

// never type ! — functions that never return
fn exit_program() -> ! {
    std::process::exit(1);
}
// Also used in match arms that panic/break/continue
let x: i32 = match some_option {
    Some(n) => n,
    None => panic!("no value"),  // type is !, coerces to i32
};

3. Ownership & Borrowing

The Three Ownership Rules
  1. Each value in Rust has a single owner (a variable).
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (memory freed).

Move Semantics

// Move: ownership transfers, original is invalidated
let s1 = String::from("hello");
let s2 = s1;         // s1 MOVED into s2
// println!("{}", s1); // ERROR: s1 is moved

// Move into function
fn take_ownership(s: String) {
    println!("{}", s);
}  // s dropped here
let s = String::from("world");
take_ownership(s);
// println!("{}", s); // ERROR: moved

// Return gives ownership back
fn give_ownership() -> String {
    String::from("mine")
}
let s = give_ownership(); // ownership transferred to caller

// Clone: explicit deep copy
let s1 = String::from("hello");
let s2 = s1.clone();    // independent copy, both valid
println!("{} {}", s1, s2); // OK

Copy vs Clone

// Copy types: stack-only, bitwise copy on assignment
// All primitives implement Copy
let x: i32 = 5;
let y = x;       // x is COPIED, not moved
println!("{}", x); // still valid

// Types that implement Copy:
// i8..i128, u8..u128, f32, f64, bool, char, isize, usize
// Tuples of Copy types: (i32, bool) is Copy
// &T (shared references) are Copy
// *const T (raw pointers) are Copy
// Arrays of Copy types: [i32; 5] is Copy

// Types that do NOT implement Copy (heap-allocated):
// String, Vec<T>, Box<T>, HashMap, etc.

// Clone: explicit, potentially expensive
#[derive(Clone)]
struct Config {
    name: String,   // String is not Copy, so Config needs Clone
    value: i32,
}
let c1 = Config { name: "x".into(), value: 1 };
let c2 = c1.clone(); // deep copy

Borrowing Rules

The Borrowing Rules
At any given time, you can have either:
  • Any number of immutable references (&T), OR
  • Exactly one mutable reference (&mut T)
References must always be valid (no dangling pointers). These rules are enforced at compile time.
// Immutable borrow — many readers allowed
let s = String::from("hello");
let r1 = &s;
let r2 = &s;          // OK: multiple & allowed
println!("{} {}", r1, r2); // r1 and r2 used here, then dropped
// r1 and r2 no longer in scope after this point (NLL: Non-Lexical Lifetimes)

// Mutable borrow — exclusive access
let mut s = String::from("hello");
let r = &mut s;
r.push_str(", world");
println!("{}", r);
// Only one &mut at a time:
// let r2 = &mut s; // ERROR while r is in scope

// Common borrow errors
fn broken() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];   // immutable borrow
    v.push(4);           // ERROR: mutable borrow while immutable exists
    println!("{}", first);
}

fn fixed() {
    let mut v = vec![1, 2, 3];
    let first = v[0];    // Copy the value (i32 is Copy)
    v.push(4);           // OK
    println!("{}", first);
}

// Returning a reference from a function
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

4. Lifetimes

Lifetimes tell the compiler how long references are valid. They prevent dangling references and are checked entirely at compile time — zero runtime cost.

Lifetime Annotations

// 'a is a lifetime parameter (apostrophe prefix, any name)
// Reads: "the returned reference lives as long as both inputs"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Struct holding a reference — must annotate
struct Important<'a> {
    part: &'a str,   // 'a: struct cannot outlive this reference
}

impl<'a> Important<'a> {
    fn announce(&self, msg: &str) -> &str {
        // lifetime elision: returns same lifetime as &self
        self.part
    }
}

// Multiple lifetime parameters
fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x // returns x's lifetime, y's lifetime independent
}

Lifetime Elision Rules

The compiler infers lifetimes in common cases via three rules:

  1. Each reference parameter gets its own lifetime: fn f(x: &T)fn f<'a>(x: &'a T)
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
  3. If one of the inputs is &self or &mut self, its lifetime is assigned to all output lifetimes.
// These are equivalent after elision:
fn first(s: &str) -> &str { &s[0..1] }
fn first<'a>(s: &'a str) -> &'a str { &s[0..1] }

// Rule 3 in action — method returns borrow of self
impl Config {
    fn name(&self) -> &str {    // &self lifetime flows to return
        &self.name
    }
}

// When you MUST annotate (multiple refs, different lifetimes in output)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'static Lifetime

// 'static: lives for the entire program duration
let s: &'static str = "I am in the binary";

// String literals are always 'static
fn greet() -> &'static str { "hello" }

// 'static bound on generics: T must be 'static (no borrowed refs)
fn store<T: 'static>(val: T) {
    // val can be stored indefinitely (no borrowed data inside)
}

// Higher-Ranked Trait Bounds (HRTB)
// "for any lifetime 'a, T implements Fn(&'a str) -> &'a str"
fn apply<F>(f: F, s: &str) -> &str
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    f(s)
}
Lifetime Intuition
Lifetime annotations don't change how long things live — they just describe relationships to the borrow checker. The annotation 'a in fn f<'a>(x: &'a T) -> &'a T says "the output won't outlive the input," not "make the output live as long as possible."

5. Structs & Enums

Structs

// Named fields struct
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

// Instantiation
let user = User {
    username: String::from("alice"),
    email: String::from("[email protected]"),
    active: true,
    sign_in_count: 1,
};

// Struct update syntax
let user2 = User {
    email: String::from("[email protected]"),
    ..user  // remaining fields from user (moves username!)
};

// Tuple struct
struct Color(i32, i32, i32);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);

// Unit struct (no fields, useful for traits)
struct Marker;

// impl block
impl User {
    // Associated function (no self) — acts as constructor
    fn new(username: &str, email: &str) -> Self {
        User {
            username: username.to_string(),
            email: email.to_string(),
            active: true,
            sign_in_count: 0,
        }
    }

    // Method (takes &self, &mut self, or self)
    fn is_active(&self) -> bool { self.active }

    fn activate(&mut self) { self.active = true; }

    // Consuming method
    fn into_username(self) -> String { self.username }
}

// Deriving common traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Config {
    name: String,
    value: i32,
}

Enums

// Enum variants: unit, tuple, struct
enum Message {
    Quit,                        // unit variant
    Move { x: i32, y: i32 },   // struct variant
    Write(String),               // tuple variant
    ChangeColor(i32, i32, i32), // tuple variant
}

impl Message {
    fn call(&self) {
        match self {
            Message::Quit => println!("quit"),
            Message::Move { x, y } => println!("move to {x},{y}"),
            Message::Write(s) => println!("write: {s}"),
            Message::ChangeColor(r, g, b) => println!("color: {r},{g},{b}"),
        }
    }
}

// Option<T> — the "nullable" type (no null in Rust!)
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;

// Accessing Option values
let n = some_number.unwrap();               // panics on None
let n = some_number.unwrap_or(0);          // default on None
let n = some_number.unwrap_or_else(|| 42); // computed default
let n = some_number.expect("must have n"); // panics with message

// Option combinators
let doubled = some_number.map(|n| n * 2);         // Some(10)
let filtered = some_number.filter(|&n| n > 3);   // Some(5)
let chained = some_number.and_then(|n| if n > 0 { Some(n) } else { None });
let or_val = no_number.or(Some(99));              // Some(99)

// Result<T, E> — success or error
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("something went wrong");

ok.map(|n| n * 2);          // Ok(84)
ok.map_err(|e| format!("Error: {e}"));
ok.and_then(|n| Ok(n + 1)); // Ok(43)
err.unwrap_or(0);            // 0
err.unwrap_or_else(|_| -1);  // -1
ok.is_ok();  ok.is_err();    // true, false

6. Pattern Matching

match Expressions

// match is exhaustive — all cases must be handled
let num = 7;
let description = match num {
    1 => "one",
    2 | 3 => "two or three",       // or-pattern
    4..=6 => "four to six",        // inclusive range
    n if n < 0 => "negative",      // guard
    _ => "something else",         // wildcard catch-all
};

// Matching enums with destructuring
enum Shape { Circle(f64), Rect(f64, f64), Point }

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect(w, h) => w * h,
        Shape::Point => 0.0,
    }
}

// Binding with @ — test and bind
let n = 15;
match n {
    x @ 1..=12 => println!("month {x}"),
    x @ 13..=19 => println!("teen {x}"),
    x => println!("other {x}"),
}

// Destructuring structs in match
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
    Point { x: 0, y } => println!("on y-axis at {y}"),
    Point { x, y: 0 } => println!("on x-axis at {x}"),
    Point { x, y } => println!("({x}, {y})"),
}

// Matching references
let v = vec![1, 2, 3];
for &val in v.iter() {       // destructure the &i32
    println!("{val}");
}

if let / while let / let-else

// if let — match one pattern, ignore others
let some_val: Option<i32> = Some(42);
if let Some(n) = some_val {
    println!("got {n}");
} else {
    println!("nothing");
}

// if let chains (Rust 1.64+)
if let Some(n) = some_val && n > 10 {
    println!("big: {n}");
}

// while let — loop while pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{top}");
}

// let-else — early return if pattern fails (Rust 1.65+)
fn process(input: &str) -> Option<i32> {
    let Ok(n) = input.trim().parse::<i32>() else {
        return None;   // must diverge: return, break, continue, panic
    };
    Some(n * 2)
}

// Nested patterns
let nums = vec![Some(1), None, Some(3)];
for item in &nums {
    if let Some(n @ 1..=5) = item {
        println!("small: {n}");
    }
}

7. Traits

Defining and Implementing Traits

// Define a trait
trait Summary {
    fn summarize_author(&self) -> String;  // required method

    fn summarize(&self) -> String {        // default implementation
        format!("(Read more from {}...)", self.summarize_author())
    }
}

// Implement for a type
struct Article { title: String, author: String, content: String }

impl Summary for Article {
    fn summarize_author(&self) -> String { self.author.clone() }

    // Override default
    fn summarize(&self) -> String {
        format!("{}, by {} — {}", self.title, self.author, &self.content[..50])
    }
}

// Trait bounds
fn notify(item: &impl Summary) {      // impl Trait syntax (sugar)
    println!("Breaking news! {}", item.summarize());
}

fn notify_generic<T: Summary>(item: &T) { // equivalent
    println!("{}", item.summarize());
}

// Multiple bounds
fn complex<T: Summary + Clone + std::fmt::Debug>(item: T) {}

// Where clause (cleaner for complex bounds)
fn complex2<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: Summary + std::fmt::Debug,
{ t.summarize() + &u.summarize() }

// Return impl Trait (hides concrete type)
fn make_summary() -> impl Summary {
    Article { title: "t".into(), author: "a".into(), content: "c".into() }
}

Trait Objects (dyn Trait)

// Trait objects enable dynamic dispatch (vtable lookup)
// Must be object-safe: no generic methods, no Self return in most methods
fn notify_dyn(item: &dyn Summary) {
    println!("{}", item.summarize());
}

// Heterogeneous collections
let items: Vec<Box<dyn Summary>> = vec![
    Box::new(Article { title: "a".into(), author: "b".into(), content: "c".into() }),
    // Box::new(Tweet { ... }),  // different concrete type, same trait
];

for item in &items {
    println!("{}", item.summarize());
}

// Object safety rules: a trait is object-safe if:
// 1. It has no generic type parameters (on methods)
// 2. Methods don't use Self except in &self / &mut self position
// 3. No associated functions (no self parameter)

// Supertraits — require implementing another trait
trait Printable: std::fmt::Display + std::fmt::Debug {
    fn print(&self) { println!("{self}"); }
}

Key Standard Traits

TraitPurposeKey method
DisplayUser-facing formatting ({})fmt(&self, f: &mut Formatter) -> Result
DebugDebug formatting ({:?})Same, derivable
CloneExplicit deep copyclone(&self) -> Self
CopyImplicit bitwise copyMarker (no methods)
PartialEq / EqEquality comparisoneq(&self, other: &Self) -> bool
PartialOrd / OrdOrderingpartial_cmp / cmp
HashHashable (for HashMap keys)hash<H: Hasher>(&self, state: &mut H)
DefaultDefault valuedefault() -> Self
From / IntoType conversionfrom(val: T) -> Self
DerefDereference operatorderef(&self) -> &T
DropCustom destructordrop(&mut self)
IteratorIterationnext(&mut self) -> Option<Self::Item>
SendSafe to send across threadsMarker
SyncSafe to share across threadsMarker
use std::fmt;

struct Matrix(f64, f64, f64, f64);

impl fmt::Display for Matrix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({} {})\n({} {})", self.0, self.1, self.2, self.3)
    }
}

// From/Into (implementing From gives Into for free)
struct Wrapper(Vec<String>);

impl From<Vec<String>> for Wrapper {
    fn from(v: Vec<String>) -> Self { Wrapper(v) }
}

let w: Wrapper = vec!["a".to_string()].into(); // uses From impl

// Deref coercion
struct MyBox<T>(T);
impl<T> std::ops::Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

// Drop (RAII)
struct Resource { name: String }
impl Drop for Resource {
    fn drop(&mut self) {
        println!("Dropping {}", self.name);
    }
}
// Called automatically when Resource goes out of scope
// std::mem::drop(val); // force early drop

8. Generics

// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest { largest = item; }
    }
    largest
}

// Generic struct
struct Pair<T> { first: T, second: T }

impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self { Self { first, second } }
}

// Conditional method (only available when T: Display + PartialOrd)
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("largest: {}", self.first);
        } else {
            println!("largest: {}", self.second);
        }
    }
}

// Generic enum
enum Either<L, R> { Left(L), Right(R) }

// Const generics (stable since 1.51)
struct Matrix<const N: usize, const M: usize> {
    data: [[f64; M]; N],
}

impl<const N: usize, const M: usize> Matrix<N, M> {
    fn new() -> Self { Self { data: [[0.0; M]; N] } }
    fn rows(&self) -> usize { N }
    fn cols(&self) -> usize { M }
}

// PhantomData — carries type information without storing it
use std::marker::PhantomData;

struct TypedId<T> {
    id: u64,
    _phantom: PhantomData<T>, // zero-sized, just for the type
}

impl<T> TypedId<T> {
    fn new(id: u64) -> Self { Self { id, _phantom: PhantomData } }
    fn value(&self) -> u64 { self.id }
}

struct User;
struct Post;
let user_id: TypedId<User> = TypedId::new(1);
let post_id: TypedId<Post> = TypedId::new(1);
// user_id == post_id  // ERROR: different types!
Monomorphization
Generic code is compiled into separate concrete implementations for each type it is used with. largest::<i32> and largest::<f64> are distinct functions in the binary. This means zero runtime overhead compared to dynamic dispatch, at the cost of larger binary size and longer compile times.

9. Error Handling

The ? Operator

use std::fs;
use std::io;

// Without ?: explicit match on every Result
fn read_username_verbose() -> Result<String, io::Error> {
    let f = fs::File::open("hello.txt");
    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

// With ?: automatic early return on Err
fn read_username() -> Result<String, io::Error> {
    let mut s = String::new();
    fs::File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}

// ? also works on Option (returns None on None)
fn first_char(s: &str) -> Option<char> {
    let c = s.lines().next()?.chars().next()?;
    Some(c)
}

// ? calls From::from to convert error types
fn process() -> Result<(), AppError> {
    let content = fs::read_to_string("file.txt")?; // io::Error → AppError via From
    Ok(())
}

Custom Error Types

// Manual implementation
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Custom(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {e}"),
            AppError::Parse(e) => write!(f, "Parse error: {e}"),
            AppError::Custom(s) => write!(f, "Error: {s}"),
        }
    }
}

impl std::error::Error for AppError {}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

// Using thiserror crate (recommended for library code)
use thiserror::Error;

#[derive(Error, Debug)]
enum ServiceError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("not found: {id}")]
    NotFound { id: u64 },

    #[error("invalid input: {0}")]
    Validation(String),
}

// Using anyhow crate (recommended for application code)
use anyhow::{Context, Result, anyhow, bail};

fn run() -> Result<()> {
    let content = fs::read_to_string("config.toml")
        .context("failed to read config file")?;

    if content.is_empty() {
        bail!("config file is empty");  // creates Err and returns
    }

    if content.len() > 1000 {
        return Err(anyhow!("config too large: {} bytes", content.len()));
    }
    Ok(())
}
panic! vs Result
Use panic! for: unrecoverable bugs, violated invariants, prototype code, tests. Use Result for: expected failure cases, I/O, parsing, network calls — anything a caller should handle. In library code, never panic in public APIs (except for precondition violations with clear documentation).

10. Collections

Vec<T>

// Creation
let mut v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3, 4, 5];
let v = Vec::with_capacity(100); // pre-allocate

// Mutation
v.push(6);
v.pop();                 // Option<T>
v.insert(2, 99);         // insert at index
v.remove(2);             // remove and return element at index
v.retain(|&x| x % 2 == 0); // keep only evens
v.sort();
v.sort_by(|a, b| b.cmp(a)); // reverse sort
v.sort_by_key(|&x| std::cmp::Reverse(x));
v.dedup();               // remove consecutive duplicates (sort first)

// Access
let third = &v[2];           // panics if out of bounds
let third = v.get(2);        // Option<&T>
let slice = &v[1..3];        // &[T]

// Capacity management
println!("{} / {}", v.len(), v.capacity());
v.shrink_to_fit();

// Iteration
for x in &v { println!("{x}"); }        // immutable
for x in &mut v { *x *= 2; }           // mutable
for x in v { println!("{x}"); }         // consuming (moves)

String vs &str

// &str — immutable string slice (borrowed, can point to anywhere)
let s: &str = "hello";           // string literal, 'static
let s: &str = &owned[0..3];      // slice of a String

// String — owned, heap-allocated, growable UTF-8
let mut s = String::new();
let s = String::from("hello");
let s = "hello".to_string();
let s = "hello".to_owned();
let s = format!("{} {}", "hello", "world");

// Manipulation
s.push_str(" world");   // append str
s.push('!');            // append char
s + " more"             // consumes s, returns new String
let s = format!("{s} more"); // non-consuming

// Searching
s.contains("world");
s.starts_with("hel");
s.ends_with("ld");
s.find("wo");           // Option<usize> (byte offset)

// Slicing — must be on char boundaries!
let hello = &s[0..5];   // OK if valid UTF-8 boundary
// let bad = &s[0..1];  // might panic mid-codepoint!

// Iteration (always safe)
for c in s.chars() {}        // characters (Unicode scalar values)
for b in s.bytes() {}        // raw bytes
for (i, c) in s.char_indices() {}  // (byte_offset, char)

// Conversion
let n: i32 = "42".parse().unwrap();
let s: String = 42.to_string();

// Why you can't index with s[0]: UTF-8 is variable-width
// 'a' = 1 byte, '£' = 2 bytes, '日' = 3 bytes, '🦀' = 4 bytes
String Indexing Gotcha
"hello"[0] does not compile in Rust. Indexing into a string would return a byte, not a character, and could produce invalid UTF-8. Use .chars().nth(n) for character access (O(n)), or .as_bytes()[n] for ASCII-only byte access.

HashMap and BTreeMap

use std::collections::HashMap;

let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert("Alice".to_string(), 100);
scores.insert("Bob".to_string(), 85);

// Access
let alice = scores.get("Alice");          // Option<&i32>
let alice = scores["Alice"];              // panics if missing
let alice = scores.get("Alice").copied(); // Option<i32>

// Entry API — the idiomatic way to insert-or-update
scores.entry("Carol".to_string()).or_insert(50);  // insert if missing
scores.entry("Alice".to_string()).or_insert(0);   // no-op (already exists)

// Insert-or-update with computation
let text = "hello world hello";
let mut word_count: HashMap<&str, i32> = HashMap::new();
for word in text.split_whitespace() {
    let count = word_count.entry(word).or_insert(0);
    *count += 1;  // deref and increment
}

// Iteration
for (key, val) in &scores { println!("{key}: {val}"); }

// Remove
scores.remove("Bob");

// Contains
scores.contains_key("Alice");

// BTreeMap: sorted by key, O(log n) operations
use std::collections::BTreeMap;
let mut btree: BTreeMap<String, i32> = BTreeMap::new();
// Iterates in sorted key order

Other Collections

use std::collections::{HashSet, BTreeSet, VecDeque, BinaryHeap};

// HashSet / BTreeSet
let mut set: HashSet<i32> = HashSet::new();
set.insert(1); set.insert(2); set.insert(1); // duplicates ignored
set.contains(&1); // true
let a: HashSet<_> = [1, 2, 3].iter().cloned().collect();
let b: HashSet<_> = [2, 3, 4].iter().cloned().collect();
let union: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();
let diff: HashSet<_>  = a.difference(&b).collect();

// VecDeque — double-ended queue (efficient push/pop at both ends)
let mut dq: VecDeque<i32> = VecDeque::new();
dq.push_back(1);
dq.push_front(0);
dq.pop_front(); // Some(0)
dq.pop_back();  // Some(1)

// BinaryHeap — max-heap priority queue
let mut heap = BinaryHeap::new();
heap.push(3); heap.push(1); heap.push(4);
heap.peek();  // Some(&4)
heap.pop();   // Some(4) — removes max

// Min-heap via std::cmp::Reverse
heap.push(std::cmp::Reverse(3));

11. Iterators

Iterator Trait

// The core trait
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... hundreds of default methods built on next()
}

// Three ways to get an iterator from a collection
let v = vec![1, 2, 3];
v.iter()       // yields &T (borrows)
v.iter_mut()   // yields &mut T (mutable borrows)
v.into_iter()  // yields T (consumes collection)

// for loops desugar to IntoIterator::into_iter()
for x in &v { }      // calls (&v).into_iter() → same as v.iter()
for x in &mut v { }  // calls (&mut v).into_iter() → iter_mut()
for x in v { }       // calls v.into_iter() → consuming

Iterator Adaptors (Lazy)

let v = vec![1, 2, 3, 4, 5, 6];

// map — transform each element
let doubled: Vec<_> = v.iter().map(|&x| x * 2).collect();

// filter — keep elements matching predicate
let evens: Vec<_> = v.iter().filter(|&&x| x % 2 == 0).collect();

// filter_map — filter and transform in one step
let strings = vec!["1", "two", "3", "four"];
let nums: Vec<i32> = strings.iter()
    .filter_map(|s| s.parse().ok())
    .collect(); // [1, 3]

// flat_map — map then flatten
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter()
    .flat_map(|s| s.split_whitespace())
    .collect(); // ["hello", "world", "foo", "bar"]

// take / skip
let first3: Vec<_> = v.iter().take(3).collect();     // [1, 2, 3]
let after2: Vec<_> = v.iter().skip(2).collect();     // [3, 4, 5, 6]
let mid: Vec<_>    = v.iter().skip(1).take(3).collect(); // [2, 3, 4]

// zip — pair two iterators
let names = vec!["Alice", "Bob"];
let scores = vec![100, 85];
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
// [("Alice", 100), ("Bob", 85)]

// enumerate — add index
for (i, x) in v.iter().enumerate() {
    println!("{i}: {x}");
}

// chain — concatenate
let a = [1, 2];
let b = [3, 4];
let chained: Vec<_> = a.iter().chain(b.iter()).collect();

// peekable — look ahead without consuming
let mut iter = v.iter().peekable();
if iter.peek() == Some(&&1) {
    iter.next(); // consume it
}

// windows / chunks (on slices)
for window in v.windows(3) { }  // overlapping: [1,2,3],[2,3,4],...
for chunk in v.chunks(2) { }    // non-overlapping: [1,2],[3,4],[5,6]

Consuming Adaptors

let v = vec![1, 2, 3, 4, 5];

// collect — build a collection
let doubled: Vec<i32> = v.iter().map(|&x| x * 2).collect();
let set: HashSet<_> = v.iter().cloned().collect();
let string: String = v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(", ");

// fold / reduce
let sum = v.iter().fold(0, |acc, &x| acc + x);
let product = v.iter().copied().reduce(|acc, x| acc * x); // Option<T>

// sum / product
let sum: i32 = v.iter().sum();
let prod: i32 = v.iter().product();

// min / max
let max = v.iter().max();       // Option<&T>
let min = v.iter().min();
let max_by = v.iter().max_by_key(|&&x| x);

// any / all
let any_even = v.iter().any(|&x| x % 2 == 0);   // true
let all_pos  = v.iter().all(|&x| x > 0);         // true

// find / position
let first_even = v.iter().find(|&&x| x % 2 == 0); // Some(&2)
let pos = v.iter().position(|&x| x == 3);          // Some(2)

// count
let n = v.iter().filter(|&&x| x > 2).count(); // 3

// flatten
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<_> = nested.into_iter().flatten().collect();

// Custom iterator
struct Counter { count: u32, max: u32 }

impl Counter {
    fn new(max: u32) -> Self { Counter { count: 0, max } }
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

// All iterator adaptors work automatically
let sum: u32 = Counter::new(5).zip(Counter::new(5).skip(1))
    .map(|(a, b)| a * b)
    .filter(|x| x % 3 == 0)
    .sum();

12. Closures

// Closure syntax: |params| body
let add = |x: i32, y: i32| -> i32 { x + y }; // explicit types
let add = |x, y| x + y;   // types inferred from usage

// Closures capture from their environment
let threshold = 5;
let is_big = |x: i32| x > threshold; // captures threshold by reference

// Three closure traits (auto-selected by compiler):
// FnOnce — can be called once (may consume captured values)
// FnMut  — can be called multiple times, may mutate captures
// Fn     — can be called multiple times, no mutation needed
// Every closure implements FnOnce. Fn implies FnMut implies FnOnce.

// FnOnce example
let s = String::from("hello");
let consume = move || {
    drop(s);   // consumes s, can only call once
};
consume();
// consume(); // ERROR: called once, FnOnce not FnMut

// FnMut example
let mut count = 0;
let mut increment = || { count += 1; count };
increment(); // 1
increment(); // 2

// Fn example (most general, read-only)
let x = 10;
let get_x = || x; // borrows x immutably, Fn
get_x(); get_x(); // can call many times

// move closure — takes ownership of captures
let s = String::from("hello");
let owns_s = move || println!("{s}"); // s moved into closure
// Thread spawning typically requires move
std::thread::spawn(move || { owns_s(); });

// Returning closures (must box or use impl Fn)
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8

// Box when concrete type not knowable at compile time
fn make_adder_dyn(x: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

// Closures as function parameters
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}
apply_twice(|x| x + 3, 7); // 13

13. Smart Pointers

Box<T>

// Box: heap allocation, single owner
let b = Box::new(5);
println!("{}", *b);  // deref to get value

// Use cases:
// 1. Recursive types (size not known at compile time)
enum List {
    Cons(i32, Box<List>),  // without Box, infinite size
    Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// 2. Large data — avoid stack copy
let large = Box::new([0u8; 1_000_000]);

// 3. Trait objects
let drawable: Box<dyn Draw> = Box::new(Button { ... });

Rc<T> and Arc<T>

use std::rc::Rc;
use std::sync::Arc;

// Rc: reference-counted, single-threaded shared ownership
let a = Rc::new(5);
let b = Rc::clone(&a);   // increments count, cheap (not deep clone)
let c = Rc::clone(&a);
println!("count: {}", Rc::strong_count(&a)); // 3
// Dropped when count reaches 0

// Arc: atomically reference-counted, thread-safe
let val = Arc::new(vec![1, 2, 3]);
let val2 = Arc::clone(&val);
std::thread::spawn(move || {
    println!("{:?}", val2);
});

// Weak references (prevent cycles)
use std::rc::Weak;
let strong = Rc::new(5);
let weak: Weak<i32> = Rc::downgrade(&strong);
// To use: weak.upgrade() -> Option<Rc<T>>
if let Some(val) = weak.upgrade() {
    println!("{}", val);
}
// weak.upgrade() returns None if strong count is 0

RefCell<T> and Interior Mutability

use std::cell::{Cell, RefCell};

// RefCell: runtime borrow checking (single-threaded)
// Useful when you need mutability inside immutable context
let data = RefCell::new(vec![1, 2, 3]);

data.borrow();            // immutable borrow (panics if &mut exists)
data.borrow_mut();        // mutable borrow (panics if any borrow exists)
data.try_borrow();        // returns Result instead of panicking
data.try_borrow_mut();

// Common pattern: Rc<RefCell<T>> for shared mutable state
let shared = Rc::new(RefCell::new(0));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
*a.borrow_mut() += 1;
*b.borrow_mut() += 1;
println!("{}", shared.borrow()); // 2

// Cell: for Copy types, no borrow tracking
let cell = Cell::new(5);
cell.set(10);
println!("{}", cell.get()); // 10

// Mutex: thread-safe interior mutability
use std::sync::Mutex;
let mutex = Mutex::new(0);
{
    let mut guard = mutex.lock().unwrap(); // blocks until acquired
    *guard += 1;
}  // guard drops here, lock released

// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let lock = RwLock::new(vec![1, 2, 3]);
let readers = lock.read().unwrap();  // multiple allowed
drop(readers);
let mut writer = lock.write().unwrap(); // exclusive
writer.push(4);

Cow<T> and Pin<T>

use std::borrow::Cow;

// Cow (Clone-on-Write): borrowed until mutation needed
fn ensure_no_spaces(s: &str) -> Cow<str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', "_"))   // allocates only if needed
    } else {
        Cow::Borrowed(s)                  // no allocation
    }
}

let s = ensure_no_spaces("hello world"); // allocates
let s = ensure_no_spaces("hello");       // no allocation

// Pin: prevents value from being moved in memory
// Required for self-referential types (async state machines)
use std::pin::Pin;
use std::marker::Unpin;

// Most types auto-implement Unpin (safe to move)
// Pin<Box<T>> where T: !Unpin prevents moves
// Used extensively in async/await internals

14. Concurrency

Threads

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

// Spawn a thread
let handle = thread::spawn(|| {
    for i in 0..5 {
        println!("thread: {i}");
        thread::sleep(Duration::from_millis(10));
    }
});

// Wait for thread to finish
handle.join().unwrap(); // returns Result with thread's return value

// move closure to transfer ownership into thread
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("{:?}", v); // v moved into thread
});
handle.join().unwrap();

// Scoped threads (Rust 1.63) — can borrow from outer scope
thread::scope(|s| {
    let v = vec![1, 2, 3];
    s.spawn(|| println!("{:?}", &v)); // borrows v (safe, scope ensures join)
    s.spawn(|| println!("{:?}", &v)); // multiple borrows OK
}); // all spawned threads joined here automatically

Message Passing

use std::sync::mpsc; // multi-producer, single-consumer

let (tx, rx) = mpsc::channel();

// Multiple producers via clone
let tx2 = tx.clone();

thread::spawn(move || {
    tx.send("hello from thread 1").unwrap();
});

thread::spawn(move || {
    tx2.send("hello from thread 2").unwrap();
});

// Receiving
let msg = rx.recv().unwrap();       // blocking
let msg = rx.try_recv();            // non-blocking, Err if empty
for msg in rx { println!("{msg}"); } // iterate until all senders drop

// Sending multiple values
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    let vals = vec!["a", "b", "c"];
    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_millis(100));
    }
});
for received in rx {
    println!("{received}");
}

Atomics

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;

// Atomic counter — no Mutex needed
let counter = Arc::new(AtomicUsize::new(0));
let c = Arc::clone(&counter);

thread::spawn(move || {
    c.fetch_add(1, Ordering::SeqCst);
});

// Ordering options (strongest to weakest):
// SeqCst   — sequentially consistent, strongest guarantee
// AcqRel   — acquire + release (for read-modify-write)
// Acquire  — for loads (see all writes before corresponding Release)
// Release  — for stores (all writes visible to Acquire)
// Relaxed  — no ordering guarantees, only atomicity

// Common atomic operations
counter.load(Ordering::Acquire);
counter.store(42, Ordering::Release);
counter.fetch_add(1, Ordering::Relaxed);
counter.fetch_sub(1, Ordering::Relaxed);
counter.compare_exchange(old, new, Ordering::AcqRel, Ordering::Acquire);

// Shutdown flag pattern
let running = Arc::new(AtomicBool::new(true));
let r = Arc::clone(&running);
thread::spawn(move || {
    while r.load(Ordering::Relaxed) {
        // do work
    }
});
running.store(false, Ordering::Relaxed); // signal shutdown
Send and Sync
Send: a type can be transferred to another thread (ownership). Sync: a type can be shared between threads via references (&T is Send). These are marker traits implemented automatically. Rc<T> is neither Send nor Sync (use Arc). RefCell<T> is Send but not Sync (use Mutex). Raw pointers are neither.

15. Async / Await

Basics

// async fn returns an impl Future
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// Futures are lazy — nothing runs until .await or executor polls
// Must run inside an async runtime (tokio, async-std)

// tokio runtime
#[tokio::main]
async fn main() {
    let result = fetch_data("https://example.com").await;
    println!("{:?}", result);
}

// Manual runtime setup
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        fetch_data("https://example.com").await.unwrap();
    });
}

// async block — creates anonymous future
let fut = async {
    let x = some_async_fn().await;
    x + 1
};
let result = fut.await;

Tokio Patterns

use tokio::time::{sleep, Duration};
use tokio::sync::{Mutex, mpsc};

// Concurrent execution — join! runs all futures concurrently
async fn concurrent() {
    let (a, b, c) = tokio::join!(
        fetch("url1"),
        fetch("url2"),
        fetch("url3"),
    ); // all three run concurrently, waits for all
}

// Select — run until first completes
async fn race() {
    tokio::select! {
        result = fetch("url1") => { println!("url1 won: {:?}", result); }
        result = fetch("url2") => { println!("url2 won: {:?}", result); }
        _ = sleep(Duration::from_secs(5)) => { println!("timeout!"); }
    }
}

// Spawning tasks (like goroutines)
let handle = tokio::spawn(async {
    sleep(Duration::from_millis(100)).await;
    42
});
let result = handle.await.unwrap(); // JoinHandle<T>

// Spawn many tasks
let handles: Vec<_> = (0..10)
    .map(|i| tokio::spawn(async move { process(i).await }))
    .collect();
let results = futures::future::join_all(handles).await;

// Async channel (tokio)
let (tx, mut rx) = mpsc::channel(32); // buffer size
tokio::spawn(async move {
    tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
    println!("{msg}");
}

// Async Mutex (needed when holding across .await)
let shared = Arc::new(Mutex::new(0));
let s = Arc::clone(&shared);
tokio::spawn(async move {
    let mut guard = s.lock().await; // async lock, doesn't block thread
    *guard += 1;
});

Futures and Streams

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

// Implementing Future manually (rarely needed)
struct Delay { deadline: std::time::Instant }

impl Future for Delay {
    type Output = ();
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        if std::time::Instant::now() >= self.deadline {
            Poll::Ready(())
        } else {
            // register waker so executor will re-poll
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

// Stream — async iterator (from futures or tokio-stream crate)
use tokio_stream::StreamExt;
use tokio_stream::wrappers::ReceiverStream;

let (tx, rx) = tokio::sync::mpsc::channel(10);
let mut stream = ReceiverStream::new(rx);

while let Some(item) = stream.next().await {
    println!("{item}");
}
Common Async Pitfalls
  • Blocking in async: Never call blocking I/O (std::fs, std::net) in an async task. Use tokio::task::spawn_blocking to offload to a thread pool.
  • Holding locks across .await: Holding a std::sync::MutexGuard across an .await point causes deadlocks. Use tokio::sync::Mutex instead.
  • Large futures on the stack: Deep .await chains can create huge stack frames. Use Box::pin to move futures to the heap.
  • async closures: Use || async move { ... } pattern; true async closures are unstable.

16. Macros

Declarative Macros (macro_rules!)

// Basic macro
macro_rules! say_hello {
    () => { println!("Hello!"); };
}
say_hello!(); // Hello!

// With arguments — fragment specifiers
macro_rules! create_fn {
    ($fn_name:ident) => {
        fn $fn_name() { println!("fn {:?}()", stringify!($fn_name)); }
    };
}
create_fn!(foo); // creates fn foo()

// Repetition with $( )* (zero or more) and $( )+ (one or more)
macro_rules! vec_of_strings {
    ($($x:expr),*) => {
        vec![$( $x.to_string() ),*]
    };
}
let v = vec_of_strings!["hello", "world"]; // Vec<String>

// Multiple match arms
macro_rules! my_assert {
    ($left:expr, $right:expr) => {
        if $left != $right {
            panic!(
                "assertion failed: {:?} != {:?} (left={:?}, right={:?})",
                $left, $right, $left, $right
            );
        }
    };
    ($cond:expr) => {
        if !$cond { panic!("assertion failed: {:?}", stringify!($cond)); }
    };
}

// Fragment specifiers:
// expr   — expression
// ident  — identifier (variable/function name)
// ty     — type
// pat    — pattern
// block  — block expression { ... }
// stmt   — statement
// item   — item (fn, struct, impl, ...)
// literal — literal
// meta   — attribute content
// tt     — any single token tree (most flexible)

// Common built-in macros
println!("{:?}", val);          // print with newline
eprintln!("error: {}", e);     // print to stderr
format!("{} {}", a, b);        // String
vec![1, 2, 3];                 // Vec::new() + push
assert!(cond);                 // panic if false
assert_eq!(a, b);              // panic if a != b
assert_ne!(a, b);              // panic if a == b
todo!();                       // marks unimplemented code, panics
unimplemented!();              // similar to todo!
unreachable!();                // marks unreachable code
dbg!(val);                     // prints file/line/value, returns val
include_str!("file.txt");      // embed file as &str at compile time
concat!("a", "b", "c");       // "abc" at compile time

Procedural Macros

// Three types of proc macros — defined in their own crate (proc-macro = true)

// 1. Derive macros — #[derive(MyTrait)]
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream { /* ... */ }

// Usage:
#[derive(Debug, Clone, Serialize, Deserialize)] // common derive macros
struct MyStruct { field: String }

// 2. Attribute macros — #[my_attribute]
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }

// Usage (e.g., actix-web style):
#[get("/hello")]
async fn hello() -> &'static str { "Hello!" }

// 3. Function-like macros — my_macro!(...)
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }

// Usage:
let query = sql!(SELECT * FROM users WHERE id = $1);

17. Unsafe Rust

Unsafe is Not "Skip the Rules"
unsafe unlocks five specific capabilities. All other Rust safety guarantees still apply inside unsafe blocks. You are promising the compiler you've upheld those invariants manually.
// The 5 unsafe superpowers:
// 1. Dereference raw pointers
// 2. Call unsafe functions/methods
// 3. Access/modify mutable static variables
// 4. Implement unsafe traits
// 5. Access fields of unions

// Raw pointers — can be null, dangling, unaligned
let x = 5;
let r1 = &x as *const i32;          // raw immutable pointer
let mut y = 5;
let r2 = &mut y as *mut i32;        // raw mutable pointer
let r3: *const i32 = std::ptr::null(); // null pointer

// Creating raw pointers is safe; dereferencing is unsafe
unsafe {
    println!("{}", *r1);    // dereference
    *r2 = 10;               // write through raw pointer
    // *r3                  // UB: null dereference!
}

// Unsafe functions
unsafe fn dangerous() {
    // caller promises invariants are upheld
}
unsafe { dangerous(); }

// Safe wrapper around unsafe code (common pattern)
fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();
    assert!(mid <= len);
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

// FFI — calling C from Rust
extern "C" {
    fn abs(input: i32) -> i32;
    fn strlen(s: *const std::ffi::c_char) -> usize;
}
unsafe { abs(-3); }

// Calling Rust from C
#[no_mangle]  // don't mangle the name
pub extern "C" fn call_from_c() {
    println!("Called from C!");
}

// Mutable statics (unsafe to access, may cause data races)
static mut COUNTER: u32 = 0;
unsafe {
    COUNTER += 1;
    println!("{}", COUNTER);
}

// Unsafe traits (Send, Sync are unsafe traits)
unsafe trait MyUnsafeTrait {}
unsafe impl MyUnsafeTrait for i32 {}

18. Testing

// Unit tests — in the same file, conditionally compiled
pub fn add(a: i32, b: i32) -> i32 { a + b }

pub fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

#[cfg(test)]
mod tests {
    use super::*;  // import everything from parent module

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 3), 6);
        assert!(add(2, 3) > 4);
    }

    #[test]
    fn test_divide() {
        assert_eq!(divide(10.0, 2.0), Some(5.0));
        assert_eq!(divide(10.0, 0.0), None);
    }

    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("this must panic");
    }

    #[test]
    #[should_panic(expected = "divide by zero")]
    fn test_specific_panic() {
        // panic message must contain "divide by zero"
        panic!("attempt to divide by zero");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        // Can return Result — Err causes test failure
        let n = "42".parse::<i32>().map_err(|e| e.to_string())?;
        assert_eq!(n, 42);
        Ok(())
    }

    #[test]
    #[ignore]
    fn expensive_test() {
        // cargo test -- --ignored  to run ignored tests
    }
}
// Integration tests — in tests/ directory (separate crate)
// tests/integration_test.rs
use my_crate::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 3), 5);
}

// Test utilities and helpers
#[cfg(test)]
mod test_helpers {
    pub fn make_user() -> crate::User {
        crate::User::new("test", "[email protected]")
    }
}

// Running tests
// cargo test                     — all tests
// cargo test test_add            — tests matching name
// cargo test -- --test-threads=1 — single-threaded
// cargo test -- --nocapture      — show stdout
// cargo test -- --ignored        — run ignored tests only

// Benchmarks (requires nightly or criterion crate)
// Using criterion (stable, recommended)
use criterion::{criterion_group, criterion_main, Criterion};

fn bench_add(c: &mut Criterion) {
    c.bench_function("add", |b| {
        b.iter(|| add(criterion::black_box(2), criterion::black_box(3)))
    });
}

criterion_group!(benches, bench_add);
criterion_main!(benches);
Property-Based Testing with proptest
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_add_commutative(a: i32, b: i32) {
        // For any a and b, a+b == b+a
        assert_eq!(add(a, b), add(b, a));
    }

    #[test]
    fn test_parse_roundtrip(n in 0i32..1000) {
        // Any number in range survives string roundtrip
        let s = n.to_string();
        let parsed: i32 = s.parse().unwrap();
        assert_eq!(n, parsed);
    }

    #[test]
    fn test_string_not_empty(s in "[a-z]+") {
        // Regex strategy for strings
        assert!(!s.is_empty());
    }
}

19. Common Patterns & Idioms

Builder Pattern

// Fluent builder for complex construction
#[derive(Debug)]
struct Request {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    timeout_ms: u64,
    body: Option<String>,
}

struct RequestBuilder {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
    timeout_ms: u64,
    body: Option<String>,
}

impl RequestBuilder {
    fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            method: "GET".to_string(),
            headers: vec![],
            timeout_ms: 30_000,
            body: None,
        }
    }
    fn method(mut self, m: impl Into<String>) -> Self { self.method = m.into(); self }
    fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.headers.push((k.into(), v.into())); self
    }
    fn timeout(mut self, ms: u64) -> Self { self.timeout_ms = ms; self }
    fn body(mut self, b: impl Into<String>) -> Self { self.body = Some(b.into()); self }
    fn build(self) -> Request {
        Request {
            url: self.url, method: self.method,
            headers: self.headers, timeout_ms: self.timeout_ms,
            body: self.body,
        }
    }
}

let req = RequestBuilder::new("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .timeout(5_000)
    .body(r#"{"name": "Alice"}"#)
    .build();

Typestate Pattern

// Use the type system to enforce state machine transitions at compile time
struct Locked;
struct Unlocked;

struct Safe<State> {
    value: String,
    _state: std::marker::PhantomData<State>,
}

impl Safe<Locked> {
    fn new(value: String) -> Self {
        Safe { value, _state: std::marker::PhantomData }
    }

    fn unlock(self, pin: &str) -> Result<Safe<Unlocked>, &'static str> {
        if pin == "1234" {
            Ok(Safe { value: self.value, _state: std::marker::PhantomData })
        } else {
            Err("wrong pin")
        }
    }
}

impl Safe<Unlocked> {
    fn read(&self) -> &str { &self.value }

    fn lock(self) -> Safe<Locked> {
        Safe { value: self.value, _state: std::marker::PhantomData }
    }
}

// Compile-time enforcement: can't read from a Locked safe
let safe = Safe::new("secret".to_string());
// safe.read();  // ERROR: method not found in Safe<Locked>
let unlocked = safe.unlock("1234").unwrap();
println!("{}", unlocked.read());
let locked_again = unlocked.lock();

Extension Traits

// Add methods to foreign types without wrapping them
trait StrExt {
    fn word_count(&self) -> usize;
    fn to_title_case(&self) -> String;
}

impl StrExt for str {
    fn word_count(&self) -> usize {
        self.split_whitespace().count()
    }
    fn to_title_case(&self) -> String {
        self.split_whitespace()
            .map(|w| {
                let mut c = w.chars();
                match c.next() {
                    None => String::new(),
                    Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
                }
            })
            .collect::<Vec<_>>()
            .join(" ")
    }
}

"hello world foo".word_count();     // 3
"hello world".to_title_case();      // "Hello World"

Deref Coercion

// Automatic dereferencing chains when types don't match
// String → &str, Vec<T> → &[T], Box<T> → &T

fn greet(name: &str) { println!("Hello, {name}!"); }

let owned = String::from("Alice");
greet(&owned);        // &String coerces to &str via Deref

let boxed = Box::new(String::from("Bob"));
greet(&boxed);        // &Box<String> → &String → &str (chain!)

fn sum(nums: &[i32]) -> i32 { nums.iter().sum() }
let v = vec![1, 2, 3];
sum(&v);              // &Vec<i32> coerces to &[i32]

// Deref coercion in method calls
let s = Box::new(String::from("hello"));
s.len();     // Box deref → String deref → str.len()
s.contains("hell"); // works without manual derefs

From / Into Patterns

// Implement From, get Into for free
#[derive(Debug)]
struct Celsius(f64);
#[derive(Debug)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

let boiling = Celsius(100.0);
let f: Fahrenheit = boiling.into();  // Into uses From impl
let f = Fahrenheit::from(Celsius(0.0));

// impl Into<String> — accept any type convertible to String
fn store_name(name: impl Into<String>) {
    let name: String = name.into();
}
store_name("literal");           // &str → String
store_name(String::from("owned")); // already String, no alloc
store_name(format!("dynamic {}", 42));

20. Common Pitfalls & Gotchas

String vs &str Confusion

// Pitfall: function takes String when &str is more flexible
fn bad(s: String) { println!("{s}"); }    // forces allocation on caller
fn good(s: &str)  { println!("{s}"); }   // accepts String, &str, anything

// Pitfall: trying to return a &str from local String
fn get_str() -> &str {           // ERROR: what lifetime?
    let s = String::from("hello");
    &s                           // s dropped at end of function!
}
// Fix: return String, or accept an output buffer, or use 'static
fn get_str() -> String { String::from("hello") }

// Pitfall: indexing a String
let s = String::from("hello");
// let c = s[0];     // ERROR: can't index String
let c = s.chars().nth(0); // Option<char> — O(n)
let b = s.as_bytes()[0];  // u8 — fine for ASCII

Borrow Checker Pitfalls

// Pitfall: mutating collection while iterating
let mut v = vec![1, 2, 3, 4, 5];
// for x in &v { v.push(*x); } // ERROR: cannot borrow mutably while borrowed
// Fix: collect indices or use retain/extend
let extras: Vec<i32> = v.iter().copied().collect();
v.extend(extras);

// Pitfall: multiple mutable references to struct fields
struct Foo { a: i32, b: i32 }
let mut foo = Foo { a: 1, b: 2 };
let a = &mut foo.a;
let b = &mut foo.b;  // OK! Rust understands disjoint fields (Rust 2021+)

// Pitfall: borrow in match arm extends too long
let mut map = std::collections::HashMap::new();
map.insert("key", 1);
// Can't do this in older Rust (NLL fixed it in Rust 2018 edition)
if let Some(v) = map.get("key") {
    map.insert("other", *v); // OK in Rust 2018+ (NLL)
}

// Pitfall: closure capturing &mut and later using the variable
let mut x = 5;
let mut add_to_x = |n| { x += n; }; // borrows x mutably
add_to_x(3);
// println!("{x}"); // ERROR: x still mutably borrowed by closure
drop(add_to_x);    // closure dropped, borrow ends
println!("{x}");   // OK: 8

Orphan Rule

// Orphan rule: you can implement a trait for a type ONLY IF:
// - the trait is defined in your crate, OR
// - the type is defined in your crate (or at least one type parameter)

// ERROR: both Display (std) and Vec (std) are foreign
// impl std::fmt::Display for Vec<i32> { }

// OK: your trait on foreign type
trait MyTrait { fn info(&self) -> String; }
impl MyTrait for Vec<i32> { fn info(&self) -> String { format!("{:?}", self) } }

// OK: foreign trait on your type
struct MyVec(Vec<i32>);
impl std::fmt::Display for MyVec {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

// The Newtype pattern works around the orphan rule
struct Wrapper(Vec<String>);
impl std::fmt::Display for Wrapper { /* ... */ }

Mutex Deadlocks

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

// Pitfall 1: lock poisoning — a thread panics while holding a lock
let m = Arc::new(Mutex::new(0));
let m2 = Arc::clone(&m);
std::thread::spawn(move || {
    let _guard = m2.lock().unwrap();
    panic!("panic while holding lock!"); // lock is now "poisoned"
});
// Later:
match m.lock() {
    Ok(guard) => { /* normal */ }
    Err(poisoned) => {
        let guard = poisoned.into_inner(); // recover from poison
        println!("recovered: {}", *guard);
    }
}

// Pitfall 2: deadlock — lock A, then B in one thread; B then A in another
// Prevention: always acquire locks in the same order
// Or use try_lock() with backoff
// Or use parking_lot crate which provides better deadlock detection

// Pitfall 3: holding MutexGuard across .await (blocks executor thread)
// BAD:
async fn bad_async(m: &Mutex<i32>) {
    let guard = m.lock().unwrap();  // std::sync::MutexGuard is !Send
    some_async_fn().await;          // guard held across await!
    drop(guard);
}
// GOOD: use tokio::sync::Mutex for async code

Performance Pitfalls

// Pitfall: unnecessary .clone() — first check if borrow works
fn bad(v: Vec<i32>) -> i32 { v.iter().sum() }       // takes ownership
fn good(v: &[i32]) -> i32   { v.iter().sum() }       // borrows

// Pitfall: .to_string() in hot paths — prefer format! or display directly
let n = 42;
let s = n.to_string();    // allocates
println!("{n}");           // no allocation

// Pitfall: collecting when iteration suffices
let sum: i32 = v.iter().map(|x| x * 2).sum(); // lazy, no intermediate Vec
// vs:
let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();
let sum: i32 = doubled.iter().sum(); // unnecessary allocation

// Pitfall: Using String::from in tight loops
// Prefer &str where possible; allocate only when needed

// Pitfall: Box<dyn Fn> when impl Fn works
// Dynamic dispatch is slower than static dispatch
fn apply_dyn(f: &Box<dyn Fn(i32) -> i32>, x: i32) -> i32 { f(x) }
fn apply_static<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } // inlined

// Pitfall: large enum variants
enum Command {
    Quit,                          // 0 bytes data
    Print(String),                 // 24 bytes
    Upload(Vec<u8>),              // 24 bytes
}
// enum size = max variant size. If one is huge, box it:
enum Command {
    Quit,
    Print(Box<String>),           // just 8 bytes (pointer)
    Upload(Box<Vec<u8>>),
}

Cargo Feature Gotchas

Feature Unification
In a Cargo workspace, if crate A depends on serde with feature derive, and crate B depends on serde without it, Cargo will unify them — B gets derive too. This means features are additive: you can't have a feature off if any other crate in the dependency graph has it on. Never gate behavior on a feature being absent.

Async + Lifetime Difficulties

// Pitfall: async fn with reference params — hidden lifetime issues
// This does NOT work as you might expect:
async fn broken(s: &str) -> usize { s.len() }
// The return Future captures the lifetime of &str:
// fn broken<'a>(s: &'a str) -> impl Future<Output = usize> + 'a

// Pitfall: storing async fn in a trait requires boxing
// Traits with async methods need special handling:
use async_trait::async_trait; // crate: async-trait

#[async_trait]
trait AsyncStore {
    async fn save(&self, data: &str) -> Result<(), String>;
}

#[async_trait]
impl AsyncStore for MyStore {
    async fn save(&self, data: &str) -> Result<(), String> {
        Ok(())
    }
}

// Native async traits are stable in Rust 1.75+ for simple cases
// but async_trait is still needed for trait objects (dyn AsyncStore)