COMPLETE RUST Β· ZERO TO PRODUCTION Β· 12 MODULES

Rust β€” Basics to Advanced

Every core Rust concept from your first variable to async runtimes and unsafe internals. Code examples for every concept. Quizzes after each module. Designed to build understanding layer by layer.

01Variables & Types 02Functions & Control Flow 03Ownership & Borrowing 04Structs & Enums 05Error Handling 06Traits & Generics 07Collections & Iterators 08Closures & Functional 09Lifetimes β€” Deep 10Async / Tokio 11Unsafe Rust & FFI 12Memory Layout & Patterns
MODULE 01 Variables, Types & Primitives FOUNDATION

Variables β€” Immutable by Default

// Variables are IMMUTABLE by default β€” a core safety feature let x = 5; // immutable β€” cannot reassign let mut y = 5; // mutable β€” can reassign y = 10; // OK // x = 10; // ERROR: cannot assign twice to immutable variable // Shadowing β€” rebind the same name (creates a NEW variable) let z = 5; let z = z + 1; // new z shadows old z. Value: 6 let z = "now a string"; // shadowing can even change the type! // Shadowing != mut: mut keeps the type, shadowing can change it // Constants β€” always immutable, type annotation required, compile-time value const MAX_POINTS: u32 = 100_000; // _ is visual separator // Cannot use runtime values in const β€” must be a literal or const expression // Static β€” single memory address for program lifetime static GREETING: &str = "Hello"; // 'static lifetime, stored in binary static mut COUNTER: u32 = 0; // mutable static β€” requires unsafe to access

Integer Types

  • Signed: i8, i16, i32, i64, i128, isize
  • Unsigned: u8, u16, u32, u64, u128, usize
  • Default: i32 β€” fastest on most modern CPUs
  • isize/usize: pointer-sized β€” always use for indexing and lengths
  • Overflow in debug: panics. In release: wraps around silently.
  • Safe ops: checked_add(), saturating_add(), wrapping_add()
  • Literals: 0xFF (hex), 0o77 (octal), 0b1010 (binary), 1_000_000 (visual)

Float, Bool, Char

  • f32: 32-bit IEEE 754 float
  • f64: 64-bit float β€” the default, more precise
  • Float arithmetic: +, -, *, /, % (remainder)
  • bool: true / false β€” 1 byte in memory
  • char: Unicode scalar value β€” 4 bytes (NOT 1 byte like C)
  • char can be any valid Unicode: 'A', 'Γ±', 'δΈ­', 'πŸ˜€'
  • char is NOT a u8 β€” they are different types in Rust

Compound Types

  • Tuple: (i32, f64, bool) β€” fixed size, mixed types
  • Access tuple: tup.0, tup.1 β€” dot notation with index
  • Destructure: let (x, y, z) = tup;
  • Unit tuple: () β€” "void", the empty tuple, 0 bytes
  • Array: [i32; 5] β€” fixed length, same type, stack allocated
  • Array access: arr[0] β€” panics if out of bounds in debug
  • Slice: &[i32] β€” view into array/vec β€” fat pointer (ptr + len)

String vs &str

  • &str: immutable reference to UTF-8 bytes β€” borrowed, no allocation
  • String: owned, heap-allocated, growable UTF-8 string
  • String literals are &'static str β€” baked into binary
  • Convert &str β†’ String: "hi".to_string() or String::from("hi")
  • Convert String β†’ &str: &my_string (deref coercion)
  • Function params: prefer &str β€” accepts both String and &str
  • Indexing by char: use .chars() β€” bytes β‰  chars in Unicode

Type Inference & Casting

  • Rust infers types from usage β€” most annotations are optional
  • Explicit cast: let x = 2.9f64 as i32; β€” truncates (not rounds) β†’ 2
  • No implicit coercions β€” all conversions must be explicit
  • Parse: "42".parse::<i32>() β€” returns Result
  • From: safe infallible conversion β€” i32::from(5u8)
  • Into: inverse of From β€” let x: i64 = 5i32.into()
  • TryFrom/TryInto: fallible conversion β€” returns Result

Quiz β€” Variables & Types

Q1. What is the default integer type in Rust when no annotation is given?

Q2. How large is a char in Rust?

Q3. What is the key difference between let mut x and let x / let x (shadowing)?

MODULE 02 Functions & Control Flow BASICS

Functions

// fn keyword, snake_case names, type annotations required on params fn add(x: i32, y: i32) -> i32 { x + y // no semicolon = expression = implicit return value } // Explicit return β€” only needed for early returns fn safe_divide(a: f64, b: f64) -> Option<f64> { if b == 0.0 { return None; } // early return Some(a / b) // implicit return } // Unit type () is the default return β€” Rust's "void" fn greet(name: &str) { // implicitly -> () println!("Hello, {}!", name); } // Statements vs Expressions β€” fundamental distinction in Rust // Statement: performs action, no value, ends with semicolon // Expression: evaluates to a value, no semicolon let y = { // block is an expression let x = 3; x + 1 // last line without ; = value of block }; // y = 4 // Adding ; to last line makes it a statement β€” block evaluates to ()

Control Flow

// if is an expression β€” use in let bindings let num = 7; let label = if num % 2 == 0 { "even" } else { "odd" }; // Both arms must return the same type! // loop β€” infinite, use break to exit (can return a value) let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } // returns 20 }; // Labeled loops β€” break/continue specific outer loop 'outer: for i in 0..5 { for j in 0..5 { if i + j == 6 { break 'outer; } } } // while let mut n = 3; while n != 0 { println!("{}!", n); n -= 1; } // for β€” idiomatic Rust, always use over while for iteration for i in 0..5 { print!("{} ", i); } // 0 1 2 3 4 (exclusive) for i in 0..=5 { print!("{} ", i); } // 0 1 2 3 4 5 (inclusive) for item in &vec { println!("{}", item); } for (i, v) in vec.iter().enumerate() { println!("{}: {}", i, v); } // match β€” exhaustive pattern matching (compiler checks all cases) let x = 5u32; match x { 1 => println!("one"), 2 | 3 => println!("two or three"), // OR patterns 4..=9 => println!("four to nine"), // range pattern n if n % 2 == 0 => println!("even: {}", n), // match guard _ => println!("something else"), // wildcard β€” must cover rest } // if let β€” concise single-pattern match if let Some(val) = some_option { println!("got: {}", val); } // while let β€” loop while pattern matches while let Some(top) = stack.pop() { println!("{}", top); }

