Rust¶
Rust's design philosophy revolves around three core pillars: performance, reliability, and productivity. It achieves performance comparable to C or C++ by compiling to native code with zero-cost abstractions—meaning high-level features don't incur runtime penalties. Reliability comes from its ownership model, which eliminates issues like null pointer dereferences, data races, and buffer overflows at compile time. Productivity is enhanced through expressive syntax, excellent documentation, and integrated tools like Cargo for package management. Unlike languages with garbage collection (e.g., Java or Go), Rust manages memory manually but safely, avoiding the pauses and overhead associated with GC. This makes it ideal for resource-constrained environments while still being accessible to developers from higher-level languages.
Key features:
- Memory safety: Compile-time guarantees prevent common bugs (use-after-free, double-free, data races).
- Zero-cost abstractions: High-level code compiles to efficient machine code.
- Ownership system: Unique approach to memory management without garbage collection.
- Pattern matching: Powerful
matchexpressions for control flow. - Trait system: Flexible type system similar to type classes or interfaces.
- Cargo: Built-in package manager and build system.
- Cross-platform: Compiles to native code for many targets.
Variables and Data Types¶
Variables in Rust are immutable by default. Use let mut for mutability. Types are inferred but can be explicit.
| Data Type | Description | Example |
|---|---|---|
i8, i16, i32, i64, i128 |
Signed integers | let x: i32 = 42; |
u8, u16, u32, u64, u128 |
Unsigned integers | let y: u32 = 100; |
isize, usize |
Pointer-sized integers | let z: usize = 0; |
f32, f64 |
Floating-point | let pi: f64 = 3.14; |
bool |
Boolean | let flag: bool = true; |
char |
Unicode scalar (4 bytes) | let c: char = '🦀'; |
&str |
String slice (immutable) | let s: &str = "hello"; |
String |
Owned, growable string | let s = String::from("hello"); |
[T; N] |
Fixed-size array | let arr: [i32; 3] = [1, 2, 3]; |
Vec<T> |
Dynamic vector | let vec = vec![1, 2, 3]; |
(&T, &U) |
Tuple | let tup: (i32, f64) = (5, 3.14); |
Operations:
- Arithmetic:
+,-,*,/,%(modulo). - Comparison:
==,!=,<,>,<=,>=. - Logical:
&&,||,!. - Bitwise:
&,|,^,<<,>>.
Control Structures¶
-
If-Else:
let x = 5; if x > 0 { println!("Positive"); } else if x < 0 { println!("Negative"); } else { println!("Zero"); } // If as expression let result = if x > 0 { "positive" } else { "non-positive" }; -
Loops:
loop: Infinite loop (usebreakto exit).while: Conditional loop.for: Iterate over iterators.
// Loop with break/continue let mut counter = 0; loop { counter += 1; if counter == 10 { break; } } // For loop for i in 0..5 { println!("{}", i); // 0, 1, 2, 3, 4 } // Iterate over vector let vec = vec![1, 2, 3]; for val in vec.iter() { println!("{}", val); } -
Match: Pattern matching (exhaustive).
let number = 5; match number { 1 => println!("One"), 2 | 3 => println!("Two or Three"), 4..=10 => println!("Between 4 and 10"), _ => println!("Something else"), }
Functions¶
Define with fn:
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn main() {
println!("{}", greet("World")); // Output: Hello, World!
}
- Parameters:
fn add(a: i32, b: i32) -> i32 { a + b } - Return type: Explicit with
->or implicit last expression. - Closures:
let square = |x| x * x;
Ownership and Borrowing¶
Rust's ownership system is its most distinctive feature—a set of rules checked at compile time that ensures memory safety without garbage collection. Understanding ownership is essential for writing idiomatic Rust code. This system eliminates entire categories of bugs: use-after-free, double-free, dangling pointers, and data races.
Memory Model: Stack vs Heap¶
Before diving into ownership, understanding Rust's memory model is crucial:
Stack:
- Fixed-size data with known lifetime
- LIFO (Last In, First Out) allocation/deallocation
- Extremely fast (just moving a pointer)
- Examples: integers, floats, booleans, fixed-size arrays, tuples of stack types
Heap:
- Dynamically-sized data or data that needs to outlive its scope
- Requires explicit allocation and deallocation
- Slower than stack (finding space, bookkeeping)
- Examples:
String,Vec<T>,Box<T>,HashMap<K, V>
fn main() {
// Stack allocation: size known at compile time
let x: i32 = 42; // 4 bytes on stack
let arr: [i32; 3] = [1, 2, 3]; // 12 bytes on stack
// Heap allocation: size can vary at runtime
let s: String = String::from("hello"); // Pointer on stack, data on heap
let v: Vec<i32> = vec![1, 2, 3, 4, 5]; // Pointer on stack, data on heap
// String layout in memory:
// Stack: | ptr | len: 5 | capacity: 5 | (24 bytes on 64-bit)
// Heap: | h | e | l | l | o |
}
The Three Ownership Rules¶
- Each value in Rust has exactly one owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (memory freed).
These rules are enforced at compile time—violations result in compiler errors, not runtime crashes.
fn main() {
{
let s = String::from("hello"); // s comes into scope, owns the String
// s is valid here
} // s goes out of scope, `drop` is called, memory is freed
// println!("{}", s); // Error: s is not in scope
}
Move Semantics¶
When you assign a heap-allocated value to another variable, Rust moves ownership rather than copying:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership moves from s1 to s2
// println!("{}", s1); // Error: value borrowed here after move
println!("{}", s2); // OK: s2 now owns the String
}
Why move instead of copy? If both s1 and s2 pointed to the same heap data, when they go out of scope, Rust would try to free the same memory twice (double-free bug). Moving ensures only one owner exists.
Move in function calls:
fn takes_ownership(s: String) {
println!("{}", s);
} // s is dropped here
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is moved into the function
// println!("{}", s); // Error: s was moved
}
Returning ownership:
fn gives_ownership() -> String {
let s = String::from("hello");
s // Ownership moves to the caller
}
fn takes_and_gives_back(s: String) -> String {
s // Ownership moves back to the caller
}
fn main() {
let s1 = gives_ownership(); // s1 owns the returned String
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 moved in, s3 owns the returned value
// s2 is no longer valid; s1 and s3 are
}
Copy vs Clone¶
Copy Trait:
Types that implement Copy are duplicated bit-for-bit on assignment (stack-only data):
fn main() {
let x = 5;
let y = x; // Copy, not move
println!("x = {}, y = {}", x, y); // Both valid
// Types that implement Copy:
// - All integer types (i32, u64, etc.)
// - Boolean (bool)
// - Floating-point types (f32, f64)
// - Character type (char)
// - Tuples containing only Copy types: (i32, i32) is Copy, (i32, String) is not
// - Arrays of Copy types with known size: [i32; 5]
}
Clone Trait: For explicit deep copies of heap data:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit deep copy
println!("s1 = {}, s2 = {}", s1, s2); // Both valid
// clone() can be expensive: it copies all heap data
let v1 = vec![1, 2, 3, 4, 5];
let v2 = v1.clone(); // Copies the entire vector
}
Implementing Copy and Clone:
#[derive(Copy, Clone, Debug)]
struct Point {
x: i32,
y: i32,
}
// Cannot derive Copy if any field doesn't implement Copy
#[derive(Clone, Debug)]
struct Person {
name: String, // String doesn't implement Copy
age: u32,
}
fn main() {
let p1 = Point { x: 5, y: 10 };
let p2 = p1; // Copy
println!("{:?} {:?}", p1, p2); // Both valid
let person1 = Person { name: String::from("Alice"), age: 30 };
let person2 = person1.clone(); // Must explicitly clone
// let person3 = person1; // Would move person1
}
Borrowing: References¶
Borrowing allows you to reference data without taking ownership. References are pointers that are guaranteed to be valid.
Immutable References (&T):
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own the String, nothing is dropped
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Pass a reference
println!("The length of '{}' is {}.", s1, len); // s1 still valid
}
Mutable References (&mut T):
fn append_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello world"
}
The Borrowing Rules¶
Rust enforces two critical rules at compile time:
-
At any given time, you can have either:
- One mutable reference, OR
- Any number of immutable references
-
References must always be valid (no dangling references).
These rules prevent data races, which occur when:
- Two or more pointers access the same data at the same time
- At least one pointer is writing
- There's no synchronization
fn main() {
let mut s = String::from("hello");
// Multiple immutable references are OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
// Now we can take a mutable reference
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
// This would fail:
// let r1 = &s;
// let r2 = &mut s; // Error: cannot borrow as mutable while immutable borrow exists
// println!("{}, {}", r1, r2);
}
Non-Lexical Lifetimes (NLL): Rust 2018+ uses NLL: references are considered "live" until their last use, not until the end of scope:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point (NLL)
let r3 = &mut s; // OK because r1 and r2 are no longer used
println!("{}", r3);
}
Dangling References¶
Rust prevents dangling references at compile time:
// This would not compile:
fn dangle() -> &String {
let s = String::from("hello");
&s // Error: returns a reference to data owned by the current function
} // s is dropped here, reference would be dangling
// Correct: return the owned value
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out
}
Slices: References to Contiguous Sequences¶
Slices are references to a portion of a collection:
fn main() {
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..11]; // "world"
// Shorthand
let hello = &s[..5]; // Same as &s[0..5]
let world = &s[6..]; // Same as &s[6..len]
let whole = &s[..]; // Same as &s[0..len]
println!("{} {}", hello, world);
// String literals are slices
let literal: &str = "hello world"; // &str, stored in binary
// Array slices
let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3]; // [2, 3]
}
Slices prevent bugs:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // Immutable borrow
// s.clear(); // Error: cannot borrow as mutable while immutable borrow exists
println!("The first word is: {}", word);
s.clear(); // OK: word is no longer used
}
Lifetimes¶
Lifetimes are Rust's way of ensuring references are valid for as long as they're used. Every reference has a lifetime—the scope for which the reference is valid.
Why lifetimes are needed:
// Which reference does the return value come from?
fn longest(x: &str, y: &str) -> &str { // Error: missing lifetime specifier
if x.len() > y.len() {
x
} else {
y
}
}
Lifetime Annotation Syntax:
&i32 // A reference
&'a i32 // A reference with an explicit lifetime 'a
&'a mut i32 // A mutable reference with an explicit lifetime 'a
Function Lifetimes:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
} // string2 goes out of scope, result would be invalid if it referenced string2
}
Understanding 'a: The lifetime 'a means "the returned reference will be valid for the shorter of the lifetimes of x and y."
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// result is valid here
} // string2 dropped
// println!("{}", result); // Error: string2 does not live long enough
}
Struct Lifetimes:
When a struct holds references, it needs lifetime annotations:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
} // excerpt cannot outlive novel
Lifetime Elision Rules:
The compiler can infer lifetimes in many cases using three rules:
- Each parameter that is a reference gets its own lifetime parameter.
- If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters.
- If there are multiple input lifetime parameters, but one is
&selfor&mut self, the lifetime ofselfis assigned to all output lifetime parameters.
// These are equivalent:
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// Rule 3 in action:
impl<'a> ImportantExcerpt<'a> {
// Elided: fn announce(&self, announcement: &str) -> &str
// Expanded: fn announce<'b>(&'a self, announcement: &'b str) -> &'a str
fn announce(&self, announcement: &str) -> &str {
println!("{}", announcement);
self.part
}
}
Static Lifetime:
The 'static lifetime means the reference lives for the entire program:
// String literals have 'static lifetime
let s: &'static str = "I have a static lifetime.";
// Use sparingly: usually indicates global data or leaked memory
Lifetime Bounds:
Combine lifetimes with generics:
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
Advanced Patterns¶
Multiple Lifetime Parameters:
fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x // Return only depends on 'a
}
// When lifetimes are related:
fn complex2<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > 0 { x } else { y } // 'b outlives 'a, so y is valid for 'a
}
Higher-Ranked Trait Bounds (HRTB):
fn apply_to_str<F>(f: F) -> String
where
F: for<'a> Fn(&'a str) -> &'a str, // F works for any lifetime
{
let s = String::from("hello");
f(&s).to_string()
}
Common Ownership Patterns¶
1. Taking and Returning Ownership:
fn process(mut data: Vec<i32>) -> Vec<i32> {
data.push(42);
data // Return ownership
}
2. Borrowing When Possible:
fn analyze(data: &[i32]) -> i32 {
data.iter().sum() // Just needs to read
}
3. Mutable Borrow for In-Place Modification:
fn double_all(data: &mut [i32]) {
for item in data.iter_mut() {
*item *= 2;
}
}
4. Clone When Necessary:
fn use_both(original: &str) -> (String, String) {
let copy = original.to_string();
let processed = copy.to_uppercase();
(copy, processed) // Need two owned values
}
5. Interior Mutability (RefCell, Cell):
When you need mutable access through an immutable reference:
use std::cell::RefCell;
struct Cache {
data: RefCell<Option<String>>,
}
impl Cache {
fn get(&self) -> String { // Note: &self, not &mut self
let mut cache = self.data.borrow_mut();
if cache.is_none() {
*cache = Some(String::from("computed value"));
}
cache.as_ref().unwrap().clone()
}
}
Best Practices¶
- Prefer borrowing over ownership transfer when you don't need to own the data.
- Use
&strinstead of&Stringfor function parameters (more flexible). - Avoid excessive cloning—it's often a sign of fighting the borrow checker.
- Let the compiler guide you—error messages explain what's wrong.
- Use slices for viewing portions of collections.
- Lifetimes are about ensuring safety, not controlling memory allocation.
- When in doubt, own the data—then refactor to borrowing if needed.
Bit Manipulation¶
Rust provides comprehensive bit manipulation operators and methods.
Basic Bit Operators¶
- & (AND): Returns 1 if both bits are 1
- | (OR): Returns 1 if at least one bit is 1
- ^ (XOR): Returns 1 if exactly one bit is 1
- ! (NOT): Inverts all bits
- \<< (Left shift): Shifts bits to the left
- >> (Right shift): Shifts bits to the right
Examples¶
// Bitwise AND
let a: u8 = 60; // 0011 1100
let b: u8 = 13; // 0000 1101
println!("{}", a & b); // 12 (0000 1100)
// Bitwise OR
println!("{}", a | b); // 61 (0011 1101)
// Bitwise XOR
println!("{}", a ^ b); // 49 (0011 0001)
// Bitwise NOT
println!("{}", !a); // 195 (1100 0011)
// Left Shift
println!("{}", a << 2); // 240 (1111 0000)
// Right Shift
println!("{}", a >> 2); // 15 (0000 1111)
Common Bit Manipulation Techniques¶
1. Check if a number is even or odd¶
fn is_even(num: i32) -> bool {
(num & 1) == 0
}
println!("{}", is_even(42)); // true
println!("{}", is_even(7)); // false
2. Check if the ith bit is set¶
fn is_bit_set(num: u32, i: u32) -> bool {
(num & (1 << i)) != 0
}
println!("{}", is_bit_set(10, 1)); // true (10 is 1010 in binary, bit 1 is set)
println!("{}", is_bit_set(10, 0)); // false (bit 0 is not set)
3. Set the ith bit¶
fn set_bit(num: u32, i: u32) -> u32 {
num | (1 << i)
}
println!("{}", set_bit(10, 0)); // 11 (changes 1010 to 1011)
4. Clear the ith bit¶
fn clear_bit(num: u32, i: u32) -> u32 {
num & !(1 << i)
}
println!("{}", clear_bit(10, 1)); // 8 (changes 1010 to 1000)
5. Toggle the ith bit¶
fn toggle_bit(num: u32, i: u32) -> u32 {
num ^ (1 << i)
}
println!("{}", toggle_bit(10, 0)); // 11 (changes 1010 to 1011)
println!("{}", toggle_bit(10, 1)); // 8 (changes 1010 to 1000)
6. Count set bits (Hamming weight)¶
fn count_set_bits(mut num: u32) -> u32 {
let mut count = 0;
while num != 0 {
count += num & 1;
num >>= 1;
}
count
}
// Using built-in method
fn count_set_bits_builtin(num: u32) -> u32 {
num.count_ones()
}
println!("{}", count_set_bits(10)); // 2 (1010 has two 1s)
Practical Applications¶
1. Bit Masking¶
// Using bit masks to store multiple boolean flags in a single integer
const READ: u8 = 1; // 001
const WRITE: u8 = 2; // 010
const EXECUTE: u8 = 4; // 100
// Set permissions
let mut permissions: u8 = 0;
permissions |= READ; // Add read permission
permissions |= WRITE; // Add write permission
// Check permissions
let has_read = (permissions & READ) != 0;
let has_write = (permissions & WRITE) != 0;
let has_execute = (permissions & EXECUTE) != 0;
println!("Read: {}, Write: {}, Execute: {}", has_read, has_write, has_execute);
// Output: Read: true, Write: true, Execute: false
2. Power of Two¶
fn is_power_of_two(num: u32) -> bool {
num > 0 && (num & (num - 1)) == 0
}
println!("{}", is_power_of_two(16)); // true
println!("{}", is_power_of_two(18)); // false
Advanced Techniques¶
1. Swapping variables without a temporary variable¶
fn swap_without_temp(mut a: i32, mut b: i32) -> (i32, i32) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
(a, b)
}
let (a, b) = swap_without_temp(5, 7);
println!("a = {}, b = {}", a, b); // a = 7, b = 5
2. Find the single number in an array where all other numbers appear twice¶
fn find_single(nums: &[i32]) -> i32 {
nums.iter().fold(0, |acc, &x| acc ^ x)
}
println!("{}", find_single(&[4, 1, 2, 1, 2])); // 4
Bit manipulation is particularly useful in systems programming, cryptography, embedded systems, and performance-critical algorithms.
Mathematics and Geometry¶
Rust provides excellent support for mathematical operations through the standard library and ecosystem.
Core Mathematical Libraries¶
- Standard Library: Basic math functions in
std::f32andstd::f64modules. - num: Comprehensive numeric traits and types (
num-traits,num-complex). - nalgebra: Linear algebra library for vectors, matrices, and geometric transformations.
- cgmath: Computer graphics math library.
- rust_decimal: Decimal arithmetic for financial calculations.
// Basic arithmetic
let a = 5.0;
let b = 3.0;
println!("{}", a + b); // 8.0
// Math functions
use std::f64::consts::PI;
let angle = PI / 4.0;
println!("sin(Ï€/4) = {}", angle.sin());
println!("cos(Ï€/4) = {}", angle.cos());
Geometry in Rust¶
1. Using nalgebra for Linear Algebra¶
// Add to Cargo.toml: nalgebra = "0.32"
use nalgebra::{Vector2, Matrix2, Point2};
// Calculate Euclidean distance between two points
let point1 = Point2::new(0.0, 0.0);
let point2 = Point2::new(3.0, 4.0);
let distance = (point2 - point1).norm();
println!("Distance: {}", distance); // Distance: 5.0
// Matrix operations
let matrix_a = Matrix2::new(1.0, 2.0, 3.0, 4.0);
let matrix_b = Matrix2::new(5.0, 6.0, 7.0, 8.0);
let result = matrix_a * matrix_b; // Matrix multiplication
println!("{}", result);
2. Custom Geometry Types¶
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance_to(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
#[derive(Debug)]
struct Triangle {
p1: Point,
p2: Point,
p3: Point,
}
impl Triangle {
fn area(&self) -> f64 {
// Using cross product
let v1 = Point::new(self.p2.x - self.p1.x, self.p2.y - self.p1.y);
let v2 = Point::new(self.p3.x - self.p1.x, self.p3.y - self.p1.y);
let cross = v1.x * v2.y - v1.y * v2.x;
(cross.abs()) / 2.0
}
}
let triangle = Triangle {
p1: Point::new(0.0, 0.0),
p2: Point::new(1.0, 0.0),
p3: Point::new(0.0, 2.0),
};
println!("Triangle area: {}", triangle.area()); // Triangle area: 1.0
3. Geometric Transformations¶
use nalgebra::{Rotation2, Vector2};
// 2D rotation (45 degrees)
let angle = std::f64::consts::PI / 4.0;
let rotation = Rotation2::new(angle);
// Define a point
let point = Vector2::new(1.0, 0.0);
// Apply rotation
let rotated_point = rotation * point;
println!("Original point: ({}, {})", point.x, point.y);
println!("Rotated point: ({}, {})", rotated_point.x, rotated_point.y);
Advanced Geometric Calculations¶
Computational Geometry Algorithms¶
// Convex Hull using Graham scan (simplified)
fn convex_hull(points: &[Point]) -> Vec<Point> {
if points.len() < 3 {
return points.to_vec();
}
// Find bottom-most point (or leftmost in case of tie)
let mut bottom = 0;
for i in 1..points.len() {
if points[i].y < points[bottom].y ||
(points[i].y == points[bottom].y && points[i].x < points[bottom].x) {
bottom = i;
}
}
// Sort points by polar angle with respect to bottom point
// Implementation details omitted for brevity
points.to_vec() // Simplified
}
Advanced Rust Concepts and Techniques¶
Rust offers powerful advanced features that enable safe systems programming, zero-cost abstractions, and fearless concurrency. These features form the foundation for writing high-performance, correct, and maintainable Rust code. Understanding them deeply is essential for mastering Rust and leveraging its full potential.
- Traits - Define shared behavior and enable polymorphism
- Generics - Write code that works with multiple types
- Error Handling - Robust error management without exceptions
- Smart Pointers - Safe memory management abstractions
- Concurrency - Fearless parallelism with compile-time guarantees
- Macros - Metaprogramming and code generation
- Unsafe Rust - Low-level control when needed
- Async/Await - Efficient asynchronous programming
- Iterators - Lazy, composable sequence processing
- Closures - Anonymous functions with environment capture
1. Traits¶
Traits are Rust's primary mechanism for defining shared behavior across types. They serve multiple roles: defining interfaces (like Java/C# interfaces), enabling polymorphism (both static and dynamic), implementing operator overloading, and providing extension methods. Unlike inheritance-based OOP, traits promote composition and are central to Rust's "zero-cost abstraction" philosophy—trait method calls are typically resolved at compile time with no runtime overhead.
Key Concepts:
- Trait Definition: Declares method signatures (and optionally default implementations) that implementing types must provide
- Trait Implementation: Types implement traits using
impl Trait for Typesyntax - Trait Bounds: Constrain generic types to those implementing specific traits
- Trait Objects: Enable dynamic dispatch via
dyn Traitfor runtime polymorphism - Associated Types: Type placeholders defined within traits
- Blanket Implementations: Implement traits for all types matching certain bounds
- The Orphan Rule: Either the trait or the type must be local to your crate
Basic Trait Definition and Implementation¶
Traits define a set of methods that a type must implement. The implementing type provides concrete behavior:
// Define a trait with a required method
trait Drawable {
fn draw(&self);
// Optional: method with default implementation
fn description(&self) -> String {
String::from("A drawable shape")
}
}
struct Circle {
radius: f64,
center: (f64, f64),
}
struct Rectangle {
width: f64,
height: f64,
position: (f64, f64),
}
// Implement the trait for Circle
impl Drawable for Circle {
fn draw(&self) {
println!(
"Drawing circle at ({}, {}) with radius {}",
self.center.0, self.center.1, self.radius
);
}
// Override the default implementation
fn description(&self) -> String {
format!("Circle with radius {:.2}", self.radius)
}
}
// Implement the trait for Rectangle
impl Drawable for Rectangle {
fn draw(&self) {
println!(
"Drawing rectangle at ({}, {}) - {}x{}",
self.position.0, self.position.1, self.width, self.height
);
}
// Uses default description() implementation
}
fn main() {
let circle = Circle { radius: 5.0, center: (0.0, 0.0) };
let rect = Rectangle { width: 10.0, height: 20.0, position: (5.0, 5.0) };
circle.draw();
println!("Description: {}", circle.description());
rect.draw();
println!("Description: {}", rect.description()); // Uses default
}
Default Implementations and Method Dependencies¶
Traits can provide default implementations that call other trait methods. This creates powerful composition patterns:
trait Summary {
// Required method - must be implemented
fn summarize_author(&self) -> String;
// Default implementation calling the required method
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
// Another default method
fn preview(&self) -> String {
let summary = self.summarize();
if summary.len() > 50 {
format!("{}...", &summary[..47])
} else {
summary
}
}
}
struct Article {
title: String,
author: String,
content: String,
}
struct Tweet {
username: String,
content: String,
retweets: u32,
}
impl Summary for Article {
fn summarize_author(&self) -> String {
self.author.clone()
}
// Override the default summarize
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// Uses default summarize() and preview()
}
Trait Bounds and Generic Constraints¶
Trait bounds constrain generic types to only those implementing specific traits. This enables writing generic code that relies on specific behavior:
use std::fmt::{Display, Debug};
// Simple trait bound using : syntax
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple bounds with + syntax
fn notify_and_display<T: Summary + Display>(item: &T) {
println!("Item: {}", item);
println!("Summary: {}", item.summarize());
}
// Complex bounds are clearer with where clause
fn complex_function<T, U>(t: &T, u: &U) -> String
where
T: Display + Clone + Summary,
U: Clone + Debug + Default,
{
format!("t: {}, u: {:?}", t, u)
}
// Trait bounds on impl blocks - conditional implementation
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
// Only implement cmp_display for Pairs where T implements both traits
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
// impl Trait syntax - simpler for function parameters and returns
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("rustlang"),
content: String::from("Hello, Rustaceans!"),
retweets: 100,
}
}
Associated Types vs Generic Traits¶
Associated types define placeholder types within traits. They differ from generic traits in important ways:
// With associated types: one implementation per type
trait Iterator {
type Item; // Associated type - implementor chooses once
fn next(&mut self) -> Option<Self::Item>;
}
// With generics: multiple implementations possible per type
trait ConvertTo<T> {
fn convert(&self) -> T;
}
// Example: Counter can only have ONE Iterator implementation
struct Counter {
count: u32,
max: u32,
}
impl Iterator for Counter {
type Item = u32; // Fixed for Counter
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
// Example: A type can implement ConvertTo multiple times
struct Temperature(f64);
impl ConvertTo<f64> for Temperature {
fn convert(&self) -> f64 { self.0 }
}
impl ConvertTo<i32> for Temperature {
fn convert(&self) -> i32 { self.0 as i32 }
}
impl ConvertTo<String> for Temperature {
fn convert(&self) -> String { format!("{}°C", self.0) }
}
// Associated types with bounds
trait Container {
type Item: Clone + Debug; // Item must implement Clone and Debug
fn get(&self, index: usize) -> Option<&Self::Item>;
fn len(&self) -> usize;
}
Trait Objects and Dynamic Dispatch¶
Trait objects enable runtime polymorphism through dynamic dispatch. They use a vtable (virtual method table) to resolve method calls at runtime:
trait Drawable {
fn draw(&self);
fn bounding_box(&self) -> (f64, f64, f64, f64); // (x, y, width, height)
}
struct Circle { x: f64, y: f64, radius: f64 }
struct Rectangle { x: f64, y: f64, width: f64, height: f64 }
struct Triangle { points: [(f64, f64); 3] }
impl Drawable for Circle {
fn draw(&self) { println!("Drawing circle at ({}, {})", self.x, self.y); }
fn bounding_box(&self) -> (f64, f64, f64, f64) {
(self.x - self.radius, self.y - self.radius,
self.radius * 2.0, self.radius * 2.0)
}
}
impl Drawable for Rectangle {
fn draw(&self) { println!("Drawing rectangle at ({}, {})", self.x, self.y); }
fn bounding_box(&self) -> (f64, f64, f64, f64) {
(self.x, self.y, self.width, self.height)
}
}
impl Drawable for Triangle {
fn draw(&self) { println!("Drawing triangle"); }
fn bounding_box(&self) -> (f64, f64, f64, f64) {
let (min_x, max_x) = self.points.iter()
.fold((f64::MAX, f64::MIN), |(min, max), p| (min.min(p.0), max.max(p.0)));
let (min_y, max_y) = self.points.iter()
.fold((f64::MAX, f64::MIN), |(min, max), p| (min.min(p.1), max.max(p.1)));
(min_x, min_y, max_x - min_x, max_y - min_y)
}
}
// Using trait objects for heterogeneous collections
fn draw_all(shapes: &[Box<dyn Drawable>]) {
for shape in shapes {
shape.draw(); // Dynamic dispatch via vtable
}
}
// Trait objects with references
fn draw_shape(shape: &dyn Drawable) {
shape.draw();
}
fn main() {
// Heterogeneous collection of shapes
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { x: 0.0, y: 0.0, radius: 5.0 }),
Box::new(Rectangle { x: 10.0, y: 10.0, width: 20.0, height: 15.0 }),
Box::new(Triangle { points: [(0.0, 0.0), (5.0, 10.0), (10.0, 0.0)] }),
];
draw_all(&shapes);
// Calculate total bounding area
let total_area: f64 = shapes.iter()
.map(|s| {
let (_, _, w, h) = s.bounding_box();
w * h
})
.sum();
println!("Total bounding area: {}", total_area);
}
Object Safety: Not all traits can be made into trait objects. A trait is object-safe if:
- All methods have
selfas a receiver (notSelftype in parameters/return) - No generic type parameters on methods
- No associated functions (methods without
self)
// Object-safe trait
trait ObjectSafe {
fn method(&self);
fn method_with_param(&self, x: i32);
}
// NOT object-safe (cannot use as dyn ObjectUnsafe)
trait ObjectUnsafe {
fn returns_self(&self) -> Self; // Returns Self
fn generic_method<T>(&self, x: T); // Generic method
fn static_method(); // No self parameter
}
Supertraits and Trait Inheritance¶
Traits can require other traits as prerequisites (supertraits):
use std::fmt::Display;
// OutlinePrint requires Display as a supertrait
trait OutlinePrint: Display {
fn outline_print(&self) {
let output = self.to_string(); // Can use Display methods
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
// Multiple supertraits
trait Drawable: Clone + Debug {
fn draw(&self);
}
struct Point {
x: i32,
y: i32,
}
impl Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl OutlinePrint for Point {} // Must also implement Display
Blanket Implementations¶
Implement traits for all types matching certain bounds:
use std::fmt::Display;
// Blanket implementation: ToString for all Display types
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
// Custom blanket implementation example
trait Printable {
fn print(&self);
}
// Implement Printable for ALL types that implement Debug
impl<T: std::fmt::Debug> Printable for T {
fn print(&self) {
println!("{:?}", self);
}
}
// Now any Debug type automatically has print()
fn demo() {
let vec = vec![1, 2, 3];
vec.print(); // Works because Vec<i32> implements Debug
"hello".print(); // Works for &str
42.print(); // Works for i32
}
Operator Overloading with Traits¶
Rust uses traits from std::ops for operator overloading:
use std::ops::{Add, Sub, Mul, Neg, Index};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Vector2D {
x: f64,
y: f64,
}
impl Vector2D {
fn new(x: f64, y: f64) -> Self {
Vector2D { x, y }
}
fn magnitude(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
// Implement + operator
impl Add for Vector2D {
type Output = Vector2D;
fn add(self, other: Vector2D) -> Vector2D {
Vector2D {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// Implement - operator
impl Sub for Vector2D {
type Output = Vector2D;
fn sub(self, other: Vector2D) -> Vector2D {
Vector2D {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
// Implement * for scalar multiplication
impl Mul<f64> for Vector2D {
type Output = Vector2D;
fn mul(self, scalar: f64) -> Vector2D {
Vector2D {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
// Implement unary - (negation)
impl Neg for Vector2D {
type Output = Vector2D;
fn neg(self) -> Vector2D {
Vector2D {
x: -self.x,
y: -self.y,
}
}
}
fn main() {
let v1 = Vector2D::new(3.0, 4.0);
let v2 = Vector2D::new(1.0, 2.0);
let sum = v1 + v2;
let diff = v1 - v2;
let scaled = v1 * 2.0;
let negated = -v1;
println!("v1 + v2 = {:?}", sum);
println!("v1 - v2 = {:?}", diff);
println!("v1 * 2 = {:?}", scaled);
println!("-v1 = {:?}", negated);
println!("|v1| = {}", v1.magnitude());
}
Marker Traits and Auto Traits¶
Marker traits have no methods but indicate type properties:
// Standard marker traits (auto-implemented by the compiler)
// Send: Safe to transfer ownership to another thread
// Sync: Safe to share references between threads
use std::marker::PhantomData;
// Creating a custom marker trait
trait Immutable {} // Marker: type should not be mutated
// PhantomData for unused type parameters
struct Container<T> {
data: Vec<u8>,
_marker: PhantomData<T>, // Indicates T is logically owned
}
impl<T> Container<T> {
fn new() -> Self {
Container {
data: Vec::new(),
_marker: PhantomData,
}
}
}
// Negative trait implementations (unstable, shown for understanding)
// impl !Send for MyType {} // Explicitly opt-out of Send
The Orphan Rule and Newtype Pattern¶
The orphan rule prevents implementing external traits on external types. The newtype pattern provides a workaround:
use std::fmt::{Display, Formatter, Result};
// Can't implement Display for Vec<String> directly (both external)
// Solution: Newtype pattern
struct Wrapper(Vec<String>);
impl Display for Wrapper {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "[{}]", self.0.join(", "))
}
}
// Deref to use Vec methods transparently
impl std::ops::Deref for Wrapper {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let w = Wrapper(vec!["hello".to_string(), "world".to_string()]);
println!("{}", w); // Uses our Display implementation
println!("Length: {}", w.len()); // Uses Vec's len() via Deref
}
Extension Traits Pattern¶
Add methods to existing types by defining a new trait:
// Extension trait for &str
trait StringExt {
fn is_blank(&self) -> bool;
fn truncate_with_ellipsis(&self, max_len: usize) -> String;
}
impl StringExt for str {
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
fn truncate_with_ellipsis(&self, max_len: usize) -> String {
if self.len() <= max_len {
self.to_string()
} else if max_len <= 3 {
"...".to_string()
} else {
format!("{}...", &self[..max_len - 3])
}
}
}
fn main() {
let text = " ";
println!("Is blank: {}", text.is_blank()); // true
let long_text = "This is a very long string that needs truncation";
println!("{}", long_text.truncate_with_ellipsis(20)); // "This is a very lo..."
}
2. Generics¶
Generics enable writing code that works with multiple types while maintaining full type safety. Rust implements generics through monomorphization—the compiler generates specialized code for each concrete type used, resulting in zero runtime overhead. This "pay for what you use" approach means generic code runs exactly as fast as hand-written type-specific code.
Key Concepts:
- Type Parameters: Placeholders like
<T>that represent any type - Lifetime Parameters: Generic lifetimes like
<'a>for reference validity - Const Generics: Compile-time constant values as parameters
- Monomorphization: Compiler generates specialized code for each concrete type
- Turbofish Syntax: Explicit type specification with
::<Type>
Basic Generic Functions¶
Generic functions work with any type that satisfies their trait bounds:
// Generic function with trait bounds
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
// Without Copy bound - returns reference
fn largest_ref<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// Multiple type parameters
fn combine<T, U>(first: T, second: U) -> (T, U) {
(first, second)
}
// Generic with complex bounds
fn debug_and_clone<T>(item: &T) -> T
where
T: std::fmt::Debug + Clone,
{
println!("Debug: {:?}", item);
item.clone()
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("Largest number: {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("Largest char: {}", result);
// Turbofish syntax for explicit type annotation
let parsed = "42".parse::<i32>().unwrap();
let collected: Vec<i32> = (0..10).collect();
let collected_turbofish = (0..10).collect::<Vec<i32>>();
}
Generic Structs, Enums, and Methods¶
// Generic struct with single type parameter
struct Point<T> {
x: T,
y: T,
}
// Generic struct with multiple type parameters
struct KeyValue<K, V> {
key: K,
value: V,
}
// Methods on generic structs
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &T {
&self.y
}
}
// Method with additional type parameters
impl<T> Point<T> {
fn mixup<U>(self, other: Point<U>) -> Point<T> {
Point {
x: self.x,
y: other.y, // This won't compile - types differ
}
}
}
// Correct version with mixed result
impl<T, U> Point<T> {
fn mixup_correct<V, W>(self, other: Point<V>) -> (T, V) {
(self.x, other.x)
}
}
// Specialized implementation for specific types
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
fn distance_to(&self, other: &Point<f64>) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
// Conditional trait implementation
impl<T: std::fmt::Display> Point<T> {
fn display(&self) {
println!("Point({}, {})", self.x, self.y);
}
}
// Standard library generic enums
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
// Custom generic enum
enum BinaryTree<T> {
Empty,
Node {
value: T,
left: Box<BinaryTree<T>>,
right: Box<BinaryTree<T>>,
},
}
impl<T: Ord> BinaryTree<T> {
fn new() -> Self {
BinaryTree::Empty
}
fn insert(&mut self, value: T) {
match self {
BinaryTree::Empty => {
*self = BinaryTree::Node {
value,
left: Box::new(BinaryTree::Empty),
right: Box::new(BinaryTree::Empty),
};
}
BinaryTree::Node { value: ref node_value, left, right } => {
if value < *node_value {
left.insert(value);
} else {
right.insert(value);
}
}
}
}
}
Const Generics¶
Const generics allow using compile-time constant values as type parameters. This is powerful for array sizes, buffer capacities, and other compile-time configurations:
// Array wrapper with const generic size
#[derive(Debug)]
struct Array<T, const N: usize> {
data: [T; N],
}
impl<T: Default + Copy, const N: usize> Array<T, N> {
fn new() -> Self {
Array {
data: [T::default(); N],
}
}
fn len(&self) -> usize {
N // Compile-time constant
}
fn is_empty(&self) -> bool {
N == 0
}
}
impl<T, const N: usize> Array<T, N> {
fn from_array(data: [T; N]) -> Self {
Array { data }
}
}
// Const generic with bounds
struct FixedBuffer<const SIZE: usize>
where
[(); SIZE]: Sized, // Ensure SIZE creates valid array
{
data: [u8; SIZE],
len: usize,
}
impl<const SIZE: usize> FixedBuffer<SIZE> {
fn new() -> Self {
FixedBuffer {
data: [0; SIZE],
len: 0,
}
}
fn push(&mut self, byte: u8) -> Result<(), &'static str> {
if self.len >= SIZE {
Err("Buffer full")
} else {
self.data[self.len] = byte;
self.len += 1;
Ok(())
}
}
}
// Const generics in functions
fn split_at_middle<T, const N: usize>(arr: [T; N]) -> ([T; N/2], [T; N/2])
where
T: Default + Copy,
{
let mut left = [T::default(); N/2];
let mut right = [T::default(); N/2];
for (i, item) in arr.into_iter().enumerate() {
if i < N/2 {
left[i] = item;
} else if i < N {
right[i - N/2] = item;
}
}
(left, right)
}
fn main() {
let arr: Array<i32, 5> = Array::new();
println!("Length: {}", arr.len());
let mut buffer: FixedBuffer<1024> = FixedBuffer::new();
buffer.push(42).unwrap();
let data = [1, 2, 3, 4, 5, 6];
let (left, right) = split_at_middle(data);
println!("Left: {:?}, Right: {:?}", left, right);
}
Lifetime Generics¶
Lifetimes are a form of generics that ensure references remain valid:
// Lifetime parameter ensures returned reference is valid
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Multiple lifetime parameters
fn first_word<'a, 'b>(s: &'a str, _marker: &'b str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
// Struct with lifetime parameter
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3
}
// Method returning reference with same lifetime
fn announce_and_return_part(&self, announcement: &str) -> &'a str {
println!("Attention: {}", announcement);
self.part
}
}
// Combining lifetimes with generic types
struct ImportantExcerpt<'a, T> {
part: &'a str,
metadata: T,
}
impl<'a, T: std::fmt::Debug> ImportantExcerpt<'a, T> {
fn display(&self) {
println!("Part: {}, Metadata: {:?}", self.part, self.metadata);
}
}
// Static lifetime - lives for entire program duration
fn static_string() -> &'static str {
"I live forever!"
}
// Lifetime bounds on generic types
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: std::fmt::Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() { x } else { y }
}
PhantomData and Variance¶
PhantomData indicates ownership of a type parameter without storing it:
use std::marker::PhantomData;
// PhantomData indicates T is logically owned even if not stored
struct Identifier<T> {
id: u64,
_marker: PhantomData<T>, // Zero-sized, indicates type association
}
struct User;
struct Product;
impl<T> Identifier<T> {
fn new(id: u64) -> Self {
Identifier { id, _marker: PhantomData }
}
}
fn process_user(id: Identifier<User>) {
println!("Processing user {}", id.id);
}
fn main() {
let user_id: Identifier<User> = Identifier::new(42);
let product_id: Identifier<Product> = Identifier::new(42);
process_user(user_id); // OK
// process_user(product_id); // Error: expected Identifier<User>
}
// PhantomData affecting variance
struct Invariant<'a, T> {
data: *mut T,
_marker: PhantomData<&'a mut T>, // Makes T invariant
}
// Covariant example
struct Covariant<'a, T> {
data: &'a T, // Covariant in 'a and T
}
Generic Associated Types (GATs)¶
GATs allow associated types in traits to be generic (stabilized in Rust 1.65):
// GAT example: Lending iterator pattern
trait LendingIterator {
type Item<'a> where Self: 'a; // GAT: Item is generic over lifetime
fn next(&mut self) -> Option<Self::Item<'_>>;
}
// Implementation example
struct WindowsMut<'a, T> {
slice: &'a mut [T],
start: usize,
window_size: usize,
}
impl<'a, T> LendingIterator for WindowsMut<'a, T> {
type Item<'b> = &'b mut [T] where Self: 'b;
fn next(&mut self) -> Option<Self::Item<'_>> {
if self.start + self.window_size > self.slice.len() {
return None;
}
let window = &mut self.slice[self.start..self.start + self.window_size];
self.start += 1;
// This requires unsafe in practice due to reborrowing constraints
Some(unsafe { &mut *(window as *mut [T]) })
}
}
// Simpler GAT example: Collection trait
trait Collection {
type Item;
type Iter<'a>: Iterator<Item = &'a Self::Item> where Self: 'a;
fn iter(&self) -> Self::Iter<'_>;
}
impl<T> Collection for Vec<T> {
type Item = T;
type Iter<'a> = std::slice::Iter<'a, T> where T: 'a;
fn iter(&self) -> Self::Iter<'_> {
self.as_slice().iter()
}
}
3. Error Handling¶
Rust takes a unique approach to error handling that avoids exceptions entirely. Instead, it uses the type system to encode error possibilities, making error handling explicit and enforced by the compiler. This leads to more robust code where errors cannot be accidentally ignored.
Two Categories of Errors:
- Recoverable errors (
Result<T, E>): Expected failures that can be handled gracefully - Unrecoverable errors (
panic!): Bugs or invariant violations where the program cannot continue
Key Concepts:
- Result Type: Encodes success (
Ok) or failure (Err) in the type system - Option Type: Encodes presence (
Some) or absence (None) of a value ?Operator: Ergonomic error propagation that returns early on error- Custom Error Types: Application-specific errors with context
- Error Conversion:
Fromtrait for automatic error type conversion - Error Crates:
thiserrorfor libraries,anyhowfor applications
The Result Type¶
Result<T, E> is Rust's primary error handling type:
use std::fs::File;
use std::io::{self, Read, Write};
// Basic Result usage
fn read_file_contents(filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?; // ? returns early on error
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// Chaining with ? operator
fn read_first_line(filename: &str) -> Result<String, io::Error> {
let contents = read_file_contents(filename)?;
let first_line = contents
.lines()
.next()
.unwrap_or("")
.to_string();
Ok(first_line)
}
// Using combinators instead of ?
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path) // Returns Result<String, io::Error>
.map(|s| s.trim().to_string())
}
// Result methods for transformation
fn demonstrate_result_methods() {
let ok: Result<i32, &str> = Ok(5);
let err: Result<i32, &str> = Err("error");
// map: Transform success value
let doubled = ok.map(|x| x * 2); // Ok(10)
// map_err: Transform error value
let new_err = err.map_err(|e| format!("Error: {}", e));
// and_then: Chain operations that return Result
let chained = ok.and_then(|x| {
if x > 0 { Ok(x * 2) } else { Err("negative") }
});
// or_else: Provide fallback for errors
let recovered = err.or_else(|_| Ok(0));
// unwrap_or: Provide default value on error
let with_default = err.unwrap_or(0); // 0
// unwrap_or_else: Compute default lazily
let computed = err.unwrap_or_else(|e| {
println!("Recovering from: {}", e);
42
});
// ok(): Convert Result<T, E> to Option<T>
let maybe: Option<i32> = ok.ok(); // Some(5)
// err(): Convert Result<T, E> to Option<E>
let maybe_err: Option<&str> = err.err(); // Some("error")
}
fn main() {
match read_file_contents("config.txt") {
Ok(contents) => println!("Config: {}", contents),
Err(e) => eprintln!("Failed to read config: {}", e),
}
// Using if let for simple cases
if let Ok(contents) = read_file_contents("optional.txt") {
println!("Optional file: {}", contents);
}
}
The Option Type¶
Option<T> handles the absence of values without null:
fn find_user(id: u64) -> Option<String> {
let users = vec![
(1, "Alice"),
(2, "Bob"),
(3, "Charlie"),
];
users.iter()
.find(|(user_id, _)| *user_id == id)
.map(|(_, name)| name.to_string())
}
fn demonstrate_option_methods() {
let some_value: Option<i32> = Some(5);
let none_value: Option<i32> = None;
// map: Transform inner value
let doubled = some_value.map(|x| x * 2); // Some(10)
// and_then (flatMap): Chain Option-returning functions
let chained = some_value.and_then(|x| {
if x > 0 { Some(x * 2) } else { None }
});
// or: Provide fallback Option
let with_fallback = none_value.or(Some(0)); // Some(0)
// or_else: Compute fallback lazily
let computed = none_value.or_else(|| Some(42));
// unwrap_or: Default value
let value = none_value.unwrap_or(0); // 0
// filter: Keep Some only if predicate matches
let filtered = some_value.filter(|x| *x > 10); // None
// ok_or: Convert Option<T> to Result<T, E>
let result: Result<i32, &str> = some_value.ok_or("no value");
// ok_or_else: Lazy error creation
let result2: Result<i32, String> = none_value
.ok_or_else(|| format!("Value not found"));
// take: Take value out, leaving None
let mut opt = Some(5);
let taken = opt.take(); // Some(5), opt is now None
// replace: Replace value, returning old
let mut opt2 = Some(5);
let old = opt2.replace(10); // Some(5), opt2 is Some(10)
}
// Combining Option with iterators
fn get_first_even(numbers: &[i32]) -> Option<i32> {
numbers.iter()
.copied()
.find(|n| n % 2 == 0)
}
// Optional chaining with ?
fn get_nested_value(data: &Option<Vec<Option<i32>>>) -> Option<i32> {
let vec = data.as_ref()?;
let first = vec.first()?;
*first
}
Custom Error Types¶
For robust error handling, define custom error types:
use std::fmt;
use std::error::Error;
// Custom error enum with multiple variants
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Validation { field: String, message: String },
NotFound(String),
Unauthorized,
RateLimit { retry_after: u64 },
}
// Implement Display for user-friendly messages
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "I/O error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::Validation { field, message } => {
write!(f, "Validation failed for '{}': {}", field, message)
}
AppError::NotFound(resource) => {
write!(f, "Resource not found: {}", resource)
}
AppError::Unauthorized => write!(f, "Unauthorized access"),
AppError::RateLimit { retry_after } => {
write!(f, "Rate limited. Retry after {} seconds", retry_after)
}
}
}
}
// Implement Error trait for standard error handling
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
_ => None,
}
}
}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::Io(err)
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(err: std::num::ParseIntError) -> Self {
AppError::Parse(err)
}
}
// Usage example
fn process_config(path: &str) -> Result<i32, AppError> {
let contents = std::fs::read_to_string(path)?; // Auto-converts io::Error
let value: i32 = contents.trim().parse()?; // Auto-converts ParseIntError
if value < 0 {
return Err(AppError::Validation {
field: "config_value".to_string(),
message: "must be non-negative".to_string(),
});
}
Ok(value)
}
// Printing error chain
fn print_error_chain(err: &dyn Error) {
eprintln!("Error: {}", err);
let mut source = err.source();
while let Some(cause) = source {
eprintln!("Caused by: {}", cause);
source = cause.source();
}
}
Using thiserror for Library Errors¶
The thiserror crate reduces boilerplate for custom errors:
// Add to Cargo.toml: thiserror = "1.0"
use thiserror::Error;
#[derive(Error, Debug)]
enum DataError {
#[error("Failed to read data from {path}: {source}")]
ReadError {
path: String,
#[source]
source: std::io::Error,
},
#[error("Failed to parse data: {0}")]
ParseError(#[from] serde_json::Error),
#[error("Invalid data format: expected {expected}, got {actual}")]
FormatError {
expected: String,
actual: String,
},
#[error("Data validation failed")]
ValidationError(#[source] ValidationError),
#[error(transparent)] // Delegate Display to inner error
Other(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
#[error("Validation error in field '{field}': {message}")]
struct ValidationError {
field: String,
message: String,
}
Using anyhow for Application Errors¶
The anyhow crate simplifies error handling in applications:
// Add to Cargo.toml: anyhow = "1.0"
use anyhow::{Context, Result, bail, ensure, anyhow};
// Result is anyhow::Result<T> = std::result::Result<T, anyhow::Error>
fn load_config(path: &str) -> Result<Config> {
let contents = std::fs::read_to_string(path)
.context(format!("Failed to read config from {}", path))?;
let config: Config = serde_json::from_str(&contents)
.context("Failed to parse config JSON")?;
// Validation with bail! macro
if config.timeout == 0 {
bail!("Config timeout must be non-zero");
}
// Validation with ensure! macro
ensure!(config.max_connections > 0, "max_connections must be positive");
Ok(config)
}
// Creating ad-hoc errors
fn validate_input(input: &str) -> Result<()> {
if input.is_empty() {
return Err(anyhow!("Input cannot be empty"));
}
if input.len() > 1000 {
bail!("Input too long: {} chars (max 1000)", input.len());
}
Ok(())
}
// Downcasting to specific error types
fn handle_error(err: anyhow::Error) {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
eprintln!("I/O Error: {}", io_err);
} else if let Some(parse_err) = err.downcast_ref::<serde_json::Error>() {
eprintln!("JSON Error: {}", parse_err);
} else {
eprintln!("Unknown error: {}", err);
}
// Print full error chain
eprintln!("\nError chain:");
for (i, cause) in err.chain().enumerate() {
eprintln!(" {}: {}", i, cause);
}
}
#[derive(serde::Deserialize)]
struct Config {
timeout: u64,
max_connections: u32,
}
Error Handling Patterns and Best Practices¶
use std::fs::File;
// Pattern 1: unwrap/expect for prototypes and tests
fn quick_prototype() {
let file = File::open("data.txt").unwrap();
let config = load_config().expect("Config must exist");
}
// Pattern 2: Match for granular control
fn match_handling() {
let file = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Cannot create file: {:?}", e),
}
}
other_error => {
panic!("Cannot open file: {:?}", other_error);
}
},
};
}
// Pattern 3: Combinators for functional style
fn combinator_style() -> Result<String, std::io::Error> {
std::fs::read_to_string("config.txt")
.map(|s| s.trim().to_uppercase())
.or_else(|_| Ok("DEFAULT".to_string()))
}
// Pattern 4: Early return with ?
fn early_return(path: &str) -> Result<Data, AppError> {
let file = File::open(path)?;
let contents = read_contents(file)?;
let parsed = parse_data(&contents)?;
validate(&parsed)?;
Ok(parsed)
}
// Pattern 5: Collecting Results
fn process_all_files(paths: &[&str]) -> Result<Vec<String>, std::io::Error> {
paths.iter()
.map(|path| std::fs::read_to_string(path))
.collect() // Collects into Result<Vec<String>, Error>
}
// Pattern 6: partition_map for separating successes and failures
fn process_with_partial_failure(inputs: Vec<&str>) -> (Vec<i32>, Vec<String>) {
let (successes, failures): (Vec<_>, Vec<_>) = inputs
.into_iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
let values: Vec<i32> = successes.into_iter().map(Result::unwrap).collect();
let errors: Vec<String> = failures.into_iter()
.map(|r| r.unwrap_err().to_string())
.collect();
(values, errors)
}
// When to use panic! vs Result
// - panic!: Programming errors, violated invariants, unrecoverable states
// - Result: Expected failures, user input errors, external system failures
fn safe_divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
// Panic for invariant violations (should never happen if code is correct)
fn get_element(vec: &[i32], index: usize) -> i32 {
assert!(index < vec.len(), "Index out of bounds: BUG in caller");
vec[index]
}
struct Data;
fn load_config() -> Result<(), ()> { Ok(()) }
fn read_contents(_: File) -> Result<Data, AppError> { Ok(Data) }
fn parse_data(_: &Data) -> Result<Data, AppError> { Ok(Data) }
fn validate(_: &Data) -> Result<(), AppError> { Ok(()) }
4. Smart Pointers¶
Smart pointers are data structures that act like pointers but include additional metadata and capabilities. They implement the Deref trait (allowing them to be used like references) and the Drop trait (for custom cleanup logic). Rust's ownership system ensures smart pointers are used safely, preventing memory leaks and dangling pointers.
Common Smart Pointers:
| Type | Purpose | Thread Safety | Overhead |
|---|---|---|---|
Box<T> |
Heap allocation, single ownership | Send + Sync if T is | Minimal (pointer only) |
Rc<T> |
Reference counting, shared ownership | Single-threaded only | Reference count |
Arc<T> |
Atomic reference counting | Thread-safe | Atomic reference count |
Cell<T> |
Interior mutability (Copy types) | Single-threaded | None |
RefCell<T> |
Interior mutability with runtime checks | Single-threaded | Borrow state tracking |
Mutex<T> |
Thread-safe interior mutability | Thread-safe | Lock overhead |
RwLock<T> |
Multiple readers OR single writer | Thread-safe | Lock overhead |
Cow<T> |
Clone-on-write | Depends on T | Enum discriminant |
Box - Heap Allocation¶
Box<T> provides the simplest form of heap allocation with single ownership:
// Basic heap allocation
let boxed: Box<i32> = Box::new(5);
println!("boxed = {}", boxed);
// Use cases for Box:
// 1. Types with unknown size at compile time (trait objects)
// 2. Large data you want to transfer ownership without copying
// 3. Recursive types
// Recursive type: Linked list
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
use List::{Cons, Nil};
fn create_list() -> List<i32> {
Cons(1, Box::new(
Cons(2, Box::new(
Cons(3, Box::new(Nil))
))
))
}
// Box for trait objects
trait Animal {
fn speak(&self);
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn speak(&self) { println!("{} says woof!", self.name); }
}
impl Animal for Cat {
fn speak(&self) { println!("{} says meow!", self.name); }
}
fn get_pet(is_dog: bool) -> Box<dyn Animal> {
if is_dog {
Box::new(Dog { name: "Buddy".to_string() })
} else {
Box::new(Cat { name: "Whiskers".to_string() })
}
}
// Box::leak for static references
fn create_static_string(s: String) -> &'static str {
Box::leak(s.into_boxed_str())
}
// Box with custom allocators (nightly)
// let boxed = Box::new_in(42, CustomAllocator);
fn main() {
let list = create_list();
println!("{:?}", list);
let pet = get_pet(true);
pet.speak();
}
Rc - Reference Counting¶
Rc<T> enables shared ownership through reference counting (single-threaded only):
use std::rc::Rc;
// Basic Rc usage
fn demonstrate_rc() {
let a = Rc::new(5);
println!("Count after creating a: {}", Rc::strong_count(&a));
let b = Rc::clone(&a); // Increments count, doesn't deep clone
println!("Count after cloning to b: {}", Rc::strong_count(&a));
{
let c = Rc::clone(&a);
println!("Count after cloning to c: {}", Rc::strong_count(&a));
}
println!("Count after c goes out of scope: {}", Rc::strong_count(&a));
}
// Shared ownership example: Multiple nodes sharing a subgraph
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn build_shared_graph() {
// Shared node
let shared_node = Rc::new(Node {
value: 3,
children: vec![]
});
let node_a = Rc::new(Node {
value: 1,
children: vec![Rc::clone(&shared_node)],
});
let node_b = Rc::new(Node {
value: 2,
children: vec![Rc::clone(&shared_node)],
});
println!("Shared node has {} strong references",
Rc::strong_count(&shared_node));
}
// Rc::make_mut for copy-on-write semantics
fn copy_on_write() {
let mut data = Rc::new(vec![1, 2, 3]);
let data2 = Rc::clone(&data);
// make_mut will clone if there are other references
Rc::make_mut(&mut data).push(4);
println!("data: {:?}", data); // [1, 2, 3, 4]
println!("data2: {:?}", data2); // [1, 2, 3]
}
// Rc::try_unwrap - attempt to take ownership
fn try_unwrap_example() {
let rc = Rc::new("hello".to_string());
match Rc::try_unwrap(rc) {
Ok(value) => println!("Got ownership: {}", value),
Err(rc) => println!("Still shared: {}", rc),
}
}
Weak - Weak References¶
Weak<T> provides non-owning references to break reference cycles:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child relationship with weak back-reference
#[derive(Debug)]
struct TreeNode {
value: i32,
parent: RefCell<Weak<TreeNode>>, // Weak reference to parent
children: RefCell<Vec<Rc<TreeNode>>>,
}
fn build_tree() {
let leaf = Rc::new(TreeNode {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!("leaf strong = {}, weak = {}",
Rc::strong_count(&leaf), Rc::weak_count(&leaf));
{
let branch = Rc::new(TreeNode {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("branch strong = {}, weak = {}",
Rc::strong_count(&branch), Rc::weak_count(&branch));
println!("leaf parent = {:?}",
leaf.parent.borrow().upgrade().map(|n| n.value));
}
// branch is dropped, weak reference returns None
println!("leaf parent after branch dropped = {:?}",
leaf.parent.borrow().upgrade());
}
// Observer pattern with weak references
struct Observable {
observers: Vec<Weak<dyn Observer>>,
}
trait Observer {
fn update(&self, data: &str);
}
impl Observable {
fn add_observer(&mut self, observer: Weak<dyn Observer>) {
self.observers.push(observer);
}
fn notify(&mut self, data: &str) {
// Clean up dead observers and notify living ones
self.observers.retain(|weak| {
if let Some(observer) = weak.upgrade() {
observer.update(data);
true
} else {
false // Remove dead reference
}
});
}
}
Cell and RefCell - Interior Mutability¶
Interior mutability allows mutation through shared references:
use std::cell::{Cell, RefCell, Ref, RefMut};
// Cell<T> - for Copy types, replaces value entirely
fn cell_example() {
let cell = Cell::new(5);
// No borrowing - just get and set
let value = cell.get();
cell.set(value + 1);
println!("Value: {}", cell.get());
// swap and replace
let old = cell.replace(10);
println!("Old: {}, New: {}", old, cell.get());
// Multiple references can exist
let ref1 = &cell;
let ref2 = &cell;
ref1.set(20);
ref2.set(30); // Both can mutate!
}
// RefCell<T> - for any type, runtime borrow checking
fn refcell_example() {
let data = RefCell::new(vec![1, 2, 3]);
// Borrow immutably
{
let borrowed: Ref<Vec<i32>> = data.borrow();
println!("Data: {:?}", *borrowed);
}
// Borrow mutably
{
let mut borrowed: RefMut<Vec<i32>> = data.borrow_mut();
borrowed.push(4);
}
// try_borrow and try_borrow_mut for fallible borrowing
if let Ok(borrowed) = data.try_borrow() {
println!("Successfully borrowed: {:?}", *borrowed);
}
// This would panic at runtime!
// let borrow1 = data.borrow();
// let borrow2 = data.borrow_mut(); // panic!
}
// Common pattern: Rc<RefCell<T>> for shared mutable data
fn shared_mutable_data() {
use std::rc::Rc;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
clone1.borrow_mut().push(4);
clone2.borrow_mut().push(5);
println!("Shared data: {:?}", shared.borrow());
}
// OnceCell - write once, read many
use std::cell::OnceCell;
fn once_cell_example() {
let cell: OnceCell<String> = OnceCell::new();
assert!(cell.get().is_none());
let value = cell.get_or_init(|| {
"computed once".to_string()
});
assert_eq!(value, "computed once");
// Second call returns cached value
let value2 = cell.get_or_init(|| {
"this won't run".to_string()
});
assert_eq!(value2, "computed once");
}
Arc - Atomic Reference Counting¶
Arc<T> is thread-safe version of Rc<T>:
use std::sync::Arc;
use std::thread;
fn basic_arc() {
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..3)
.map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("Thread {} sees: {:?}", i, data);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
// Arc with Mutex for shared mutable state
use std::sync::Mutex;
fn arc_mutex() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
// Arc with RwLock for read-heavy workloads
use std::sync::RwLock;
fn arc_rwlock() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// Multiple readers simultaneously
let readers: Vec<_> = (0..5)
.map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
let read_guard = data.read().unwrap();
println!("Reader {} sees: {:?}", i, *read_guard);
})
})
.collect();
// Single writer
{
let mut write_guard = data.write().unwrap();
write_guard.push(4);
}
for handle in readers {
handle.join().unwrap();
}
}
Cow - Clone on Write¶
Cow (Clone on Write) is an enum that can hold either borrowed or owned data:
use std::borrow::Cow;
// Function that may or may not need to modify input
fn process_text(input: &str) -> Cow<str> {
if input.contains("bad") {
// Need to modify - return owned
Cow::Owned(input.replace("bad", "good"))
} else {
// No modification needed - return borrowed
Cow::Borrowed(input)
}
}
fn cow_example() {
let text1 = "hello world";
let text2 = "this is bad text";
let result1 = process_text(text1);
let result2 = process_text(text2);
// Can check if data was cloned
match &result1 {
Cow::Borrowed(_) => println!("result1 is borrowed (no allocation)"),
Cow::Owned(_) => println!("result1 is owned (allocated)"),
}
// Use like a string slice
println!("Result 1: {}", result1);
println!("Result 2: {}", result2);
// Convert to owned if needed
let owned: String = result1.into_owned();
}
// Cow in function parameters
fn append_if_needed<'a>(s: &'a str, suffix: &str, condition: bool) -> Cow<'a, str> {
if condition {
Cow::Owned(format!("{}{}", s, suffix))
} else {
Cow::Borrowed(s)
}
}
// Cow for efficient string building
fn build_path(base: &str, segments: &[&str]) -> Cow<str> {
if segments.is_empty() {
Cow::Borrowed(base)
} else {
let mut path = base.to_string();
for segment in segments {
path.push('/');
path.push_str(segment);
}
Cow::Owned(path)
}
}
Pin - Memory Pinning¶
Pin guarantees that the pointed-to value won't be moved in memory:
use std::pin::Pin;
use std::marker::PhantomPinned;
// Self-referential struct (needs pinning)
struct SelfReferential {
value: String,
// Points to value field - becomes invalid if struct moves!
self_ptr: *const String,
_marker: PhantomPinned, // Makes type !Unpin
}
impl SelfReferential {
fn new(value: String) -> Pin<Box<Self>> {
let mut boxed = Box::new(SelfReferential {
value,
self_ptr: std::ptr::null(),
_marker: PhantomPinned,
});
let self_ptr: *const String = &boxed.value;
// Safe because we're initializing before pinning
unsafe {
let mut_ref: Pin<&mut Self> = Pin::new_unchecked(&mut *boxed);
Pin::get_unchecked_mut(mut_ref).self_ptr = self_ptr;
}
unsafe { Pin::new_unchecked(boxed) }
}
fn value(self: Pin<&Self>) -> &str {
&self.value
}
fn self_ptr_value(self: Pin<&Self>) -> &str {
unsafe { &*self.self_ptr }
}
}
// Pin is crucial for async/await
// Future trait requires Pin<&mut Self> for poll()
use std::future::Future;
use std::task::{Context, Poll};
struct MyFuture {
// async state machine fields
}
impl Future for MyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(42)
}
}
// Most types are Unpin (can be safely moved after pinning)
fn unpin_types() {
let mut x = 5;
let pinned = Pin::new(&mut x);
// Unpin types can be unpinned
let unpinned: &mut i32 = Pin::into_inner(pinned);
*unpinned = 10;
}
5. Concurrency¶
Rust's ownership system prevents data races at compile time, enabling what's called "fearless concurrency." The compiler enforces that either multiple threads can read data, OR one thread can mutate it—never both simultaneously. This eliminates entire classes of bugs that plague other languages.
Concurrency Primitives:
| Primitive | Purpose | Key Characteristics |
|---|---|---|
thread::spawn |
Create OS threads | Heavy-weight, 1:1 mapping |
thread::scope |
Scoped threads | Can borrow from parent |
mpsc::channel |
Multi-producer, single-consumer | Message passing |
Mutex<T> |
Mutual exclusion | One accessor at a time |
RwLock<T> |
Read-write lock | Many readers OR one writer |
Condvar |
Condition variable | Wait for conditions |
Barrier |
Synchronization point | Wait for all threads |
atomic types |
Lock-free operations | AtomicBool, AtomicUsize, etc. |
Threads and Thread Spawning¶
use std::thread;
use std::time::Duration;
fn basic_threads() {
// Spawn a thread - returns JoinHandle
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Spawned thread: count {}", i);
thread::sleep(Duration::from_millis(100));
}
42 // Return value
});
// Main thread continues
for i in 1..=3 {
println!("Main thread: count {}", i);
thread::sleep(Duration::from_millis(150));
}
// Wait for thread and get result
let result = handle.join().unwrap();
println!("Thread returned: {}", result);
}
// Moving data into threads
fn thread_with_move() {
let data = vec![1, 2, 3];
// move keyword transfers ownership
let handle = thread::spawn(move || {
println!("Data in thread: {:?}", data);
data.iter().sum::<i32>()
});
// data is no longer accessible here
let sum = handle.join().unwrap();
println!("Sum: {}", sum);
}
// Named threads and thread builder
fn thread_builder() {
let builder = thread::Builder::new()
.name("worker".into())
.stack_size(4 * 1024 * 1024); // 4MB stack
let handle = builder.spawn(|| {
println!("Thread name: {:?}", thread::current().name());
}).unwrap();
handle.join().unwrap();
}
// Get current thread info
fn thread_info() {
println!("Current thread: {:?}", thread::current().name());
println!("Thread ID: {:?}", thread::current().id());
// Yield to other threads
thread::yield_now();
// Sleep
thread::sleep(Duration::from_millis(100));
// Get number of CPUs
let cpus = thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
println!("Available parallelism: {}", cpus);
}
Scoped Threads¶
Scoped threads can borrow from the parent stack without requiring 'static:
use std::thread;
fn scoped_threads() {
let data = vec![1, 2, 3, 4, 5];
let mut results = vec![0; 5];
thread::scope(|s| {
// These threads can borrow data and results
for (i, value) in data.iter().enumerate() {
let result_slot = &mut results[i];
s.spawn(move || {
// Can borrow from parent scope!
*result_slot = value * 2;
});
}
}); // All threads joined here
println!("Results: {:?}", results);
}
// Parallel map with scoped threads
fn parallel_map<T, R, F>(items: &[T], f: F) -> Vec<R>
where
T: Sync,
R: Send + Default + Clone,
F: Fn(&T) -> R + Sync,
{
let mut results = vec![R::default(); items.len()];
thread::scope(|s| {
for (item, result) in items.iter().zip(results.iter_mut()) {
s.spawn(|| {
*result = f(item);
});
}
});
results
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let squared = parallel_map(&numbers, |x| x * x);
println!("Squared: {:?}", squared);
}
Message Passing with Channels¶
Channels provide safe communication between threads:
use std::sync::mpsc::{self, Sender, Receiver};
use std::thread;
use std::time::Duration;
// Basic channel usage
fn basic_channel() {
let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
thread::spawn(move || {
let messages = vec!["hello", "from", "the", "thread"];
for msg in messages {
tx.send(msg.to_string()).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
// Receive messages
for received in rx {
println!("Got: {}", received);
}
}
// Multiple producers
fn multiple_producers() {
let (tx, rx) = mpsc::channel();
for i in 0..3 {
let tx_clone = tx.clone();
thread::spawn(move || {
for j in 0..3 {
tx_clone.send(format!("Thread {} message {}", i, j)).unwrap();
thread::sleep(Duration::from_millis(50));
}
});
}
drop(tx); // Drop original sender so rx knows when to stop
for msg in rx {
println!("{}", msg);
}
}
// Non-blocking receive
fn non_blocking() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
tx.send(42).unwrap();
});
loop {
match rx.try_recv() {
Ok(value) => {
println!("Received: {}", value);
break;
}
Err(mpsc::TryRecvError::Empty) => {
println!("No message yet, doing other work...");
thread::sleep(Duration::from_millis(20));
}
Err(mpsc::TryRecvError::Disconnected) => {
println!("Channel closed");
break;
}
}
}
}
// Receive with timeout
fn with_timeout() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
thread::sleep(Duration::from_millis(500));
let _ = tx.send(42);
});
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(value) => println!("Received: {}", value),
Err(mpsc::RecvTimeoutError::Timeout) => println!("Timed out"),
Err(mpsc::RecvTimeoutError::Disconnected) => println!("Channel closed"),
}
}
// Bounded channel (sync_channel)
fn bounded_channel() {
// Buffer size of 2
let (tx, rx) = mpsc::sync_channel(2);
thread::spawn(move || {
for i in 0..5 {
println!("Sending {}", i);
tx.send(i).unwrap(); // Blocks when buffer full
println!("Sent {}", i);
}
});
thread::sleep(Duration::from_millis(500));
for received in rx {
println!("Received: {}", received);
}
}
Mutex and Shared State¶
use std::sync::{Arc, Mutex, MutexGuard};
use std::thread;
fn basic_mutex() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() returns MutexGuard, unlocks on drop
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
// Handling poisoned mutex
fn handle_poisoning() {
let mutex = Arc::new(Mutex::new(0));
let mutex_clone = Arc::clone(&mutex);
let handle = thread::spawn(move || {
let _guard = mutex_clone.lock().unwrap();
panic!("Thread panicked while holding lock!");
});
let _ = handle.join(); // Thread panicked
// Mutex is now poisoned
match mutex.lock() {
Ok(guard) => println!("Got lock: {}", *guard),
Err(poisoned) => {
// Can still access data, but know it might be inconsistent
let guard = poisoned.into_inner();
println!("Recovered from poisoned mutex: {}", *guard);
}
}
}
// try_lock for non-blocking
fn try_lock_example() {
let mutex = Arc::new(Mutex::new(0));
let mutex_clone = Arc::clone(&mutex);
// Hold the lock
let _guard = mutex.lock().unwrap();
let handle = thread::spawn(move || {
match mutex_clone.try_lock() {
Ok(guard) => println!("Got lock: {}", *guard),
Err(_) => println!("Lock is held by another thread"),
}
});
handle.join().unwrap();
}
RwLock for Read-Heavy Workloads¶
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn rwlock_example() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
// Multiple readers can access simultaneously
let readers: Vec<_> = (0..5)
.map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
for _ in 0..3 {
let guard = data.read().unwrap();
println!("Reader {} sees: {:?}", i, *guard);
thread::sleep(Duration::from_millis(10));
}
})
})
.collect();
// Writer gets exclusive access
let data_clone = Arc::clone(&data);
let writer = thread::spawn(move || {
for i in 4..7 {
let mut guard = data_clone.write().unwrap();
guard.push(i);
println!("Writer added {}", i);
thread::sleep(Duration::from_millis(50));
}
});
for reader in readers {
reader.join().unwrap();
}
writer.join().unwrap();
println!("Final data: {:?}", *data.read().unwrap());
}
Condition Variables¶
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
use std::time::Duration;
fn condvar_example() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair_clone = Arc::clone(&pair);
// Spawned thread waits for signal
let handle = thread::spawn(move || {
let (lock, cvar) = &*pair_clone;
let mut started = lock.lock().unwrap();
while !*started {
// Wait releases lock and sleeps until notified
started = cvar.wait(started).unwrap();
}
println!("Worker: received signal, starting work");
});
// Main thread signals
thread::sleep(Duration::from_millis(100));
println!("Main: sending signal");
let (lock, cvar) = &*pair;
{
let mut started = lock.lock().unwrap();
*started = true;
}
cvar.notify_one();
handle.join().unwrap();
}
// Producer-consumer with condvar
fn producer_consumer() {
let queue = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
let producer_queue = Arc::clone(&queue);
let producer = thread::spawn(move || {
for i in 0..5 {
let (lock, cvar) = &*producer_queue;
let mut queue = lock.lock().unwrap();
queue.push(i);
println!("Produced: {}", i);
cvar.notify_one();
drop(queue);
thread::sleep(Duration::from_millis(100));
}
});
let consumer_queue = Arc::clone(&queue);
let consumer = thread::spawn(move || {
for _ in 0..5 {
let (lock, cvar) = &*consumer_queue;
let mut queue = lock.lock().unwrap();
while queue.is_empty() {
queue = cvar.wait(queue).unwrap();
}
let item = queue.remove(0);
println!("Consumed: {}", item);
}
});
producer.join().unwrap();
consumer.join().unwrap();
}
Barrier for Synchronization¶
use std::sync::{Arc, Barrier};
use std::thread;
fn barrier_example() {
let num_threads = 5;
let barrier = Arc::new(Barrier::new(num_threads));
let handles: Vec<_> = (0..num_threads)
.map(|i| {
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
println!("Thread {} doing setup work", i);
thread::sleep(std::time::Duration::from_millis(i as u64 * 100));
// Wait for all threads
println!("Thread {} waiting at barrier", i);
barrier.wait();
// All threads continue together
println!("Thread {} passed barrier", i);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
Atomic Operations¶
Lock-free operations for simple shared state:
use std::sync::atomic::{AtomicBool, AtomicUsize, AtomicI64, Ordering};
use std::sync::Arc;
use std::thread;
fn atomic_example() {
let counter = Arc::new(AtomicUsize::new(0));
let running = Arc::new(AtomicBool::new(true));
let handles: Vec<_> = (0..4)
.map(|_| {
let counter = Arc::clone(&counter);
let running = Arc::clone(&running);
thread::spawn(move || {
while running.load(Ordering::Relaxed) {
// Atomic increment
counter.fetch_add(1, Ordering::SeqCst);
thread::yield_now();
}
})
})
.collect();
thread::sleep(std::time::Duration::from_millis(10));
running.store(false, Ordering::Relaxed);
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", counter.load(Ordering::SeqCst));
}
// Compare-and-swap for lock-free data structures
fn compare_and_swap() {
let value = AtomicI64::new(5);
// Only update if current value matches expected
let result = value.compare_exchange(
5, // expected
10, // new value
Ordering::SeqCst, // success ordering
Ordering::SeqCst, // failure ordering
);
match result {
Ok(old) => println!("Updated from {} to 10", old),
Err(current) => println!("Failed, current value is {}", current),
}
// fetch_update for conditional atomic updates
let result = value.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
if x > 0 { Some(x - 1) } else { None }
});
}
// Memory orderings explained:
// - Relaxed: No synchronization, just atomicity
// - Acquire: Reads can't move before this load
// - Release: Writes can't move after this store
// - AcqRel: Both acquire and release
// - SeqCst: Strongest guarantee, total ordering
Send and Sync Traits¶
These marker traits determine thread safety:
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// Send: Type can be transferred to another thread
// Sync: Type can be shared between threads (&T is Send)
// Most primitive types are both Send and Sync
fn send_sync_primitives() {
let x: i32 = 5; // Send + Sync
let s: String = "hello".to_string(); // Send + Sync
let v: Vec<i32> = vec![1, 2, 3]; // Send + Sync
}
// Rc is neither Send nor Sync
fn rc_not_thread_safe() {
let rc = Rc::new(5);
// This won't compile:
// thread::spawn(move || {
// println!("{}", rc);
// });
}
// Arc is both Send and Sync
fn arc_is_thread_safe() {
let arc = Arc::new(5);
let arc_clone = Arc::clone(&arc);
std::thread::spawn(move || {
println!("{}", arc_clone);
}).join().unwrap();
}
// RefCell is Send but not Sync
fn refcell_is_send() {
let refcell = RefCell::new(5);
// Can move to another thread
std::thread::spawn(move || {
*refcell.borrow_mut() += 1;
println!("{}", refcell.borrow());
}).join().unwrap();
}
// Mutex<T> is Send + Sync if T is Send
fn mutex_thread_safety() {
use std::sync::Mutex;
// Mutex<i32> is Send + Sync
let mutex = Arc::new(Mutex::new(5));
// Can be shared across threads
let mutex_clone = Arc::clone(&mutex);
std::thread::spawn(move || {
*mutex_clone.lock().unwrap() += 1;
}).join().unwrap();
}
// Raw pointers are neither Send nor Sync
// Use unsafe to opt-in when you know it's safe
struct SendWrapper<T>(*mut T);
// SAFETY: We ensure T is only accessed from one thread at a time
unsafe impl<T: Send> Send for SendWrapper<T> {}
6. Macros¶
Macros enable metaprogramming—writing code that generates other code at compile time. Rust macros are more structured and safer than C preprocessor macros. They operate on the Abstract Syntax Tree (AST) rather than raw text, preventing many common macro-related bugs.
Types of Macros:
| Type | Syntax | Use Case | Crate Requirements |
|---|---|---|---|
Declarative (macro_rules!) |
Pattern matching | Simple code generation | None |
| Derive macros | #[derive(MyTrait)] |
Auto-implement traits | Separate proc-macro crate |
| Attribute macros | #[my_attribute] |
Transform items | Separate proc-macro crate |
| Function-like macros | my_macro!(...) |
Custom syntax | Separate proc-macro crate |
Declarative Macros (macro_rules!)¶
Declarative macros use pattern matching on Rust syntax:
// Basic macro: create a vector
macro_rules! vec {
// Empty case
() => {
Vec::new()
};
// With elements
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
// With trailing comma
( $( $x:expr ),+ , ) => {
vec![$( $x ),*]
};
}
// Fragment types:
// $x:expr - expression
// $x:stmt - statement
// $x:ty - type
// $x:ident - identifier
// $x:path - path (like std::vec::Vec)
// $x:pat - pattern
// $x:block - block expression
// $x:item - item (fn, struct, etc.)
// $x:meta - meta item (attributes)
// $x:tt - token tree (anything)
// $x:literal - literal value
// Repetition operators:
// $(...)* - zero or more
// $(...)+ - one or more
// $(...)? - zero or one
// Implementing a hashmap! macro
macro_rules! hashmap {
() => {
std::collections::HashMap::new()
};
( $( $key:expr => $value:expr ),+ $(,)? ) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert($key, $value);
)+
map
}
};
}
fn use_hashmap() {
let map = hashmap! {
"name" => "Alice",
"city" => "Boston",
};
println!("{:?}", map);
}
// Debug/trace macro
macro_rules! dbg_verbose {
($val:expr) => {
match $val {
tmp => {
eprintln!(
"[{}:{}] {} = {:?}",
file!(),
line!(),
stringify!($val),
&tmp
);
tmp
}
}
};
}
// Macro with multiple pattern arms
macro_rules! calculate {
// Addition
(add $a:expr, $b:expr) => {
$a + $b
};
// Multiplication
(mul $a:expr, $b:expr) => {
$a * $b
};
// Power (recursive)
(pow $base:expr, $exp:expr) => {
{
let mut result = 1;
for _ in 0..$exp {
result *= $base;
}
result
}
};
}
fn use_calculate() {
let sum = calculate!(add 5, 3); // 8
let product = calculate!(mul 5, 3); // 15
let power = calculate!(pow 2, 10); // 1024
}
// Recursive macro for nested structures
macro_rules! nested_vec {
// Base case: single element
($elem:expr) => {
vec![$elem]
};
// Recursive case: nested vectors
([ $( $inner:tt ),* ]) => {
vec![ $( nested_vec!($inner) ),* ]
};
}
// TT munching pattern for complex parsing
macro_rules! count_exprs {
() => { 0 };
($head:expr) => { 1 };
($head:expr, $($tail:expr),*) => {
1 + count_exprs!($($tail),*)
};
}
fn use_count() {
let count = count_exprs!(1, 2, 3, 4, 5); // 5
}
// Macro for struct builder pattern
macro_rules! builder {
($name:ident { $( $field:ident : $type:ty ),* $(,)? }) => {
#[derive(Default)]
struct $name {
$( $field: Option<$type>, )*
}
impl $name {
fn new() -> Self {
Self::default()
}
$(
fn $field(mut self, value: $type) -> Self {
self.$field = Some(value);
self
}
)*
}
};
}
builder!(PersonBuilder {
name: String,
age: u32,
email: String,
});
fn use_builder() {
let person = PersonBuilder::new()
.name("Alice".to_string())
.age(30)
.email("alice@example.com".to_string());
}
Procedural Macros¶
Procedural macros are more powerful, operating on TokenStreams. They require a separate crate with proc-macro = true in Cargo.toml.
Derive Macros¶
// In Cargo.toml of proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
// Derive macro that implements a trait
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
// Derive macro with field inspection
#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let description = match &ast.data {
Data::Struct(data_struct) => {
let field_names: Vec<_> = match &data_struct.fields {
Fields::Named(fields) => {
fields.named.iter()
.map(|f| f.ident.as_ref().unwrap().to_string())
.collect()
}
Fields::Unnamed(fields) => {
(0..fields.unnamed.len())
.map(|i| format!("field_{}", i))
.collect()
}
Fields::Unit => vec![],
};
format!("Struct {} with fields: {:?}", name, field_names)
}
Data::Enum(data_enum) => {
let variants: Vec<_> = data_enum.variants.iter()
.map(|v| v.ident.to_string())
.collect();
format!("Enum {} with variants: {:?}", name, variants)
}
Data::Union(_) => format!("Union {}", name),
};
let gen = quote! {
impl Describe for #name {
fn describe() -> &'static str {
#description
}
}
};
gen.into()
}
// Usage in main crate:
// use my_macros::HelloMacro;
//
// trait HelloMacro {
// fn hello_macro();
// }
//
// #[derive(HelloMacro)]
// struct Pancakes;
//
// fn main() {
// Pancakes::hello_macro(); // "Hello, Macro! My name is Pancakes!"
// }
Derive Macros with Attributes¶
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Lit, Meta, NestedMeta};
// Derive with helper attributes
#[proc_macro_derive(Builder, attributes(builder))]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let builder_name = syn::Ident::new(
&format!("{}Builder", name),
name.span()
);
// Extract fields and their attributes
let fields = if let Data::Struct(data) = &ast.data {
if let Fields::Named(fields) = &data.fields {
&fields.named
} else {
panic!("Builder only works on structs with named fields");
}
} else {
panic!("Builder only works on structs");
};
let field_names: Vec<_> = fields.iter()
.map(|f| f.ident.as_ref().unwrap())
.collect();
let field_types: Vec<_> = fields.iter()
.map(|f| &f.ty)
.collect();
let gen = quote! {
#[derive(Default)]
pub struct #builder_name {
#( #field_names: Option<#field_types>, )*
}
impl #builder_name {
pub fn new() -> Self {
Self::default()
}
#(
pub fn #field_names(mut self, value: #field_types) -> Self {
self.#field_names = Some(value);
self
}
)*
pub fn build(self) -> Result<#name, &'static str> {
Ok(#name {
#(
#field_names: self.#field_names
.ok_or(concat!("Missing field: ", stringify!(#field_names)))?,
)*
})
}
}
impl #name {
pub fn builder() -> #builder_name {
#builder_name::new()
}
}
};
gen.into()
}
// Usage:
// #[derive(Builder)]
// struct Command {
// executable: String,
// #[builder(default)]
// args: Vec<String>,
// }
Attribute Macros¶
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, AttributeArgs};
// Attribute macro for timing functions
#[proc_macro_attribute]
pub fn timed(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_block = &input.block;
let fn_vis = &input.vis;
let fn_sig = &input.sig;
let gen = quote! {
#fn_vis #fn_sig {
let start = std::time::Instant::now();
let result = (|| #fn_block)();
let duration = start.elapsed();
println!("{} took {:?}", stringify!(#fn_name), duration);
result
}
};
gen.into()
}
// Attribute macro with arguments
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as AttributeArgs);
let input = parse_macro_input!(item as ItemFn);
// Parse route path from attributes
let path = if let Some(NestedMeta::Lit(Lit::Str(s))) = args.first() {
s.value()
} else {
"/".to_string()
};
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_sig = &input.sig;
let fn_block = &input.block;
let gen = quote! {
#fn_vis #fn_sig #fn_block
inventory::submit! {
Route {
path: #path,
handler: #fn_name,
}
}
};
gen.into()
}
// Usage:
// #[timed]
// fn slow_function() {
// std::thread::sleep(Duration::from_secs(1));
// }
//
// #[route("/api/users")]
// fn get_users() -> Vec<User> { ... }
Function-like Procedural Macros¶
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr, parse::Parse, parse::ParseStream, Token};
// SQL query macro with compile-time validation
struct SqlQuery {
query: String,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lit: LitStr = input.parse()?;
Ok(SqlQuery { query: lit.value() })
}
}
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let SqlQuery { query } = parse_macro_input!(input as SqlQuery);
// Could validate SQL at compile time here
if !query.to_uppercase().starts_with("SELECT") {
return syn::Error::new(
proc_macro2::Span::call_site(),
"Only SELECT queries are supported"
).to_compile_error().into();
}
let gen = quote! {
Query::new(#query)
};
gen.into()
}
// Custom DSL macro
struct KeyValue {
key: syn::Ident,
value: syn::Expr,
}
impl Parse for KeyValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: syn::Ident = input.parse()?;
input.parse::<Token![=>]>()?;
let value: syn::Expr = input.parse()?;
Ok(KeyValue { key, value })
}
}
struct Config {
items: Vec<KeyValue>,
}
impl Parse for Config {
fn parse(input: ParseStream) -> syn::Result<Self> {
let items = input.parse_terminated::<KeyValue, Token![,]>(KeyValue::parse)?;
Ok(Config { items: items.into_iter().collect() })
}
}
#[proc_macro]
pub fn config(input: TokenStream) -> TokenStream {
let Config { items } = parse_macro_input!(input as Config);
let keys: Vec<_> = items.iter().map(|kv| &kv.key).collect();
let values: Vec<_> = items.iter().map(|kv| &kv.value).collect();
let gen = quote! {
{
let mut config = Config::new();
#(
config.set(stringify!(#keys), #values);
)*
config
}
};
gen.into()
}
// Usage:
// let query = sql!("SELECT * FROM users WHERE id = ?");
// let cfg = config!(timeout => 30, retries => 3);
Macro Best Practices¶
// 1. Use descriptive names
macro_rules! assert_approx_eq {
($left:expr, $right:expr, $epsilon:expr) => {
let left_val = $left;
let right_val = $right;
if (left_val - right_val).abs() > $epsilon {
panic!(
"assertion failed: {} ≈ {} (difference: {}, epsilon: {})",
left_val, right_val,
(left_val - right_val).abs(),
$epsilon
);
}
};
}
// 2. Use helper macros for complex logic
macro_rules! impl_from_for_error {
($error_type:ty, $variant:ident, $source_type:ty) => {
impl From<$source_type> for $error_type {
fn from(err: $source_type) -> Self {
Self::$variant(err)
}
}
};
}
// 3. Document macros well
/// Creates a new HashMap with the given key-value pairs.
///
/// # Examples
/// ```
/// let map = hashmap! {
/// "key1" => "value1",
/// "key2" => "value2",
/// };
/// ```
macro_rules! hashmap {
// ... implementation
() => { std::collections::HashMap::new() };
}
// 4. Export macros properly
#[macro_export]
macro_rules! my_public_macro {
() => { /* ... */ };
}
// 5. Use $crate for path hygiene
#[macro_export]
macro_rules! create_struct {
($name:ident) => {
struct $name {
data: $crate::internal::Data,
}
};
}
7. Unsafe Rust¶
Unsafe Rust provides escape hatches for when the compiler's safety guarantees are too restrictive. It doesn't disable the borrow checker—it enables five specific capabilities that the compiler can't verify. The goal of unsafe is not to write dangerous code, but to write safe abstractions that require low-level operations internally.
The Five Unsafe Superpowers:
- Dereference raw pointers -
*const Tand*mut T - Call unsafe functions or methods
- Access or modify mutable static variables
- Implement unsafe traits
- Access union fields
Philosophy:
unsafedoesn't mean "dangerous"—it means "the programmer guarantees safety"- Keep unsafe blocks as small as possible
- Document safety invariants with
// SAFETY:comments - Encapsulate unsafe code in safe abstractions
- Prefer safe alternatives when they exist
Raw Pointers¶
Raw pointers (*const T and *mut T) bypass Rust's borrowing rules:
fn raw_pointer_basics() {
let mut num = 5;
// Creating raw pointers is safe
let r1: *const i32 = &num as *const i32;
let r2: *mut i32 = &mut num as *mut i32;
// Dereferencing raw pointers requires unsafe
unsafe {
println!("r1 points to: {}", *r1);
*r2 = 10;
println!("r2 now points to: {}", *r2);
}
// Raw pointers can be null
let null_ptr: *const i32 = std::ptr::null();
// Raw pointers can point to invalid memory
let arbitrary_address = 0x012345usize;
let _invalid_ptr = arbitrary_address as *const i32;
// DON'T dereference this!
// Pointer arithmetic
let arr = [1, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
for i in 0..5 {
// offset() for pointer arithmetic
println!("arr[{}] = {}", i, *ptr.add(i));
}
}
}
// Safe abstraction over raw pointers: split_at_mut
fn split_at_mut_example() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left and right are both &mut [i32], which Rust normally wouldn't allow
left[0] = 10;
right[0] = 20;
println!("{:?}, {:?}", left, right); // [10, 2, 3], [20, 5, 6]
}
// Implementing split_at_mut ourselves
fn my_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);
// SAFETY: We've verified mid <= len, so:
// - Both slices are within the original allocation
// - The slices don't overlap
// - We're returning two mutable references to non-overlapping parts
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
// Working with C strings
fn c_string_example() {
use std::ffi::{CStr, CString};
// Create C string from Rust
let rust_string = "Hello, C world!";
let c_string = CString::new(rust_string).unwrap();
let ptr: *const i8 = c_string.as_ptr();
// Convert C string back to Rust
unsafe {
let c_str = CStr::from_ptr(ptr);
let rust_str = c_str.to_str().unwrap();
println!("{}", rust_str);
}
}
Unsafe Functions and Methods¶
// Declaring an unsafe function
unsafe fn dangerous_operation(ptr: *mut i32) {
// SAFETY: Caller must ensure ptr is valid and properly aligned
*ptr += 1;
}
fn calling_unsafe_functions() {
let mut num = 5;
let ptr = &mut num as *mut i32;
// Must use unsafe block to call
unsafe {
dangerous_operation(ptr);
}
println!("num is now: {}", num);
}
// Safe wrapper around unsafe code
pub fn safe_increment(value: &mut i32) {
let ptr = value as *mut i32;
// SAFETY: ptr is derived from a valid mutable reference
unsafe {
dangerous_operation(ptr);
}
}
// Unsafe traits
unsafe trait UnsafeTrait {
fn dangerous_method(&self);
}
// SAFETY: Implementing type must uphold the trait's invariants
unsafe impl UnsafeTrait for i32 {
fn dangerous_method(&self) {
println!("Value: {}", self);
}
}
// Common unsafe trait: Send and Sync
struct MySendType {
ptr: *mut i32,
}
// SAFETY: We ensure the pointee is only accessed from one thread at a time
unsafe impl Send for MySendType {}
// SAFETY: We ensure all access is synchronized
unsafe impl Sync for MySendType {}
Mutable Static Variables¶
// Static variables have 'static lifetime
static HELLO_WORLD: &str = "Hello, world!";
// Mutable statics require unsafe to access
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
// SAFETY: We're single-threaded, so no data races
unsafe {
COUNTER += inc;
}
}
fn get_count() -> u32 {
// SAFETY: We're single-threaded, so no data races
unsafe { COUNTER }
}
// Better alternative: use atomic types
use std::sync::atomic::{AtomicU32, Ordering};
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);
fn safe_add_to_count(inc: u32) {
SAFE_COUNTER.fetch_add(inc, Ordering::SeqCst);
}
fn safe_get_count() -> u32 {
SAFE_COUNTER.load(Ordering::SeqCst)
}
// Or use OnceCell/LazyLock for complex initialization
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| {
Config::load_from_file("config.toml").unwrap()
})
}
struct Config;
impl Config {
fn load_from_file(_path: &str) -> Result<Self, ()> { Ok(Config) }
}
Unions¶
Unions are like enums but all variants share the same memory:
// Union for type punning
#[repr(C)]
union FloatBits {
f: f32,
bits: u32,
}
fn examine_float_bits() {
let value = FloatBits { f: 1.0 };
// SAFETY: Reading any union field is valid (though bits may be garbage)
// Here we know f32 and u32 have the same size
unsafe {
println!("1.0f32 as bits: 0x{:08x}", value.bits);
// Output: 0x3f800000
}
}
// Unions for FFI compatibility
#[repr(C)]
union IpAddress {
v4: [u8; 4],
v6: [u16; 8],
}
// Using ManuallyDrop in unions for non-Copy types
use std::mem::ManuallyDrop;
union MaybeString {
nothing: (),
string: ManuallyDrop<String>,
}
impl MaybeString {
fn new_string(s: String) -> Self {
MaybeString {
string: ManuallyDrop::new(s)
}
}
// SAFETY: Only call if string variant is active
unsafe fn get_string(&self) -> &String {
&self.string
}
// SAFETY: Only call once, and only if string variant is active
unsafe fn take_string(&mut self) -> String {
ManuallyDrop::take(&mut self.string)
}
}
FFI (Foreign Function Interface)¶
Calling C code from Rust and exposing Rust code to C:
// Calling C functions
extern "C" {
fn abs(input: i32) -> i32;
fn strlen(s: *const i8) -> usize;
fn memcpy(dest: *mut u8, src: *const u8, n: usize) -> *mut u8;
}
fn call_c_functions() {
unsafe {
println!("Absolute value of -3: {}", abs(-3));
let c_string = b"Hello\0";
let len = strlen(c_string.as_ptr() as *const i8);
println!("String length: {}", len);
}
}
// Exposing Rust functions to C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_multiply(a: f64, b: f64) -> f64 {
a * b
}
// Callbacks from C
type CCallback = extern "C" fn(i32) -> i32;
extern "C" {
fn register_callback(cb: CCallback);
}
extern "C" fn my_callback(value: i32) -> i32 {
println!("Callback received: {}", value);
value * 2
}
fn register_with_c() {
unsafe {
register_callback(my_callback);
}
}
// Working with C structs
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn distance(p1: *const Point, p2: *const Point) -> f64;
}
fn use_c_struct() {
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 3.0, y: 4.0 };
unsafe {
let dist = distance(&p1, &p2);
println!("Distance: {}", dist); // 5.0
}
}
// Linking with libraries
// In Cargo.toml or build.rs:
// #[link(name = "mylib")]
// #[link(name = "mylib", kind = "static")]
// #[link(name = "mylib", kind = "framework")] // macOS
extern "C" {
// Functions from linked library
}
Inline Assembly¶
use std::arch::asm;
fn inline_assembly_examples() {
// Basic inline assembly (x86_64)
let result: u64;
unsafe {
asm!(
"mov {}, 42",
out(reg) result,
);
}
println!("Result: {}", result);
// Computation with inputs and outputs
let x: u64 = 10;
let y: u64 = 20;
let sum: u64;
unsafe {
asm!(
"add {0}, {1}",
inout(reg) x => sum,
in(reg) y,
);
}
println!("Sum: {}", sum);
// CPUID example
let (eax, ebx, ecx, edx): (u32, u32, u32, u32);
unsafe {
asm!(
"cpuid",
inout("eax") 0 => eax,
out("ebx") ebx,
out("ecx") ecx,
out("edx") edx,
);
}
println!("CPUID: {} {} {} {}", eax, ebx, ecx, edx);
}
// Platform-specific intrinsics
#[cfg(target_arch = "x86_64")]
fn use_simd() {
use std::arch::x86_64::*;
unsafe {
let a = _mm256_set_ps(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0);
let b = _mm256_set_ps(8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0);
let result = _mm256_add_ps(a, b);
// All elements are 9.0
}
}
Unsafe Best Practices¶
// 1. Document safety requirements
/// Dereferences a raw pointer to read a value.
///
/// # Safety
///
/// - `ptr` must be valid for reads
/// - `ptr` must be properly aligned
/// - `ptr` must point to a properly initialized value
unsafe fn read_ptr<T: Copy>(ptr: *const T) -> T {
*ptr
}
// 2. Minimize unsafe scope
fn process_data(data: &mut [u32]) {
// Safe code here...
// SAFETY: We've verified the slice has at least one element above
let first = unsafe { data.get_unchecked(0) };
// Safe code continues...
}
// 3. Create safe abstractions
pub struct SafeWrapper {
ptr: *mut i32,
}
impl SafeWrapper {
pub fn new(value: i32) -> Self {
let ptr = Box::into_raw(Box::new(value));
SafeWrapper { ptr }
}
pub fn get(&self) -> i32 {
// SAFETY: ptr is always valid because we created it from Box
// and only free it in Drop
unsafe { *self.ptr }
}
pub fn set(&mut self, value: i32) {
// SAFETY: same as get()
unsafe { *self.ptr = value }
}
}
impl Drop for SafeWrapper {
fn drop(&mut self) {
// SAFETY: ptr was created by Box::into_raw, we're only calling
// this once (in Drop), and we never expose the raw pointer
unsafe { drop(Box::from_raw(self.ptr)) }
}
}
// 4. Use #[deny(unsafe_op_in_unsafe_fn)] for better hygiene
#![deny(unsafe_op_in_unsafe_fn)]
unsafe fn strict_unsafe_function(ptr: *const i32) -> i32 {
// Even inside unsafe fn, must use unsafe block
// SAFETY: Caller guarantees ptr is valid
unsafe { *ptr }
}
8. Async/Await¶
Rust's async/await provides efficient asynchronous programming using cooperative multitasking. Unlike OS threads, async tasks are lightweight and can number in the millions. The async model uses zero-cost abstractions—async code compiles to state machines with no hidden allocations.
Key Concepts:
| Concept | Description |
|---|---|
async fn |
Declares a function returning a Future |
.await |
Suspends execution until a Future completes |
Future |
Trait representing an asynchronous computation |
| Executor/Runtime | Polls futures to completion (Tokio, async-std) |
Pin |
Ensures self-referential futures aren't moved |
Stream |
Async version of Iterator |
When to Use Async:
- I/O-bound work: Network requests, file operations, database queries
- High concurrency: Thousands of simultaneous connections
- Event-driven systems: Web servers, message processors
When NOT to Use Async:
- CPU-bound work: Use threads or rayon instead
- Simple scripts: Async adds complexity
- Synchronous dependencies: Blocking calls negate async benefits
Understanding Futures¶
A Future is a value that might not be ready yet:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Futures are state machines
// async fn becomes something like this:
enum ReadFileFuture {
Opening,
Reading { file: std::fs::File },
Done,
}
// Manual Future implementation
struct CountdownFuture {
count: u32,
}
impl Future for CountdownFuture {
type Output = String;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.count == 0 {
Poll::Ready("Liftoff!".to_string())
} else {
self.count -= 1;
// Tell the runtime to poll us again
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
// async fn desugars to returning impl Future
async fn async_hello() -> String {
"Hello, async!".to_string()
}
// Equivalent to:
fn sync_hello() -> impl Future<Output = String> {
async { "Hello, async!".to_string() }
}
// Futures are lazy - nothing happens until polled
fn futures_are_lazy() {
let future = async_hello(); // Nothing printed yet!
// Must await or spawn to execute
}
Basic Async/Await¶
// async fn returns impl Future<Output = T>
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
// .await suspends until the request completes
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
async fn process_urls(urls: Vec<&str>) {
for url in urls {
match fetch_data(url).await {
Ok(data) => println!("Got {} bytes from {}", data.len(), url),
Err(e) => eprintln!("Error fetching {}: {}", url, e),
}
}
}
// Async blocks for inline futures
async fn async_blocks() {
let future = async {
// Async code here
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
42
};
let result = future.await;
println!("Result: {}", result);
}
// move async blocks capture by value
fn move_async_block() -> impl Future<Output = String> {
let s = String::from("hello");
async move {
// s is moved into this future
format!("{} world", s)
}
}
Tokio Runtime¶
Tokio is the most popular async runtime for Rust:
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration, timeout};
use tokio::task;
// Using #[tokio::main] macro
#[tokio::main]
async fn main() {
println!("Starting async application");
// Simple async operation
sleep(Duration::from_millis(100)).await;
// Run multiple tasks concurrently
let result = run_concurrent_tasks().await;
println!("Concurrent result: {:?}", result);
}
// Manual runtime creation
fn manual_runtime() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
println!("Running in manually created runtime");
});
// Or spawn and don't wait
rt.spawn(async {
println!("Background task");
});
}
// Spawning tasks
async fn run_concurrent_tasks() -> Vec<i32> {
let mut handles = vec![];
for i in 0..5 {
// spawn returns JoinHandle
let handle = task::spawn(async move {
sleep(Duration::from_millis(100 * i as u64)).await;
i * 2
});
handles.push(handle);
}
// Wait for all tasks
let mut results = vec![];
for handle in handles {
results.push(handle.await.unwrap());
}
results
}
// Timeouts
async fn with_timeout() {
let slow_future = async {
sleep(Duration::from_secs(10)).await;
"completed"
};
match timeout(Duration::from_secs(1), slow_future).await {
Ok(result) => println!("Got result: {}", result),
Err(_) => println!("Operation timed out"),
}
}
// Blocking operations in async context
async fn handle_blocking() {
// For CPU-bound work or blocking I/O
let result = task::spawn_blocking(|| {
// This runs on a separate thread pool
std::thread::sleep(std::time::Duration::from_secs(1));
"blocking completed"
}).await.unwrap();
println!("Blocking result: {}", result);
}
// Yielding to other tasks
async fn cooperative() {
for i in 0..1000 {
// CPU-intensive work
expensive_computation(i);
// Yield periodically to not starve other tasks
if i % 100 == 0 {
task::yield_now().await;
}
}
}
fn expensive_computation(_: i32) {}
Concurrent Operations with join! and select!¶
use tokio::{join, select, time::{sleep, Duration}};
async fn fetch_user(id: u64) -> String {
sleep(Duration::from_millis(100)).await;
format!("User {}", id)
}
async fn fetch_posts(user_id: u64) -> Vec<String> {
sleep(Duration::from_millis(150)).await;
vec![format!("Post by {}", user_id)]
}
// join! - wait for all futures concurrently
async fn parallel_fetch() {
let (user, posts) = join!(
fetch_user(1),
fetch_posts(1)
);
println!("User: {}, Posts: {:?}", user, posts);
}
// try_join! - short-circuit on first error
async fn parallel_with_errors() -> Result<(), Box<dyn std::error::Error>> {
async fn fallible_op() -> Result<String, &'static str> {
Ok("success".to_string())
}
let (a, b) = tokio::try_join!(
fallible_op(),
fallible_op()
)?;
println!("Both succeeded: {}, {}", a, b);
Ok(())
}
// select! - wait for first future to complete
async fn race_operations() {
select! {
_ = sleep(Duration::from_secs(1)) => {
println!("Timeout occurred");
}
result = fetch_user(1) => {
println!("Got user: {}", result);
}
}
}
// select! with loop for event handling
async fn event_loop() {
let mut interval = tokio::time::interval(Duration::from_secs(1));
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(10);
// Simulate sending a message
let tx_clone = tx.clone();
tokio::spawn(async move {
sleep(Duration::from_millis(500)).await;
tx_clone.send("Hello".to_string()).await.ok();
});
loop {
select! {
_ = interval.tick() => {
println!("Tick");
}
Some(msg) = rx.recv() => {
println!("Received: {}", msg);
break;
}
}
}
}
// Biased select! for priority
async fn priority_select() {
let (tx1, mut rx1) = tokio::sync::mpsc::channel::<i32>(10);
let (tx2, mut rx2) = tokio::sync::mpsc::channel::<i32>(10);
// biased; gives priority to earlier branches
select! {
biased;
Some(high_priority) = rx1.recv() => {
println!("High priority: {}", high_priority);
}
Some(low_priority) = rx2.recv() => {
println!("Low priority: {}", low_priority);
}
}
}
Async Channels¶
use tokio::sync::{mpsc, oneshot, broadcast, watch};
// mpsc: Multi-producer, single-consumer
async fn mpsc_example() {
let (tx, mut rx) = mpsc::channel::<String>(32);
// Multiple senders
for i in 0..3 {
let tx = tx.clone();
tokio::spawn(async move {
tx.send(format!("Message from {}", i)).await.unwrap();
});
}
drop(tx); // Drop original sender
// Single receiver
while let Some(msg) = rx.recv().await {
println!("Received: {}", msg);
}
}
// oneshot: Single value, single use
async fn oneshot_example() {
let (tx, rx) = oneshot::channel::<String>();
tokio::spawn(async move {
// Simulate async work
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
tx.send("Result".to_string()).unwrap();
});
match rx.await {
Ok(result) => println!("Got: {}", result),
Err(_) => println!("Sender dropped"),
}
}
// broadcast: Multiple consumers, each gets all messages
async fn broadcast_example() {
let (tx, _rx) = broadcast::channel::<String>(16);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tokio::spawn(async move {
while let Ok(msg) = rx1.recv().await {
println!("Receiver 1: {}", msg);
}
});
tokio::spawn(async move {
while let Ok(msg) = rx2.recv().await {
println!("Receiver 2: {}", msg);
}
});
tx.send("Hello everyone".to_string()).unwrap();
}
// watch: Single value that can be observed
async fn watch_example() {
let (tx, mut rx) = watch::channel("initial".to_string());
tokio::spawn(async move {
loop {
rx.changed().await.unwrap();
println!("Value changed to: {}", *rx.borrow());
}
});
tx.send("updated".to_string()).unwrap();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Streams (Async Iterators)¶
use tokio_stream::{self as stream, StreamExt};
// Basic stream usage
async fn stream_basics() {
let mut stream = stream::iter(vec![1, 2, 3, 4, 5]);
while let Some(value) = stream.next().await {
println!("Value: {}", value);
}
}
// Stream combinators
async fn stream_combinators() {
let stream = stream::iter(1..=10)
.filter(|x| futures::future::ready(x % 2 == 0))
.map(|x| x * 2)
.take(3);
tokio::pin!(stream);
while let Some(value) = stream.next().await {
println!("Value: {}", value);
}
}
// Creating streams from channels
async fn channel_as_stream() {
let (tx, rx) = tokio::sync::mpsc::channel::<i32>(10);
tokio::spawn(async move {
for i in 0..5 {
tx.send(i).await.unwrap();
}
});
// ReceiverStream adapter
use tokio_stream::wrappers::ReceiverStream;
let mut stream = ReceiverStream::new(rx);
while let Some(value) = stream.next().await {
println!("From channel: {}", value);
}
}
// Interval stream
async fn interval_stream() {
use tokio_stream::wrappers::IntervalStream;
use tokio::time::{interval, Duration};
let mut stream = IntervalStream::new(interval(Duration::from_millis(100)))
.take(5);
while let Some(_instant) = stream.next().await {
println!("Tick");
}
}
Async Traits¶
// Rust 1.75+: Native async trait methods
trait AsyncDatabase {
async fn get(&self, key: &str) -> Option<String>;
async fn set(&self, key: &str, value: &str) -> Result<(), String>;
}
struct InMemoryDb {
data: std::collections::HashMap<String, String>,
}
impl AsyncDatabase for InMemoryDb {
async fn get(&self, key: &str) -> Option<String> {
self.data.get(key).cloned()
}
async fn set(&self, _key: &str, _value: &str) -> Result<(), String> {
// Would need interior mutability for real implementation
Ok(())
}
}
// For trait objects, use #[trait_variant::make(SendDatabase: Send)]
// or the async-trait crate for backward compatibility
// Using async-trait crate (for older Rust or Send bounds)
// use async_trait::async_trait;
//
// #[async_trait]
// trait AsyncTrait {
// async fn async_method(&self) -> String;
// }
// Boxed futures for trait objects
trait DynAsyncTrait {
fn async_method(&self) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + '_>>;
}
Cancellation and Graceful Shutdown¶
use tokio::signal;
use tokio::sync::watch;
async fn graceful_shutdown() {
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
// Spawn worker tasks
let worker = tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown_rx.changed() => {
if *shutdown_rx.borrow() {
println!("Worker shutting down");
break;
}
}
_ = do_work() => {
println!("Work completed");
}
}
}
});
// Wait for shutdown signal
signal::ctrl_c().await.unwrap();
println!("Shutdown signal received");
// Notify workers
shutdown_tx.send(true).unwrap();
// Wait for workers to finish
worker.await.unwrap();
println!("Graceful shutdown complete");
}
async fn do_work() {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
// CancellationToken for structured cancellation
use tokio_util::sync::CancellationToken;
async fn with_cancellation_token() {
let token = CancellationToken::new();
let task_token = token.clone();
let task = tokio::spawn(async move {
tokio::select! {
_ = task_token.cancelled() => {
println!("Task was cancelled");
}
_ = long_running_operation() => {
println!("Task completed");
}
}
});
// Cancel after 1 second
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
token.cancel();
task.await.unwrap();
}
async fn long_running_operation() {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
9. Iterators¶
Iterators are Rust's primary abstraction for processing sequences. They are lazy (compute values on demand), composable (chain operations together), and zero-cost (compile to efficient loops). The iterator pattern is pervasive in Rust—most collection operations use iterators.
Core Traits:
| Trait | Description | Key Method |
|---|---|---|
Iterator |
Produces a sequence of values | fn next(&mut self) -> Option<Self::Item> |
IntoIterator |
Can be converted to an iterator | fn into_iter(self) -> Self::IntoIter |
FromIterator |
Can be built from an iterator | fn from_iter<I: IntoIterator>(iter: I) -> Self |
DoubleEndedIterator |
Can iterate from both ends | fn next_back(&mut self) -> Option<Self::Item> |
ExactSizeIterator |
Knows its exact length | fn len(&self) -> usize |
Iterator Types for Collections:
| Method | Yields | Ownership |
|---|---|---|
.iter() |
&T |
Borrows collection |
.iter_mut() |
&mut T |
Borrows mutably |
.into_iter() |
T |
Consumes collection |
Basic Iterator Usage¶
fn basic_iteration() {
let numbers = vec![1, 2, 3, 4, 5];
// for loop uses IntoIterator
for n in &numbers { // Same as numbers.iter()
println!("{}", n);
}
// Explicit iterator
let mut iter = numbers.iter();
while let Some(n) = iter.next() {
println!("{}", n);
}
// Different iterator types
let v = vec![String::from("a"), String::from("b")];
for s in &v { // s: &String (borrowing)
println!("{}", s);
}
for s in &mut v.clone() { // s: &mut String (mutable borrow)
s.push_str("!");
}
for s in v { // s: String (ownership transfer)
println!("{}", s);
}
// v is no longer accessible
}
// Ranges are iterators
fn range_iterators() {
// Exclusive range
for i in 0..5 { // 0, 1, 2, 3, 4
print!("{} ", i);
}
// Inclusive range
for i in 0..=5 { // 0, 1, 2, 3, 4, 5
print!("{} ", i);
}
// Reverse iteration
for i in (0..5).rev() { // 4, 3, 2, 1, 0
print!("{} ", i);
}
// Step by
for i in (0..10).step_by(2) { // 0, 2, 4, 6, 8
print!("{} ", i);
}
}
Iterator Adapters (Lazy Transformations)¶
Adapters transform iterators into new iterators without consuming them:
fn iterator_adapters() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: Transform each element
let doubled: Vec<_> = numbers.iter()
.map(|x| x * 2)
.collect();
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter: Keep elements matching predicate
let evens: Vec<_> = numbers.iter()
.filter(|x| *x % 2 == 0)
.collect();
// [2, 4, 6, 8, 10]
// filter_map: Combined filter and map
let parsed: Vec<i32> = ["1", "two", "3", "four", "5"]
.iter()
.filter_map(|s| s.parse().ok())
.collect();
// [1, 3, 5]
// flat_map: Map then flatten
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<_> = nested.iter()
.flat_map(|v| v.iter())
.collect();
// [1, 2, 3, 4, 5]
// flatten: Flatten nested iterators
let flat2: Vec<_> = nested.into_iter().flatten().collect();
// [1, 2, 3, 4, 5]
// take: Take first n elements
let first_three: Vec<_> = numbers.iter().take(3).collect();
// [1, 2, 3]
// skip: Skip first n elements
let after_three: Vec<_> = numbers.iter().skip(3).collect();
// [4, 5, 6, 7, 8, 9, 10]
// take_while: Take while predicate is true
let less_than_5: Vec<_> = numbers.iter()
.take_while(|x| **x < 5)
.collect();
// [1, 2, 3, 4]
// skip_while: Skip while predicate is true
let from_5: Vec<_> = numbers.iter()
.skip_while(|x| **x < 5)
.collect();
// [5, 6, 7, 8, 9, 10]
// chain: Concatenate iterators
let combined: Vec<_> = [1, 2].iter()
.chain([3, 4].iter())
.collect();
// [1, 2, 3, 4]
// zip: Pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter()
.zip(['a', 'b', 'c'].iter())
.collect();
// [(1, 'a'), (2, 'b'), (3, 'c')]
// enumerate: Add indices
for (index, value) in numbers.iter().enumerate() {
println!("{}: {}", index, value);
}
// peekable: Look at next without consuming
let mut iter = numbers.iter().peekable();
while let Some(&value) = iter.next() {
if let Some(&&next) = iter.peek() {
println!("{} followed by {}", value, next);
}
}
// cycle: Repeat infinitely
let repeated: Vec<_> = [1, 2, 3].iter()
.cycle()
.take(7)
.collect();
// [1, 2, 3, 1, 2, 3, 1]
// inspect: Debug without consuming
let result: Vec<_> = numbers.iter()
.inspect(|x| println!("Before filter: {}", x))
.filter(|x| *x % 2 == 0)
.inspect(|x| println!("After filter: {}", x))
.collect();
}
Consuming Adapters (Terminal Operations)¶
Consuming adapters evaluate the iterator and produce a final result:
fn consuming_adapters() {
let numbers = vec![1, 2, 3, 4, 5];
// collect: Build a collection
let vec: Vec<i32> = (0..5).collect();
let set: std::collections::HashSet<i32> = (0..5).collect();
let string: String = ['h', 'e', 'l', 'l', 'o'].iter().collect();
// sum and product
let sum: i32 = numbers.iter().sum(); // 15
let product: i32 = numbers.iter().product(); // 120
// count
let count = numbers.iter().count(); // 5
// fold: Reduce to single value with accumulator
let sum_fold = numbers.iter()
.fold(0, |acc, x| acc + x);
let sentence = ["hello", "world", "!"].iter()
.fold(String::new(), |acc, s| {
if acc.is_empty() {
s.to_string()
} else {
format!("{} {}", acc, s)
}
});
// reduce: Like fold, but uses first element as initial
let max = numbers.iter()
.copied()
.reduce(|a, b| if a > b { a } else { b });
// find: First element matching predicate
let first_even = numbers.iter().find(|x| *x % 2 == 0); // Some(&2)
// find_map: Combined find and map
let parsed = ["1", "two", "3"].iter()
.find_map(|s| s.parse::<i32>().ok()); // Some(1)
// position: Index of first match
let pos = numbers.iter().position(|x| *x == 3); // Some(2)
// any and all
let has_even = numbers.iter().any(|x| x % 2 == 0); // true
let all_positive = numbers.iter().all(|x| *x > 0); // true
// min and max
let min = numbers.iter().min(); // Some(&1)
let max = numbers.iter().max(); // Some(&5)
// min_by and max_by with custom comparison
let people = vec![("Alice", 30), ("Bob", 25), ("Charlie", 35)];
let oldest = people.iter()
.max_by(|a, b| a.1.cmp(&b.1)); // Some(("Charlie", 35))
// min_by_key and max_by_key
let youngest = people.iter()
.min_by_key(|(_, age)| age); // Some(("Bob", 25))
// partition: Split into two collections
let (evens, odds): (Vec<_>, Vec<_>) = numbers.iter()
.partition(|x| *x % 2 == 0);
// unzip: Split pairs into two collections
let pairs = vec![(1, 'a'), (2, 'b'), (3, 'c')];
let (nums, chars): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
// for_each: Execute side effect
numbers.iter().for_each(|x| println!("{}", x));
// nth: Get element at index
let third = numbers.iter().nth(2); // Some(&3)
// last: Get last element
let last = numbers.iter().last(); // Some(&5)
}
Advanced Iterator Patterns¶
use std::iter::{self, FromIterator};
fn advanced_patterns() {
// Collect into Result: stops on first error
let strings = vec!["1", "2", "three", "4"];
let parsed: Result<Vec<i32>, _> = strings.iter()
.map(|s| s.parse::<i32>())
.collect();
// Err(ParseIntError { ... }) - stops at "three"
// Collect successes only, ignoring errors
let parsed_ok: Vec<i32> = strings.iter()
.filter_map(|s| s.parse().ok())
.collect();
// [1, 2, 4]
// scan: Like fold but yields intermediate values
let running_sum: Vec<i32> = [1, 2, 3, 4, 5].iter()
.scan(0, |state, x| {
*state += x;
Some(*state)
})
.collect();
// [1, 3, 6, 10, 15]
// Creating iterators from functions
let mut count = 0;
let counter = iter::from_fn(move || {
count += 1;
if count <= 5 { Some(count) } else { None }
});
// successors: Generate sequence from initial value
let powers_of_2: Vec<_> = iter::successors(Some(1u32), |n| n.checked_mul(2))
.take(10)
.collect();
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
// repeat and repeat_with
let fives: Vec<_> = iter::repeat(5).take(3).collect(); // [5, 5, 5]
let randoms: Vec<_> = iter::repeat_with(|| rand::random::<u8>())
.take(5)
.collect();
// once: Iterator with single element
let combined: Vec<_> = iter::once(0)
.chain(1..5)
.chain(iter::once(100))
.collect();
// [0, 1, 2, 3, 4, 100]
// empty: Iterator with no elements
let nothing: Vec<i32> = iter::empty().collect();
// windows: Overlapping slices
let data = [1, 2, 3, 4, 5];
for window in data.windows(3) {
println!("{:?}", window); // [1,2,3], [2,3,4], [3,4,5]
}
// chunks: Non-overlapping slices
for chunk in data.chunks(2) {
println!("{:?}", chunk); // [1,2], [3,4], [5]
}
// array_chunks (nightly) / chunks_exact
for chunk in data.chunks_exact(2) {
println!("{:?}", chunk); // [1,2], [3,4] (skips incomplete)
}
}
// Parallel iteration with rayon
fn parallel_iteration() {
use rayon::prelude::*;
let numbers: Vec<i32> = (0..1000).collect();
// Parallel map
let squared: Vec<_> = numbers.par_iter()
.map(|x| x * x)
.collect();
// Parallel filter
let evens: Vec<_> = numbers.par_iter()
.filter(|x| *x % 2 == 0)
.collect();
// Parallel sum
let sum: i32 = numbers.par_iter().sum();
}
Implementing Iterator¶
// Basic iterator implementation
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
// Optional: Provide size hint for optimization
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.max - self.count) as usize;
(remaining, Some(remaining))
}
}
// Implement ExactSizeIterator when size is known
impl ExactSizeIterator for Counter {
fn len(&self) -> usize {
(self.max - self.count) as usize
}
}
// DoubleEndedIterator for reverse iteration
struct Range {
start: i32,
end: i32,
}
impl Iterator for Range {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.start < self.end {
let result = self.start;
self.start += 1;
Some(result)
} else {
None
}
}
}
impl DoubleEndedIterator for Range {
fn next_back(&mut self) -> Option<Self::Item> {
if self.start < self.end {
self.end -= 1;
Some(self.end)
} else {
None
}
}
}
// IntoIterator for custom types
struct MyCollection {
items: Vec<String>,
}
impl IntoIterator for MyCollection {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
// Reference iteration
impl<'a> IntoIterator for &'a MyCollection {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.items.iter()
}
}
// FromIterator for collecting into custom types
impl FromIterator<String> for MyCollection {
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
MyCollection {
items: iter.into_iter().collect(),
}
}
}
fn use_custom_iterators() {
// Counter usage
let counter = Counter::new(5);
let sum: u32 = counter.sum(); // 1 + 2 + 3 + 4 + 5 = 15
// Can use all iterator methods
let evens: Vec<_> = Counter::new(10)
.filter(|x| x % 2 == 0)
.collect();
// [2, 4, 6, 8, 10]
// Double-ended range
let range = Range { start: 1, end: 6 };
let reversed: Vec<_> = range.rev().collect();
// [5, 4, 3, 2, 1]
// Collect into custom collection
let collection: MyCollection = vec!["a", "b", "c"]
.into_iter()
.map(String::from)
.collect();
}
10. Closures¶
Closures are anonymous functions that can capture variables from their enclosing scope. They are one of Rust's most powerful features, enabling functional programming patterns, callbacks, and lazy evaluation. Unlike regular functions, closures can "close over" their environment.
Closure Traits:
| Trait | Captures By | Can Call | Use Case |
|---|---|---|---|
Fn |
&T (shared reference) |
Multiple times | Read-only access |
FnMut |
&mut T (mutable reference) |
Multiple times | Mutable access |
FnOnce |
T (by value) |
Once | Consumes captured values |
Trait Hierarchy: Fn: FnMut: FnOnce (Fn is a subset of FnMut, which is a subset of FnOnce)
Basic Closures¶
fn basic_closures() {
// Type inference for parameters and return type
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
// Multiple parameters
let add = |x, y| x + y;
println!("{}", add(2, 3)); // 5
// Explicit type annotations
let multiply: fn(i32, i32) -> i32 = |x, y| x * y;
let divide = |x: f64, y: f64| -> f64 { x / y };
// Multi-line closure with block
let complex = |x: i32| {
let squared = x * x;
let cubed = squared * x;
squared + cubed
};
// No parameters
let greet = || println!("Hello!");
greet();
// Closure that returns nothing
let log = |msg: &str| {
println!("[LOG] {}", msg);
};
}
Environment Capture¶
Closures can capture variables from their enclosing scope:
fn capture_examples() {
// Capture by reference (Fn)
let x = 10;
let print_x = || println!("x = {}", x);
print_x();
print_x(); // Can call multiple times
println!("x is still accessible: {}", x);
// Capture by mutable reference (FnMut)
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
println!("final counter: {}", counter); // 2
// Capture by value with move (FnOnce, but could be Fn if no mutation)
let data = vec![1, 2, 3];
let consume = move || {
println!("Taking ownership: {:?}", data);
data // Returns owned data
};
let _owned = consume();
// consume(); // Error: already moved
// data; // Error: already moved into closure
// move with Copy types
let x = 42; // i32 implements Copy
let closure = move || x * 2;
println!("{}", closure()); // Works
println!("{}", x); // x is still accessible (copied, not moved)
}
// Why capture matters
fn capture_semantics() {
let mut s = String::from("hello");
// This closure captures s by mutable reference
let mut append = || s.push_str(" world");
// Can't use s while closure exists and might mutate it
// println!("{}", s); // Error: s is borrowed mutably
append();
// Now we can use s again
println!("{}", s); // "hello world"
}
Closure Trait Inference¶
The compiler infers which trait a closure implements based on how it uses captured variables:
fn trait_inference() {
// Fn - only reads captured value
let x = 5;
let fn_closure = || println!("{}", x);
call_fn(&fn_closure);
call_fn(&fn_closure); // Fn can be called multiple times
// FnMut - modifies captured value
let mut y = 5;
let mut fn_mut_closure = || y += 1;
call_fn_mut(&mut fn_mut_closure);
call_fn_mut(&mut fn_mut_closure);
// FnOnce - consumes captured value
let z = String::from("hello");
let fn_once_closure = || {
drop(z); // Consumes z
};
call_fn_once(fn_once_closure);
// call_fn_once(fn_once_closure); // Error: already consumed
}
fn call_fn<F: Fn()>(f: &F) {
f();
}
fn call_fn_mut<F: FnMut()>(f: &mut F) {
f();
}
fn call_fn_once<F: FnOnce()>(f: F) {
f();
}
// Accepting any callable
fn flexible_callback<F>(f: F)
where
F: FnOnce(), // FnOnce accepts Fn, FnMut, and FnOnce
{
f();
}
Closures as Parameters¶
// Generic closure parameter
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
// Multiple calls require Fn or FnMut
fn apply_twice<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(x))
}
// Mutable closure parameter
fn call_with_counter<F>(mut f: F) -> i32
where
F: FnMut() -> i32,
{
f() + f()
}
// Using impl Trait (simpler syntax)
fn apply_simple(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn use_closure_params() {
let result = apply(|x| x * 2, 5); // 10
let result = apply_twice(|x| x + 1, 5); // 7
let mut count = 0;
let result = call_with_counter(|| {
count += 1;
count
});
println!("Result: {}, count: {}", result, count); // 3, 2
}
// Choosing the right trait bound
// Use FnOnce when you only call once
// Use FnMut when you need to call multiple times with mutation
// Use Fn when you need to call multiple times without mutation
// Prefer FnOnce for maximum flexibility (accepts all closure types)
Returning Closures¶
Closures have anonymous types, so returning them requires special handling:
// Return closure as trait object (dynamic dispatch)
fn returns_closure_boxed() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
// Return closure as impl Trait (static dispatch, more efficient)
fn returns_closure_impl() -> impl Fn(i32) -> i32 {
|x| x + 1
}
// Closure that captures environment
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// Closure factory with mutable state
fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
// Multiple return paths require Box (or enum)
fn conditional_closure(use_add: bool) -> Box<dyn Fn(i32) -> i32> {
if use_add {
Box::new(|x| x + 1)
} else {
Box::new(|x| x * 2)
}
}
fn use_returned_closures() {
let add_one = returns_closure_impl();
println!("{}", add_one(5)); // 6
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
let mut counter = make_counter();
println!("{}", counter()); // 1
println!("{}", counter()); // 2
println!("{}", counter()); // 3
}
Closure and Function Pointer Coercion¶
// Function pointer type (no environment capture)
fn regular_function(x: i32) -> i32 {
x + 1
}
fn use_function_pointers() {
// Function pointer type
let fn_ptr: fn(i32) -> i32 = regular_function;
// Non-capturing closures can coerce to function pointers
let closure_ptr: fn(i32) -> i32 = |x| x + 1;
// Capturing closures CANNOT be function pointers
let y = 5;
// let invalid: fn(i32) -> i32 = |x| x + y; // Error!
// Use in FFI or when exact fn signature required
extern "C" fn c_callback(x: i32) -> i32 { x }
// Arrays of function pointers
let operations: [fn(i32) -> i32; 3] = [
|x| x + 1,
|x| x * 2,
|x| x - 1,
];
for op in operations {
println!("{}", op(5));
}
}
Advanced Closure Patterns¶
use std::collections::HashMap;
// Memoization cache
struct Memoizer<T, R>
where
T: Fn(u64) -> R,
R: Clone,
{
calculation: T,
cache: HashMap<u64, R>,
}
impl<T, R> Memoizer<T, R>
where
T: Fn(u64) -> R,
R: Clone,
{
fn new(calculation: T) -> Self {
Memoizer {
calculation,
cache: HashMap::new(),
}
}
fn value(&mut self, arg: u64) -> R {
self.cache.get(&arg).cloned().unwrap_or_else(|| {
let result = (self.calculation)(arg);
self.cache.insert(arg, result.clone());
result
})
}
}
// Lazy initialization
struct Lazy<T, F: FnOnce() -> T> {
init: Option<F>,
value: Option<T>,
}
impl<T, F: FnOnce() -> T> Lazy<T, F> {
fn new(init: F) -> Self {
Lazy { init: Some(init), value: None }
}
fn get(&mut self) -> &T {
if self.value.is_none() {
let init = self.init.take().unwrap();
self.value = Some(init());
}
self.value.as_ref().unwrap()
}
}
// Builder pattern with closure configuration
struct ServerBuilder {
port: u16,
handlers: Vec<Box<dyn Fn(&str) -> String>>,
}
impl ServerBuilder {
fn new() -> Self {
ServerBuilder {
port: 8080,
handlers: vec![],
}
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn handler<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> String + 'static,
{
self.handlers.push(Box::new(f));
self
}
}
// Callback patterns
type Callback<T> = Box<dyn Fn(T) + Send + Sync>;
struct EventEmitter<T> {
listeners: Vec<Callback<T>>,
}
impl<T: Clone> EventEmitter<T> {
fn new() -> Self {
EventEmitter { listeners: vec![] }
}
fn on<F>(&mut self, callback: F)
where
F: Fn(T) + Send + Sync + 'static,
{
self.listeners.push(Box::new(callback));
}
fn emit(&self, value: T) {
for listener in &self.listeners {
listener(value.clone());
}
}
}
fn advanced_patterns_demo() {
// Memoized fibonacci
let mut fib_memo = Memoizer::new(|n| {
if n <= 1 { n } else {
// Note: This naive recursion doesn't use memoization internally
// Real implementation would need interior mutability
n // Placeholder
}
});
// Lazy config loading
let mut config = Lazy::new(|| {
println!("Loading config...");
std::collections::HashMap::from([
("key1", "value1"),
("key2", "value2"),
])
});
// Config not loaded yet
println!("Getting config...");
let _cfg = config.get(); // Now loads
let _cfg = config.get(); // Uses cached value
}
Closures in Common Patterns¶
// Option/Result combinators
fn option_combinators() {
let value: Option<i32> = Some(5);
let doubled = value.map(|x| x * 2);
let filtered = value.filter(|x| *x > 3);
let or_else = value.or_else(|| Some(0));
let unwrap_or = value.unwrap_or_else(|| {
println!("Computing default...");
0
});
}
// Iterator closures
fn iterator_closures() {
let numbers = vec![1, 2, 3, 4, 5];
// Each of these takes a closure
let _doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
let _evens: Vec<_> = numbers.iter().filter(|x| *x % 2 == 0).collect();
let _sum: i32 = numbers.iter().fold(0, |acc, x| acc + x);
numbers.iter().for_each(|x| println!("{}", x));
}
// Thread spawning
fn thread_closure() {
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
// Closure moves data into thread
println!("Data in thread: {:?}", data);
});
handle.join().unwrap();
}
// Sorting with closures
fn sorting_closures() {
let mut numbers = vec![3, 1, 4, 1, 5, 9];
// Sort with custom comparator
numbers.sort_by(|a, b| b.cmp(a)); // Descending
// Sort by key
let mut people = vec![("Alice", 30), ("Bob", 25), ("Charlie", 35)];
people.sort_by_key(|(_, age)| *age);
}
Best Practices¶
// 1. Prefer closures for short, inline functions
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
// 2. Use move for thread safety and 'static lifetimes
let handle = std::thread::spawn(move || { /* ... */ });
// 3. Choose appropriate trait bounds
// FnOnce: Maximum flexibility, single call
// FnMut: Multiple calls with mutation
// Fn: Multiple calls, no mutation
// 4. Use impl Trait for simple return types
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// 5. Box closures for heterogeneous collections or dynamic dispatch
let callbacks: Vec<Box<dyn Fn()>> = vec![
Box::new(|| println!("First")),
Box::new(|| println!("Second")),
];
// 6. Avoid capturing more than needed
let expensive_data = vec![1; 1000000];
let len = expensive_data.len(); // Copy the length
let closure = move || println!("Length: {}", len); // Only captures len
// expensive_data still accessible
Summary¶
Rust's advanced features work together to provide a powerful, safe, and performant systems programming language:
- Traits enable polymorphism, abstraction, and code reuse through shared behavior
- Generics allow writing flexible, type-safe code with zero runtime overhead
- Error Handling with
ResultandOptionmakes failures explicit and recoverable - Smart Pointers provide safe memory management beyond simple references
- Concurrency primitives enable fearless parallel programming with compile-time safety
- Macros offer powerful metaprogramming for code generation and DSLs
- Unsafe Rust provides escape hatches for low-level operations when needed
- Async/Await enables efficient asynchronous programming for I/O-bound workloads
- Iterators provide lazy, composable, and zero-cost sequence processing
- Closures capture environment and enable functional programming patterns
The ownership system, combined with these features and zero-cost abstractions, enables writing high-level code that compiles to efficient machine code while maintaining memory safety and preventing data races at compile time.