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.
// 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.
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 02Functions & Control FlowBASICS
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 03Ownership & BorrowingCORE 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
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 04Structs & EnumsDATA 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 05Error HandlingPRODUCTION 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
// 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 07Collections & IteratorsDATA 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:
// 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 09Lifetimes β DeepADVANCED
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 10Async Rust & TokioCONCURRENCY
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::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 11Unsafe Rust & FFIADVANCED
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 12Memory Layout, Smart Pointers & Rust vs GoEXPERT
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