Quiz β€” Functions & Control Flow

Q1. In Rust, omitting the semicolon from the last line of a function:

Q2. Which construct can return a value in Rust via break?

Q3. match in Rust must be:

MODULE 03 Ownership & Borrowing CORE CONCEPT

The Mental Model: Memory as a Book

To understand Ownership without external docs, imagine memory as a library book:

  • Ownership: You own the book. You decide when it's thrown away. You can give it to someone else (Move), but then you don't have it anymore.
  • Immutable Borrowing (&T): You let many people read the book at once. No one can write in it or change the pages while others are reading.
  • Mutable Borrowing (&mut T): You let exactly one person have the book to edit it. While they are editing, no one else can even look at it.
Why this matters: This prevents Data Races. A data race happens when two threads access the same memory at the same time, and at least one is writing. Rust makes this impossible to compile.
Key insight: Ownership is Rust's solution to memory safety without a garbage collector. All checks happen at compile time β€” zero runtime cost. Once you internalize the 3 rules, everything else follows.

The Three Ownership Rules

// Rule 1: Every value has exactly ONE owner let s1 = String::from("hello"); // s1 owns this String on the heap // Rule 2: Only one owner at a time β€” assignment MOVES ownership let s2 = s1; // s1 is MOVED to s2. s1 is now invalid! // println!("{}", s1); // ERROR: value borrowed after move println!("{}", s2); // OK // Rule 3: When owner goes out of scope, value is DROPPED (memory freed) { let s3 = String::from("world"); } // s3 goes out of scope β†’ drop() called β†’ heap memory freed automatically // Clone: explicit deep copy β€” duplicates heap data let s1 = String::from("hello"); let s2 = s1.clone(); // BOTH s1 and s2 are valid, independent copies println!("{} and {}", s1, s2); // Copy types (stack-only, no destructor): i32, f64, bool, char, tuples of Copy // These are COPIED, not moved β€” both variables remain valid let x = 5; let y = x; // Copy β€” x is still valid println!("{} and {}", x, y); // OK // Functions behave like assignment β€” they take ownership or copy fn takes_ownership(s: String) { println!("{}", s); } // s dropped here fn makes_copy(n: i32) { println!("{}", n); } // i32 is Copy let s = String::from("hello"); takes_ownership(s); // s moved into function // println!("{}", s); // ERROR: s was moved let n = 5; makes_copy(n); // n is copied println!("{}", n); // OK β€” Copy type

Borrowing β€” References

// &T β€” immutable reference. Borrow without taking ownership. fn length(s: &String) -> usize { s.len() } // borrows s, does not own it let s = String::from("hello"); let len = length(&s); // pass reference β€” s is NOT moved println!("{} has {} chars", s, len); // s still valid! // &mut T β€” mutable reference. Borrow AND allow modification. fn push_world(s: &mut String) { s.push_str(", world"); } let mut s = String::from("hello"); // must be mut to create &mut push_world(&mut s); println!("{}", s); // "hello, world" // BORROW RULES β€” enforced at compile time: // RULE A: ONE &mut OR any number of & β€” NEVER both at same time let mut s = String::from("hello"); let r1 = &s; // OK let r2 = &s; // OK β€” multiple immutable refs allowed // let r3 = &mut s; // ERROR: cannot borrow mutably while immutably borrowed println!("{} {}", r1, r2); // r1, r2 last used here β€” borrows END here (NLL) let r3 = &mut s; // OK now β€” r1 and r2 are no longer in scope r3.push('!'); // RULE B: References must NOT outlive the value they point to // fn dangle() -> &String { // ERROR // let s = String::from("hi"); // &s // s is dropped at end of scope β€” reference would dangle! // } // solution: return String (transfer ownership) instead of &String

Slices

  • Slice = reference to a contiguous part of a collection
  • String slice: &s[0..5] β€” bytes 0 to 4 (half-open range)
  • Array slice: &arr[1..3] β€” elements 1 and 2
  • Fat pointer: stores (pointer to start, length)
  • &str is just a string slice β€” all string literals are &str
  • Slices prevent common off-by-one and dangling pointer bugs
  • Prefer &[T] over &Vec<T> in function params β€” more flexible

Move vs Copy vs Clone

  • Move: heap types (String, Vec, Box) β€” ownership transfers, original invalid
  • Copy: stack types (i32, bool, f64, char, arrays of Copy) β€” both valid
  • Clone: explicit deep copy β€” always works, may allocate
  • A type implements Copy only if it has no heap data and no Drop impl
  • Tuple is Copy only if ALL its elements are Copy
  • Array [T; N] is Copy only if T is Copy

Quiz β€” Ownership & Borrowing

Q1. After let s2 = s1; where s1 is a String, what happens to s1?

Q2. Can you have a mutable reference and an immutable reference to the same value at the same time?

Q3. Why can't a function return a reference to a local variable?

MODULE 04 Structs & Enums DATA MODELING

Structs

// Named-field struct struct User { username: String, email: String, age: u32, active: bool, } // Instantiation β€” order doesn't matter, but all fields required let user1 = User { username: String::from("alice"), email: String::from("alice@example.com"), age: 30, active: true, }; // Field shorthand β€” when variable name matches field name let username = String::from("bob"); let user2 = User { username, email: String::from("bob@b.com"), age: 25, active: true }; // Struct update syntax β€” copy remaining fields from another instance let user3 = User { email: String::from("carol@c.com"), ..user1 // NOTE: String fields are MOVED from user1 β€” user1.email no longer valid }; // Tuple struct β€” named tuple type struct Color(u8, u8, u8); struct Point(f64, f64, f64); let red = Color(255, 0, 0); println!("{}", red.0); // access by index // Unit struct β€” no fields; useful for implementing traits struct Sentinel; // Methods via impl block impl User { // Associated function (no self) β€” constructor pattern fn new(username: String, email: String) -> Self { Self { username, email, age: 0, active: true } } fn is_active(&self) -> bool { self.active } // immutable borrow fn deactivate(&mut self) { self.active = false; } // mutable borrow fn into_email(self) -> String { self.email } // takes ownership } let mut u = User::new(String::from("dan"), String::from("dan@d.com")); u.deactivate(); // Rust auto-borrows: sugar for User::deactivate(&mut u) println!("{}", u.is_active()); // false

Enums β€” Rust's Superpower

// Each variant can hold DIFFERENT types and amounts of data enum Message { Quit, // unit variant β€” no data Move { x: i32, y: i32 }, // struct-like variant Write(String), // single value ChangeColor(u8, u8, u8), // 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> β€” built-in nullable type. No null in safe Rust! // enum Option<T> { Some(T), None } let five: Option<i32> = Some(5); let absent: Option<i32> = None; // MUST unwrap before using β€” forces you to handle the None case let n = five.unwrap_or(0); // default if None let n = five.unwrap_or_default(); // type's Default value let doubled = five.map(|n| n * 2); // transform the inner value: Some(10) if let Some(val) = five { println!("{}", val); } // Pattern matching β€” exhaustive match five { Some(n) if n > 0 => println!("positive: {}", n), // guard Some(n) => println!("non-positive: {}", n), None => println!("nothing"), }

Advanced Pattern Matching

// Destructuring structs struct Point { x: i32, y: i32 } let p = Point { x: 3, y: 7 }; let Point { x, y } = p; // x=3, y=7 // Destructuring with rename let Point { x: px, y: py } = p; // Ignore fields let Point { x, .. } = p; // ignore y // @ bindings β€” bind a value while also testing it match age { n @ 0..=17 => println!("minor, age {}", n), n @ 18..=64 => println!("adult, age {}", n), n => println!("senior, age {}", n), } // Tuple patterns match (x, y) { (0, 0) => println!("origin"), (x, 0) => println!("on x-axis at {}", x), (0, y) => println!("on y-axis at {}", y), (x, y) => println!("({}, {})", x, y), } // Nested enum patterns enum Color { Rgb(u8,u8,u8), Hsv(u16,u8,u8) } enum Shape { Circle { radius: f64, color: Color }, Square(f64) } match shape { Shape::Circle { radius, color: Color::Rgb(r,g,b) } => println!("Circle r={} rgb({},{},{})", radius, r, g, b), Shape::Circle { radius, color: Color::Hsv(..) } => println!("HSV circle, r={}", radius), Shape::Square(side) => println!("Square side={}", side), }

Quiz β€” Structs & Enums

Q1. What does Option<T> solve in Rust?

Q2. An "associated function" in a Rust impl block differs from a method because:

Q3. Rust's match requires exhaustive coverage because:

MODULE 05 Error Handling PRODUCTION ESSENTIAL

Result<T, E> and the ? Operator

// Result<T, E> is the primary error type in Rust // enum Result<T, E> { Ok(T), Err(E) } use std::fs; use std::io; // The ? operator β€” propagate errors without boilerplate fn read_username(path: &str) -> Result<String, io::Error> { let s = fs::read_to_string(path)?; // ? = return Err early if error, else unwrap Ok Ok(s.trim().to_string()) } // ? desugars to: // match fs::read_to_string(path) { // Ok(val) => val, // Err(e) => return Err(e.into()), // .into() converts error type // } // Chaining ? elegantly fn process(path: &str) -> Result<Vec<i32>, Box<dyn std::error::Error>> { let content = fs::read_to_string(path)?; let nums: Vec<i32> = content .lines() .map(|l| l.trim().parse::<i32>()) .collect::<Result<Vec<_>, _>>()?; // collect into Result Ok(nums) } // Common Result methods result.is_ok(); result.is_err(); result.unwrap() // panics on Err β€” tests/prototypes only result.expect("custom message") // panics with message result.unwrap_or(default_val) // default if Err result.unwrap_or_else(|e| handle(e)) result.map(|v| transform(v)) // transform Ok value, pass Err through result.map_err(|e| convert(e)) // transform Err value result.and_then(|v| next(v)) // chain β€” flatMap for Result result.or_else(|e| recover(e)) // recover from Err result.ok() // convert to Option (drops error info)

Custom Error Types

// Method 1: thiserror β€” derive macro for library error types use thiserror::Error; #[derive(Error, Debug)] enum AppError { #[error("IO error: {0}")] Io(#[from] std::io::Error), // #[from] auto-impl From<io::Error> #[error("Parse error: {0}")] Parse(#[from] std::num::ParseIntError), #[error("config value {value} is out of range {min}..={max}")] OutOfRange { value: i32, min: i32, max: i32 }, #[error("unknown error")] Unknown, } // ? now works across error types automatically fn load_config(path: &str) -> Result<i32, AppError> { let s = std::fs::read_to_string(path)?; // io::Error β†’ AppError::Io via From let n: i32 = s.trim().parse()?; // ParseIntError β†’ AppError::Parse if n < 0 || n > 100 { return Err(AppError::OutOfRange { value: n, min: 0, max: 100 }); } Ok(n) } // Method 2: anyhow β€” type-erased errors for application code use anyhow::{Context, Result, bail, ensure}; fn run() -> Result<()> { // anyhow::Result hides the error type let s = std::fs::read_to_string("config.txt") .context("failed to read config.txt")?; // adds context to any error let n: i32 = s.trim().parse() .context("config must be an integer")?; ensure!(n > 0, "value must be positive, got {}", n); // returns Err if false if n > 1000 { bail!("value {} too large (max 1000)", n); } // early return Err println!("config: {}", n); Ok(()) } // Idiom: use thiserror in libraries, anyhow in binaries/applications

panic! vs Result

  • panic!: unrecoverable bugs β€” broken invariants, programmer errors
  • Result: expected, recoverable failures β€” bad input, network, file not found
  • Use unwrap() only when: in tests, when failure is truly logically impossible, or prototyping
  • assert! / assert_eq!: in tests and debug invariant checks
  • Libraries: always return Result β€” let caller decide
  • Applications: may panic at program entry, propagate Results otherwise

The Error Trait

  • std::error::Error: the base trait for all error types
  • Requires: Display (human message) + Debug
  • Optional: source() β€” returns underlying cause (error chain)
  • Box<dyn Error>: type-erased error β€” OK for simple programs
  • dyn Error + Send + Sync: thread-safe type-erased error
  • anyhow::Error: idiomatic type-erased error with context and backtrace

Quiz β€” Error Handling

Q1. The ? operator in Rust:

Q2. unwrap() should be used in production code:

Q3. The idiomatic Rust error handling stack for a library is:

MODULE 06 Traits & Generics ABSTRACTION

Traits β€” Shared Behaviour

// Define a trait β€” shared interface across types trait Animal { fn name(&self) -> &str; fn sound(&self) -> String; // Default implementation β€” can be overridden fn describe(&self) -> String { format!("{} says {}", self.name(), self.sound()) } } struct Dog { name: String } struct Cat { name: String } impl Animal for Dog { fn name(&self) -> &str { &self.name } fn sound(&self) -> String { String::from("woof") } } impl Animal for Cat { fn name(&self) -> &str { &self.name } fn sound(&self) -> String { String::from("meow") } // describe() uses default implementation } // Trait bounds β€” static dispatch (monomorphized, zero overhead) fn print_animal(a: &impl Animal) { println!("{}", a.describe()); } // Equivalent longer form (trait bound syntax): fn print_animal<A: Animal>(a: &A) { println!("{}", a.describe()); } // Multiple bounds fn print_and_debug<T: Animal + std::fmt::Debug>(a: &T) { println!("{:?}", a); println!("{}", a.describe()); } // Where clause β€” cleaner for complex bounds fn compare<T, U>(t: &T, u: &U) where T: Animal + Clone, U: Animal + std::fmt::Display, { /* ... */ } // Return impl Trait β€” caller doesn't know concrete type fn make_animal() -> impl Animal { Dog { name: String::from("Rex") } } // Return dyn Trait β€” type erased, heap-allocated, runtime dispatch fn make_animal_dyn(is_dog: bool) -> Box<dyn Animal> { if is_dog { Box::new(Dog { name: String::from("Rex") }) } else { Box::new(Cat { name: String::from("Whiskers") }) } } // Use dyn Trait when you need DIFFERENT concrete types at runtime

Generics

// Generic function β€” works for any T that satisfies the bound fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } println!("{}", largest(&[34, 50, 25, 100, 65])); // 100 println!("{}", largest(&[1.5, 2.7, 0.3])); // 2.7 // Generic struct struct Stack<T> { items: Vec<T> } impl<T> Stack<T> { fn new() -> Self { Stack { items: Vec::new() } } fn push(&mut self, item: T) { self.items.push(item); } fn pop(&mut self) -> Option<T> { self.items.pop() } fn peek(&self) -> Option<&T> { self.items.last() } fn is_empty(&self) -> bool { self.items.is_empty() } } // Conditional impl β€” only add method when T meets extra bound impl<T: std::fmt::Display> Stack<T> { fn print_top(&self) { match self.peek() { Some(t) => println!("top: {}", t), None => println!("stack is empty"), } } } // Important derive macros (auto-implement common traits) #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] struct Point { x: i32, y: i32 } // Debug: {:?} printing Clone: .clone() PartialEq: == // Eq: stricter equality Hash: use as HashMap key Default: Point::default()

impl Trait vs dyn Trait

  • impl Trait: static dispatch β€” compiler generates code per type (monomorphization) β€” fast, no heap, no vtable
  • dyn Trait: dynamic dispatch β€” vtable lookup at runtime β€” one level of indirection, requires Box
  • Use impl Trait when all types known at compile time
  • Use dyn Trait when you need different concrete types at runtime (e.g., Vec<Box<dyn Trait>>)
  • impl Trait: cannot return different types from if/else arms
  • dyn Trait: object-safe traits only (no generics in methods, no Self returns)

Key Standard Traits

  • Display: {} formatting β€” user-facing output
  • Debug: {:?} formatting β€” derive always
  • Clone / Copy: duplication
  • PartialEq / Eq: == and != operators
  • PartialOrd / Ord: <, >, sort()
  • Hash: use as HashMap key (requires Eq)
  • Default: zero/empty value
  • From / Into: type conversions
  • Iterator: custom iteration
  • Deref: * operator, coercions

Quiz β€” Traits & Generics

Q1. What is monomorphization in Rust generics?

Q2. When should you prefer dyn Trait over impl Trait?

MODULE 07 Collections & Iterators DATA STRUCTURES

Core Collections

// Vec<T> β€” growable heap array (use most often) let mut v: Vec<i32> = Vec::new(); let mut v = vec![1, 2, 3, 4, 5]; // macro shorthand v.push(6); // O(1) amortized v.pop(); // Option<T> v.insert(1, 10); // insert at index β€” O(n) v.remove(1); // remove at index β€” O(n) v.len(); v.is_empty(); v.capacity(); v[0]; // panics if out of bounds v.get(0); // Option<&T> β€” safe indexing v.contains(&3); v.sort(); v.sort_by(|a,b| b.cmp(a)); v.sort_by_key(|k| k.abs()); v.dedup(); // remove consecutive duplicates (sort first) v.retain(|&x| x % 2 == 0); // keep only even numbers in-place v.extend([7, 8, 9]); // append slice v.truncate(3); // keep only first 3 v.clear(); // remove all // HashMap<K, V> β€” hash table, O(1) average get/set use std::collections::HashMap; let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Alice"), 100); map.insert(String::from("Bob"), 90); map.get("Alice"); // Option<&V> map.contains_key("Bob"); map.remove("Bob"); // Option<V> map.len(); map.is_empty(); // Entry API β€” idiomatic insert-if-absent map.entry(String::from("Carol")).or_insert(0); // insert 0 if absent *map.entry(String::from("Alice")).or_insert(0) += 1; // increment or init for (key, val) in &map { println!("{}: {}", key, val); } // HashSet<T> β€” unique values, O(1) contains use std::collections::HashSet; let mut set: HashSet<i32> = HashSet::new(); set.insert(1); set.insert(2); set.insert(1); // {1, 2} β€” no duplicates set.contains(&1); set.remove(&1); let a: HashSet<_> = [1,2,3].iter().collect(); let b: HashSet<_> = [2,3,4].iter().collect(); let union: HashSet<_> = a.union(&b).collect(); // {1,2,3,4} let intersection: HashSet<_> = a.intersection(&b).collect(); // {2,3} let difference: HashSet<_> = a.difference(&b).collect(); // {1} // Other collections: // BTreeMap<K,V> β€” sorted map (use when iteration order matters) // VecDeque<T> β€” double-ended queue (fast push/pop from both ends) // BinaryHeap<T> β€” max-heap priority queue // LinkedList<T> β€” rarely used β€” Vec is usually faster

Iterators β€” Lazy & Zero-Cost

// Iterator trait β€” produces values lazily // trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } let v = vec![1, 2, 3, 4, 5, 6]; // Three iteration modes for x in v.iter() { /* &T β€” borrows */ } for x in v.iter_mut() { /* &mut T β€” mutable borrow */ } for x in v.into_iter() { /* T β€” takes ownership, v unusable after */ } for x in &v { /* &T β€” sugar for v.iter() */ } for x in &mut v { /* &mut T β€” sugar for v.iter_mut() */ } // Adapters β€” lazy, return new iterators (nothing runs yet) let result: Vec<i32> = v.iter() .filter(|&&x| x % 2 == 0) // keep: 2, 4, 6 .map(|&x| x * x) // square: 4, 16, 36 .take(2) // first 2: 4, 16 .collect(); // NOW it runs! [4, 16] // Common adapters iter.map(|x| transform(x)) iter.filter(|x| predicate(x)) iter.filter_map(|x| maybe(x)) // filter + map in one β€” removes None iter.take(n) // first n elements iter.skip(n) // skip first n iter.enumerate() // (usize, T) pairs iter.zip(other) // pair with another iterator iter.chain(other) // append another iterator iter.flat_map(|x| iter_of(x)) // map then flatten one level iter.flatten() // flatten one level of nesting iter.cloned() // &T β†’ T by cloning iter.copied() // &T β†’ T for Copy types iter.rev() // reverse (requires DoubleEndedIterator) iter.peekable() // peek() without consuming iter.step_by(n) // every nth element iter.windows(n) // overlapping windows of size n iter.chunks(n) // non-overlapping chunks // Consumers β€” run the pipeline, produce a final value iter.collect::<Vec<_>>() // into Vec, HashMap, HashSet, String, ... iter.sum::<i32>() // sum all iter.product::<i32>() // multiply all iter.count() // number of elements iter.max(); iter.min() // Option<T> iter.max_by_key(|x| key(x)) // max by a derived key iter.any(|x| condition(x)) // true if any match (short-circuits) iter.all(|x| condition(x)) // true if all match (short-circuits) iter.find(|x| condition(x)) // Option<&T> β€” first match iter.position(|x| cond(x)) // Option<usize> β€” index of first match iter.fold(init, |acc, x| f(acc, x)) // general reduction iter.for_each(|x| action(x)) // run side effect on each iter.last() // Option<T> β€” consume all, return last iter.nth(n) // Option<T> β€” nth element (0-indexed) iter.unzip() // Vec of (A,B) β†’ (Vec<A>, Vec<B>) iter.partition(|x| cond(x)) // split into (Vec matching, Vec not) // Custom iterator struct Fibonacci { a: u64, b: u64 } impl Iterator for Fibonacci { type Item = u64; fn next(&mut self) -> Option<u64> { let next = self.a + self.b; self.a = self.b; self.b = next; Some(self.a) // infinite iterator β€” never returns None } } let fibs: Vec<u64> = Fibonacci { a: 0, b: 1 }.take(10).collect(); // Custom iterator gets ALL adapter methods for free!

Quiz β€” Collections & Iterators

Q1. What does vec.get(index) return that vec[index] does not?

Q2. Iterator adapters like .map() and .filter() are:

Q3. filter_map is useful when:

MODULE 08 Closures & Functional Patterns FUNCTIONAL RUST

Closures

// Closure syntax β€” | params | body let add = |x, y| x + y; // types inferred let add = |x: i32, y: i32| -> i32 { x + y }; // explicit types let double = |x| x * 2; println!("{}", double(5)); // 10 // Closures CAPTURE their environment β€” functions cannot let threshold = 10; let is_big = |n| n > threshold; // captures threshold by reference println!("{}", is_big(15)); // true // The three closure traits β€” how the closure uses captured values: // // Fn β€” captures by reference (&T) β€” can call many times // FnMut β€” captures by mutable reference (&mut T) β€” can call many times, may mutate // FnOnce β€” captures by value (T) β€” can call only ONCE (consumes captured values) // // Every closure implements FnOnce; closures that don't move implement FnMut; // closures that don't mutate also implement Fn. Fn βŠ‚ FnMut βŠ‚ FnOnce // FnMut example let mut count = 0; let mut increment = || { count += 1; count }; // mutably borrows count println!("{}", increment()); // 1 println!("{}", increment()); // 2 // FnOnce example β€” can only call once let name = String::from("Alice"); let greet = move || { println!("Hello, {}!", name); // name is MOVED into closure drop(name); // explicitly consume β€” makes it FnOnce }; greet(); // OK β€” consumes name // greet(); // ERROR: closure was already called and consumed name // move keyword β€” force ownership transfer into closure let s = String::from("data"); let f = move || println!("{}", s); // s moved into f β€” required for threads // s is no longer accessible here std::thread::spawn(f); // thread may outlive s β€” move ensures it owns it // Closures as parameters β€” generic over Fn trait fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } fn apply_mut<F: FnMut() -> i32>(mut f: F) -> i32 { f() } fn apply_once<F: FnOnce() -> String>(f: F) -> String { f() } // Store closure in struct β€” use Box<dyn Fn> for type erasure struct Handler { callback: Box<dyn Fn(i32) -> i32> } impl Handler { fn new(f: impl Fn(i32) -> i32 + 'static) -> Self { Self { callback: Box::new(f) } } fn run(&self, x: i32) -> i32 { (self.callback)(x) } }

Quiz β€” Closures

Q1. A closure that is FnOnce can be called:

Q2. Why is the move keyword needed for closures passed to std::thread::spawn?

MODULE 09 Lifetimes β€” Deep ADVANCED
Lifetimes are not about duration β€” they are annotations that tell the compiler how long references must remain valid relative to each other. The compiler infers most lifetimes; you only annotate when it can't.

Lifetime Annotations

// When do you need lifetime annotations? // When a function takes MULTIPLE references and returns a reference β€” // the compiler doesn't know which input the output is tied to. // 'a is a lifetime parameter β€” reads as "lifetime a" fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } } // 'a = the SHORTER of x and y's lifetimes β€” result valid as long as both inputs let result; { let s1 = String::from("long string"); let s2 = String::from("xyz"); result = longest(s1.as_str(), s2.as_str()); println!("{}", result); // OK β€” both s1 and s2 still alive } // println!("{}", result); // ERROR β€” s1, s2 dropped, result would dangle // Lifetime in structs β€” struct cannot outlive the reference it holds struct Important<'a> { content: &'a str, // Important holds a borrowed string slice } impl<'a> Important<'a> { fn announce(&self) -> &str { self.content } // lifetime elided β€” OK here } let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("no period"); let i = Important { content: first_sentence }; // i cannot outlive novel // Static lifetime β€” reference valid for entire program duration let s: &'static str = "I live forever"; // string literals are 'static

Lifetime Elision Rules

// Rust infers lifetimes using 3 elision rules. // You only annotate when rules don't resolve all lifetimes. // Rule 1: Each reference parameter gets its own lifetime fn foo(x: &str) -> &str { x } // Expands to: fn foo<'a>(x: &'a str) -> &'a str fn bar(x: &str, y: &str) -> &str { x } // ERROR β€” ambiguous which input // Must write: fn bar<'a>(x: &'a str, y: &str) -> &'a str { x } // Rule 2: If exactly one input reference, output gets same lifetime fn first_word(s: &str) -> &str { // no annotation needed let bytes = s.as_bytes(); for (i, &byte) in bytes.iter().enumerate() { if byte == b' ' { return &s[..i]; } } &s[..] } // Rule 3: If &self or &mut self, output gets self's lifetime impl Important<'_> { fn level(&self) -> &str { self.content } // no annotation needed } // Higher-Ranked Trait Bounds (HRTB) β€” "for any lifetime 'a" fn apply_to_str<F>(f: F) -> String where F: for<'a> Fn(&'a str) -> &'a str, // works for any lifetime { f("hello").to_string() } // Variance β€” how lifetime relationships work in subtyping: // Covariant: &'long T can be used where &'short T expected (safe β€” longer is stricter) // Invariant: &'a mut T is invariant over T β€” must match exactly (prevents aliasing bugs) // Contravariant: fn(T) β€” argument types are contravariant

Quiz β€” Lifetimes

Q1. Lifetime annotations in Rust tell the compiler:

Q2. 'static lifetime means:

MODULE 10 Async Rust & Tokio CONCURRENCY

How async/await Works

// The Mental Model: The State Machine Transformation // When you write: async fn example() { let a = step_one().await; let b = step_two(a).await; } // The compiler transforms it into an internal Enum: enum ExampleFuture { State0, // Initial state State1 { a: ResultType }, // Waiting for step_one State2 { b: FinalType }, // Waiting for step_two Done, // Completed } // Why this matters: // 1. No stack frames: Unlike OS threads, futures don't need a massive stack. // 2. Cooperative: The task gives up control (yields) at every .await point. // 3. Lazy: If you don't poll the state machine, it never moves. // async fn returns an impl Future β€” a lazy state machine // Nothing runs until the future is polled by an executor async fn fetch_data(url: &str) -> Result<String, reqwest::Error> { let response = reqwest::get(url).await?; // .await = poll until ready let text = response.text().await?; Ok(text) } // async fn desugars to a state machine enum: // State0: before first .await // State1: after first .await, waiting for second // State2: done // The compiler generates this automatically // Futures are LAZY β€” you must .await them or they do nothing let f = fetch_data("https://example.com"); // not executed yet! let result = f.await; // NOW it runs // Tokio β€” the most popular async runtime // Add to Cargo.toml: tokio = { version = "1", features = ["full"] } #[tokio::main] // macro that creates the Tokio runtime and calls your async main async fn main() { let result = fetch_data("https://api.example.com/data").await; match result { Ok(data) => println!("{}", data), Err(e) => eprintln!("Error: {}", e), } } // Spawn a task β€” runs concurrently, like a goroutine let handle = tokio::spawn(async move { fetch_data("https://example.com").await }); let result = handle.await?; // JoinHandle returns Result (task may panic) // Run multiple futures CONCURRENTLY (not parallel β€” same thread unless spawned) let (r1, r2) = tokio::join!( fetch_data("https://api.example.com/a"), fetch_data("https://api.example.com/b"), ); // both run concurrently, wait for both // Race futures β€” first to complete wins tokio::select! { result = fetch_data("https://fast.example.com") => println!("fast: {:?}", result), result = fetch_data("https://slow.example.com") => println!("slow: {:?}", result), _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => println!("timeout!"), }

Pin and the Waker System

// Why Pin exists: // async state machines can be self-referential (State1 holds a ref to State0's data) // If the future is moved in memory, internal pointers become dangling // Pin<P> guarantees the pointee will NOT be moved after pinning use std::pin::Pin; use std::task::{Context, Poll}; use std::future::Future; // The Future trait β€” what the executor calls pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } // Poll::Ready(value) β€” done, return value // Poll::Pending β€” not ready, future has registered a Waker // When a future returns Pending, it MUST register cx.waker() // When the event is ready (I/O, timer), Waker::wake() is called // The executor then re-polls the future // Pinning in practice: let future = fetch_data("https://example.com"); let pinned = Box::pin(future); // heap-pin β€” stable address tokio::spawn(pinned); // pin! macro β€” stack pinning (avoids heap allocation) tokio::pin!(future); future.await;

Tokio Primitives

  • tokio::spawn: spawn a task β€” requires Send + 'static
  • tokio::join!: run all concurrently, wait for all
  • tokio::select!: race β€” first ready wins
  • tokio::time::sleep: async sleep (never block the thread)
  • tokio::sync::Mutex: async-aware mutex (use over std::Mutex in async)
  • tokio::sync::mpsc: multi-producer single-consumer channel
  • tokio::sync::broadcast: one sender, many receivers
  • tokio::sync::oneshot: single-value channel (req/resp pattern)
  • tokio::task::spawn_blocking: run blocking code on thread pool

Common Async Mistakes

  • ❌ Holding std::sync::MutexGuard across .await β€” deadlock risk
  • βœ… Use tokio::sync::Mutex when locking across .await
  • ❌ Calling blocking functions (std::fs, std::thread::sleep) in async
  • βœ… Use tokio::fs, tokio::time::sleep async equivalents
  • ❌ Forgetting .await β€” future silently does nothing
  • βœ… tokio::spawn requires Send + 'static β€” use move closures
  • ❌ CPU-intensive work on async executor β€” blocks all tasks
  • βœ… Use spawn_blocking to offload CPU work to thread pool

Quiz β€” Async Rust

Q1. What happens if you create a Future but never .await it?

Q2. Why does Pin exist in Rust's async system?

Q3. You need to run CPU-intensive work inside an async Tokio task. Best approach?

MODULE 11 Unsafe Rust & FFI ADVANCED
Warning: unsafe does NOT disable the borrow checker. It unlocks exactly 5 additional capabilities. Every unsafe block is a promise: "I have manually verified the invariants the compiler cannot check." Always document with // SAFETY: comments.

The 5 Things unsafe Unlocks

// 1. Dereference raw pointers (*const T, *mut T) let x = 42i32; let r1 = &x as *const i32; // creating raw pointer is safe let r2 = &mut x as *mut i32; // creating is safe; dereferencing requires unsafe unsafe { println!("{}", *r1); // SAFETY: r1 points to valid i32 on the stack *r2 = 43; } // 2. Call unsafe functions unsafe fn dangerous() { println!("I am unsafe!"); } unsafe { dangerous(); } // 3. Implement unsafe traits struct MyPtr(*mut u8); unsafe impl Send for MyPtr {} // YOU guarantee this is safe to send across threads unsafe impl Sync for MyPtr {} // 4. Access or modify mutable static variables static mut GLOBAL: i32 = 0; unsafe { GLOBAL += 1; } // data race if called from multiple threads! // 5. Access fields of unions union IntOrFloat { i: i32, f: f32 } let u = IntOrFloat { i: 42 }; unsafe { println!("{}", u.f); } // reinterprets bits β€” valid bit pattern not guaranteed // Safe abstraction pattern β€” hide unsafe behind a safe public API pub fn split_at(slice: &[i32], mid: usize) -> (&[i32], &[i32]) { assert!(mid <= slice.len()); let ptr = slice.as_ptr(); unsafe { // SAFETY: mid <= slice.len() guaranteed by assert above. // The two halves don't overlap β€” safe to create two non-overlapping slices. ( std::slice::from_raw_parts(ptr, mid), std::slice::from_raw_parts(ptr.add(mid), slice.len() - mid), ) } }

FFI β€” Calling C from Rust

// Declare C functions with extern "C" block #[link(name = "c")] // link against libc extern "C" { fn abs(input: i32) -> i32; fn sqrt(x: f64) -> f64; } unsafe { println!("|{1}| = {0}", abs(-5)); println!("sqrt(2) = {}", sqrt(2.0)); } // Structs passed to C β€” must use #[repr(C)] for C-compatible layout #[repr(C)] struct CPoint { x: f64, y: f64 } extern "C" { fn distance(p1: *const CPoint, p2: *const CPoint) -> f64; } // Exposing Rust functions to C #[no_mangle] // don't mangle the name β€” C can find it by "add" pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b } // String passing across FFI boundary use std::ffi::{CStr, CString}; use std::os::raw::c_char; extern "C" { fn puts(s: *const c_char); } let s = CString::new("Hello from Rust!").unwrap(); // Rust String β†’ C string unsafe { puts(s.as_ptr()); } // Receive C string extern "C" { fn get_greeting() -> *const c_char; } unsafe { let ptr = get_greeting(); let s = CStr::from_ptr(ptr); // C string β†’ Rust &CStr println!("{}", s.to_str().unwrap()); } // Use bindgen crate to auto-generate FFI bindings from C headers: // bindgen input.h -o bindings.rs

Quiz β€” Unsafe Rust

Q1. Which of the following does NOT require an unsafe block?

Q2. The correct approach to using unsafe in a public library is:

MODULE 12 Memory Layout, Smart Pointers & Rust vs Go EXPERT

Memory Layout

// Default repr: compiler may reorder fields for optimal alignment struct Foo { a: u8, b: u64, c: u8 } // compiler reorders to: b(8), a(1), c(1), padding(6) = 16 bytes // or keeps order: a(1)+7pad, b(8), c(1)+7pad = 24 bytes β€” depends on version // #[repr(C)] β€” C-compatible layout, fields in declaration order #[repr(C)] struct CCompatible { a: u8, _pad: [u8; 7], b: u64, c: u8, _pad2: [u8; 7] } // Enum layout: discriminant + largest variant size enum Message { Quit, // 0 bytes payload Move { x: i32, y: i32 },// 8 bytes payload Write(String), // 24 bytes (ptr+len+cap) } // Size: 24 bytes (largest variant) + 8 bytes (discriminant+alignment) = 32 bytes // Null pointer optimization β€” Option<&T> same size as &T use std::mem::size_of; assert_eq!(size_of::<Option<&i32>>(), size_of::<&i32>()); // true! assert_eq!(size_of::<Option<Box<i32>>>(), size_of::<Box<i32>>()); // true! // None = null pointer; Some(ptr) = non-null β€” no extra space needed // Zero-sized types (ZST) β€” no memory, used as markers struct Marker; assert_eq!(size_of::<Marker>(), 0); // 0 bytes β€” optimized away assert_eq!(size_of::<Vec<Marker>>(), size_of::<usize>() * 3); // just the vec header

Smart Pointers

// Box<T> β€” heap allocation, single owner, zero overhead let b = Box::new(5); // 5 lives on heap; b is stack pointer println!("{}", *b); // auto-deref // Use Box for: recursive types, large values to avoid stack copies, // trait objects (Box<dyn Trait>) // Rc<T> β€” reference counted, single-threaded use std::rc::Rc; let a = Rc::new(String::from("hello")); let b = Rc::clone(&a); // increment ref count β€” no heap copy println!("{} refs", Rc::strong_count(&a)); // 2 // When last Rc dropped, value freed. Not Send β€” cannot cross thread boundary. // Arc<T> β€” atomic reference count, multi-threaded use std::sync::Arc; let a = Arc::new(5); let b = Arc::clone(&a); std::thread::spawn(move || println!("{}", b)); // Arc is Send // Cell<T> β€” interior mutability for Copy types (no borrowing overhead) use std::cell::Cell; let cell = Cell::new(5); cell.set(10); println!("{}", cell.get()); // 10 // RefCell<T> β€” interior mutability with runtime borrow checking use std::cell::RefCell; let rc = RefCell::new(vec![1, 2, 3]); rc.borrow().iter().for_each(|x| println!("{}", x)); // immutable borrow rc.borrow_mut().push(4); // mutable borrow // borrow_mut panics at runtime if another borrow is active (same as compile rules, runtime) // Common combination: Rc<RefCell<T>> β€” shared mutable data, single-threaded let shared = Rc::new(RefCell::new(vec![1, 2])); let clone1 = Rc::clone(&shared); let clone2 = Rc::clone(&shared); clone1.borrow_mut().push(3); println!("{:?}", clone2.borrow()); // [1, 2, 3] // Multi-threaded equivalent: Arc<Mutex<T>> use std::sync::{Arc, Mutex}; let counter = Arc::new(Mutex::new(0)); let c = Arc::clone(&counter); std::thread::spawn(move || { *c.lock().unwrap() += 1; }); // Cow<T> β€” clone on write: borrowed when possible, owned when needed use std::borrow::Cow; fn process(s: &str) -> Cow<str> { if s.contains(' ') { Cow::Owned(s.replace(' ', "_")) } else { Cow::Borrowed(s) } // no allocation if no spaces }

Rust vs Go β€” When to Use Each

πŸ¦€ CHOOSE RUST WHEN

  • Predictable latency is critical β€” no GC pauses ever
  • Interfacing with C/system libraries (FFI)
  • Memory-constrained environments (embedded, WASM)
  • Crypto/security code β€” no memory safety bugs
  • High-throughput data processing with zero-copy
  • OS kernels, device drivers, hypervisors
  • WebAssembly targets
  • When compile-time safety proofs matter more than dev speed

🐹 CHOOSE GO WHEN

  • Network services with high concurrency (goroutines shine)
  • Team velocity and simplicity matter more than raw perf
  • Microservices, APIs, CLI tools
  • GC pauses acceptable (< 1ms in modern Go)
  • Fast onboarding for new engineers
  • DevOps tooling, infrastructure automation
  • When you need to ship fast and iterate
  • Cross-platform binaries with minimal dependencies

Best Rust Learning Resources

BOOK Β· FREE
The Rust Programming Language (The Book)

Official book β€” free online. Read chapters 1-10 for basics, 13-16 for advanced. The definitive reference.

BOOK Β· FREE
The Rustonomicon β€” Unsafe Rust

Official guide to unsafe Rust. Read chapters 1-5 once you're comfortable with safe Rust. Essential for systems programming.

INTERACTIVE
Rustlings β€” Hands-on Exercises

80+ small exercises covering every concept. Best way to solidify the basics. Do every exercise.

VIDEO
Jon Gjengset β€” Crust of Rust Series

The best advanced Rust content on YouTube. Watch: Lifetimes, Iterators, Smart Pointers, Async. Staff-level Rust engineer.

BOOK Β· FREE
Learn Rust With Entirely Too Many Linked Lists

Best way to deeply understand ownership, lifetimes, and unsafe. More practical than it sounds.

DOCS
Tokio Official Tutorial

Best async Rust practical guide. Covers tasks, channels, I/O, and all Tokio primitives with working examples.

PRACTICE
Exercism Rust Track

100+ exercises with mentor feedback. Great for practicing idiomatic Rust patterns.

Quiz β€” Memory & Smart Pointers

Q1. Why is Option<&T> the same size as &T in Rust?

Q2. When should you use Arc<Mutex<T>> vs Rc<RefCell<T>>?

Q3. RefCell<T> enforces borrow rules: