Basics
Variables & Mutability
Rust variables are immutable by default — this is a core safety feature preventing accidental mutation. Use 'mut' only when you genuinely need to change a value. 'const' requires explicit type and is inlined at compile time, while 'static' has a fixed memory address with a 'static lifetime.
let x = 5; // immutable by default
let mut y = 10; // mutable with 'mut'
y += 1; // OK, y is mutable
const MAX: u32 = 100; // compile-time constant
static GREETING: &str = "Hi"; // global static
let z: i32 = -5; // explicit type annotationShadowing
Shadowing lets you reuse a variable name while changing its type or value. Unlike 'mut', shadowing creates a new binding — useful for transforming data (e.g., parsing a string to int) without inventing a new name. The old value is dropped after the new binding.
let x = 5;
let x = x * 2; // shadow previous x, now 10
let x = "text"; // can even change type!
println!("{}", x); // "text"
// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changesComments & Printing
Use /// for item documentation (shown by 'cargo doc') and //! for module/crate docs. println! writes to stdout, eprintln! to stderr. The {} placeholder supports formatting specs: > for right-align, < for left-align, ^ for center, .N for precision, #b/#o/#x for binary/octal/hex.
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment
let name = "Alice";
println!("Hello, {}!", name); // format string
println!("{0} {1} {0}", "a", "b"); // positional args
println!("{:>5}", 42); // right-align, width 5
println!("{:.2}", 3.14159); // 2 decimal places: 3.14
println!("{:#b}", 0b1010); // binary: 0b1010
eprintln!("Error to stderr"); // error outputData Types Overview
Rust has no implicit type conversion — use 'as' for casts. i32 is the default integer type, f64 the default float. usize is used for indexing and sizes. char is a full Unicode scalar value (not a byte), so '🦀' is a single char.
// Scalar types
let a: i32 = 42; // signed 32-bit integer
let b: u64 = 100; // unsigned 64-bit
let c: f64 = 3.14; // 64-bit float (default)
let d: bool = true;
let e: char = '🦀'; // 4-byte Unicode scalar
// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();
// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3]; // [0, 0, 0]Type Casting & Aliases
The 'as' cast is unchecked and may lose data (e.g., 300u16 as u8 wraps to 44). For safe conversions use From/Into traits which are implemented for lossless casts. Type aliases improve readability without creating new types — use 'struct NewType(i32)' for a distinct newtype pattern.
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64; // 42.0
let z: u8 = 300u16 as u8; // truncated: 44
// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();
// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;Strings
String vs &str
The distinction between String (owned, heap) and &str (borrowed slice) is fundamental in Rust. Use String when you need to own/modify/grow the text; use &str when you only need to read it. &str can point to either a String's buffer or a string literal in the binary. Prefer &str in function parameters for flexibility.
// &str: immutable string slice (borrowed)
let s1: &str = "literal"; // stored in binary
let s2: &str = &s3[..2]; // slice of a String
// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world"); // append
s3.push('!'); // append char
s3.insert(0, '>'); // insert at index
println!("{}", s3); // >hello, world!
// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");String Methods
String methods return new owned Strings rather than modifying in place (except push_*/insert methods on mutable Strings). split() returns an iterator, so use .collect() to materialize. Note that indexing s[0] is NOT allowed on strings because UTF-8 boundaries don't align with byte indices — use s.chars().nth(0) instead.
let s = String::from("Hello, World");
// inspection
println!("len: {}", s.len()); // 12
println!("is_empty: {}", s.is_empty()); // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true
// transformation
let upper = s.to_uppercase(); // HELLO, WORLD
let replaced = s.replace("o", "0"); // Hell0, W0rld
let trimmed = " hi ".trim().to_string(); // hi
// splitting
for word in s.split(", ") {
println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();Formatting & Concatenation
The + operator takes ownership of the left String and borrows the right (&str). This is why s1 becomes invalid after s1 + &s2. For chaining multiple strings, prefer format! which is more readable and doesn't move any operands. concat! works only on literals and produces a &'static str.
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);
// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// s1 is now invalid!
let s4 = format!("{}{}", s2, s3); // neither moved
// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile timeIterating Characters & Bytes
Rust strings are UTF-8 encoded, so byte index != char index. chars() iterates Unicode scalar values (O(n) to decode), bytes() iterates raw bytes. Slicing with [n..m] panics if n or m falls in the middle of a multi-byte character. For byte-level access, convert to Vec<u8> via as_bytes().
let s = "héllo"; // é is 2 bytes in UTF-8
// iterate over chars (Unicode scalar values)
for c in s.chars() {
print!("{} ", c); // h é l l o
}
// iterate over bytes
for b in s.bytes() {
print!("{} ", b); // 104 195 169 108 108 111
}
// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')
// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!Parsing & Conversion
parse() returns Result because the string might not be a valid number — always handle the error. The turbofish syntax parse::<T>() lets you specify the type inline. Converting String to &str is free (just a borrow), but &str to String allocates. collect() can build a String from an iterator of chars.
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();
// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);
// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;
// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();Data Structures
Arrays & Slices
Arrays [T; N] have a fixed size known at compile time and live on the stack. Slices &[T] are fat pointers (pointer + length) that borrow a contiguous sequence — they allow functions to accept any array or vector without caring about size. Use slices in function signatures for generality.
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());
// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3]; // [2, 3]
let full: &[i32] = &arr; // whole array
let first = &arr[..1]; // [1]
// iterating
for n in &arr {
println!("{}", n);
}Vectors (Vec<T>)
Vec<T> is Rust's growable array, backed by heap memory with capacity doubling. push/pop are O(1) amortized; insert/remove are O(n) because elements shift. Use .get(i) instead of v[i] when you want safe access (returns Option). into_iter() consumes the vector, yielding owned values.
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3]; // macro shorthand
v.push(4); // append
v.pop(); // remove last -> Option
v.insert(0, 0); // insert at index (O(n))
v.remove(0); // remove at index (O(n))
v.extend([5, 6]); // append multiple
// access
println!("{}", v[0]); // panics if out of bounds
println!("{:?}", v.get(0)); // Some(&4) — safe
// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; } // double each
let owned: Vec<i32> = v.into_iter().collect();HashMap & BTreeMap
HashMap uses hashing for O(1) average access but has no ordering. BTreeMap uses a B-tree for O(log n) access but keeps keys sorted. Use HashMap when you need fast lookups; use BTreeMap when you need ordered iteration or range queries. entry().or_insert() is the idiomatic way to 'upsert' — it returns a mutable reference to the value, inserting a default if absent.
use std::collections::HashMap;
use std::collections::BTreeMap;
// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)
// iterate (unordered)
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebraHashSet & BTreeSet
HashSet stores unique values with O(1) membership checks. Set operations (union, intersection, difference, symmetric_difference) return iterators. Use sets for deduplication, membership testing, and mathematical set operations. BTreeSet is the sorted equivalent backed by BTreeMap.
use std::collections::HashSet;
let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();
a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));
// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();Tuples & Destructuring
Tuples group a fixed number of values of potentially different types. Access fields with .0, .1, etc. Destructuring with let patterns is idiomatic. The unit type () has one value () and is used like void in other languages. Tuples are commonly used to return multiple values from functions.
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");
// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);
// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);
// nested
let ((a, b), c) = ((1, 2), 3);
// unit tuple (empty)
let unit: () = ();
// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)Control Flow
If / Else If / Else
Unlike many languages, 'if' in Rust is an expression that returns a value. This eliminates the need for a ternary operator (condition ? a : b) — just use if/else. Both branches must return the same type. The condition does NOT need parentheses, but the body must be a block { }.
let score = 85;
// if is an expression — returns a value
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"F"
};
println!("Grade: {}", grade);
// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!Loops: loop, while, for
loop creates an infinite loop — break exits it and can return a value (break value). while checks a condition before each iteration. for is the most common loop, iterating over ranges, arrays, vectors, iterators. Use 'continue' to skip to the next iteration. Ranges: a..b is exclusive, a..=b is inclusive.
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
count += 1;
if count == 10 {
break count * 2; // break with value
}
};
println!("{}", result); // 20
// while: condition-checked loop
let mut n = 5;
while n > 0 {
println!("{}", n);
n -= 1;
}
// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); } // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }Match (Pattern Matching)
match is Rust's powerful pattern matching construct. It must be exhaustive (all possibilities covered) — use _ as a catch-all. Patterns support literals, ranges (..=), or-patterns (|), bindings, and guards (if). match is an expression and returns a value. It's the idiomatic way to handle enums like Option and Result.
let coin = 25;
match coin {
25 => println!("quarter"),
10 => println!("dime"),
5 => println!("nickel"),
1 => println!("penny"),
_ => println!("unknown"), // catch-all (required)
}
// matching with bindings
let x = 3;
match x {
1 | 2 => println!("one or two"), // or-pattern
3..=9 => println!("single digit"), // range
n if n % 2 == 0 => println!("even: {}", n), // guard
_ => println!("other"),
}
// match is exhaustive — all cases must be coveredIf Let & While Let
if let is syntactic sugar for match when you only care about one variant. It's less verbose but less exhaustive than match — use it when the other cases don't matter. while let loops as long as the pattern matches, commonly used with iterators (next() returns Option). The else branch is optional.
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);
// verbose: match
match maybe {
Some(x) => println!("got {}", x),
None => println!("nothing"),
}
// concise: if let
if let Some(x) = maybe {
println!("got {}", x);
} else {
println!("nothing");
}
// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
println!("{}", n);
}Break, Continue & Labels
Labels (single quote + name) allow breaking or continuing outer loops from within nested loops. This is essential when you need to exit multiple loop levels at once. Without labels, break/continue only affect the innermost loop. Labels are a clean alternative to flag variables.
// continue: skip to next iteration
for i in 0..10 {
if i % 2 == 0 { continue; }
println!("{}", i); // prints odd numbers
}
// break: exit loop
for i in 0..10 {
if i == 5 { break; }
println!("{}", i); // prints 0..4
}
// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
break 'outer; // breaks the outer loop
}
println!("{} {}", i, j);
}
}Functions & Closures
Defining Functions
Functions use 'fn' keyword. The last expression without a semicolon is the return value (expression). Adding a semicolon turns it into a statement returning (). Use explicit 'return' only for early exits. The ! return type marks diverging functions that never return (infinite loops, panics, process exits).
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = expression = return value
}
// statements (with semicolon) return ()
fn greet(name: &str) {
println!("Hi, {}", name);
// implicit return ()
}
// explicit return
fn abs(x: i32) -> i32 {
if x < 0 {
return -x; // early return needs 'return'
}
x // tail expression
}
// diverging function (never returns)
fn forever() -> ! {
loop {}
}Parameters & Arguments
Rust has no function overloading or optional parameters (use generics or builders instead). Choose parameter types carefully: &T for read access, &mut T for write access, T for ownership transfer. Slices (&[T]) are the idiomatic way to accept variable-length sequences. Default arguments aren't supported — use the builder pattern or Option<T>.
// immutable borrow
fn len(s: &String) -> usize { s.len() }
// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }
// take ownership
fn consume(s: String) { println!("{}", s); }
// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }
// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10Closures
Closures are anonymous functions that can capture their environment. They're inferred by usage. Closures capture by reference by default; 'move' forces ownership transfer (essential for threads). Closures implement Fn (borrow), FnMut (mut borrow), or FnOnce (consume) traits, enabling them to be passed as function parameters.
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3
// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };
// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15
// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();Higher-Order Functions & Iterators
Rust iterators are lazy — operations don't execute until .collect() or another consuming method is called. This allows zero-cost abstraction: the compiler can optimize chained iterator methods into efficient loops. Common methods: map (transform), filter (select), fold (accumulate), take (limit), skip, enumerate, zip, flat_map.
let nums = vec![1, 2, 3, 4, 5];
// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// fold/reduce: accumulate
let sum: i32 = nums.iter().sum(); // 15
let product: i32 = nums.iter().product(); // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);
// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
.filter(|&&x| x > 1)
.map(|&x| x * x)
.collect(); // [4, 9, 16, 25]Function Pointers & Traits
fn (lowercase) is a function pointer type — zero-cost, but can't capture environment. For closures that capture, use generic <F: Fn(...)> bounds. Fn borrows, FnMut mutably borrows, FnOnce consumes. Function pointers are useful for storing functions in structs or passing to C. Generics with Fn bounds are more flexible and still zero-cost when monomorphized.
// function pointer type
type MathFn = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }
fn apply(f: MathFn, a: i32, b: i32) -> i32 {
f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12
// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7Ownership & Borrowing
Ownership Rules
Ownership is Rust's core memory management system — no garbage collector needed. When you assign a heap value (String, Vec), ownership MOVES and the old variable becomes invalid. Stack types (i32, f64, bool, char, tuples of Copy types) implement Copy and are duplicated instead. This eliminates use-after-free and double-free bugs at compile time.
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1; // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move
// Rule 2: When owner goes out of scope, value is dropped
{
let s = String::from("temp");
// s is valid here
} // s is automatically dropped (memory freed)
// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a; // a is copied, both valid
println!("{} {}", a, b); // OKBorrowing & References
Borrowing lets you use a value without taking ownership. &T creates an immutable reference — you can have many simultaneously. &mut T creates a mutable reference — but only ONE mutable reference OR any number of immutable references, never both. This prevents data races at compile time. References must always point to valid data (no dangling pointers).
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
s.len()
// s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s); // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid
// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, worldSlice References
Slices are references to a contiguous portion of a collection. They're 'fat pointers' containing a pointer and length. String slices (&str) let functions accept both String and string literals. Array slices (&[T]) work with any contiguous sequence. Slices borrow the underlying data, preventing it from being modified or dropped while the slice exists.
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..]; // "world"
let full: &str = &s[..]; // "hello world"
// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]
// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i];
}
}
&s[..]
}Lifetimes
Lifetimes tell the compiler how long references are valid. The 'a annotation doesn't change lifetimes — it describes relationships. 'static is a special lifetime lasting the entire program (string literals have it). Most code uses lifetime elision (compiler infers). You need explicit lifetimes when: a function returns a reference, or a struct holds a reference.
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes
let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);
// struct holding references needs lifetime
struct Excerpt<'a> {
part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };Smart Pointers
Box<T> moves data to the heap (single owner). Rc<T> enables shared ownership via reference counting (single-threaded only). Arc<T> is the thread-safe version using atomics. RefCell<T> moves borrow checking to runtime, allowing mutation through shared references (interior mutability). Use Box for recursive types, Rc/Arc for graph-like structures, RefCell when you need to mutate shared data.
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b); // dereferenced automatically
// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2
// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);
// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtimeStructs, Enums & Traits
Defining Structs
Structs group related fields. Named-field structs are most common. Tuple structs are useful when field names aren't meaningful (Color, Point). Unit structs have no data and are used to implement traits. The .. syntax copies unspecified fields from another instance. Structs are stack-allocated unless they contain heap types (String, Vec, Box).
// named-field struct
struct User {
name: String,
age: u32,
active: bool,
}
let u = User {
name: String::from("Alice"),
age: 30,
active: true,
};
// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };
// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };
// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
// unit struct (no fields, useful for traits)
struct AlwaysEqual;Methods with impl
Methods go in impl blocks. &self borrows immutably, &mut self borrows mutably, self takes ownership (consuming). Associated functions (no self parameter) are like static methods — called with Type::function(). Self is an alias for the type. Multiple impl blocks are allowed, useful for splitting methods by concern or conditional compilation.
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// associated function (constructor, no &self)
fn new(w: f64, h: f64) -> Self {
Rectangle { width: w, height: h }
}
// method (borrows self)
fn area(&self) -> f64 {
self.width * self.height
}
// mutable method
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// consuming method (takes ownership)
fn into_square(self) -> Rectangle {
let side = (self.width + self.height) / 2.0;
Rectangle { width: side, height: side }
}
}
let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumedEnums & Pattern Matching
Enums in Rust are algebraic data types — each variant can carry different data. This makes them far more powerful than C enums. Pattern matching with match destructures variants and extracts their data. Use enums when a value can be one of several distinct forms. matches! macro is a shorthand for single-pattern matching returning bool.
// enum with data (algebraic data type)
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // tuple variant
ChangeColor(i32, i32, i32), // tuple variant
}
// pattern matching with destructuring
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
}
}
process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));
// enums can have methods too
impl Message {
fn is_quit(&self) -> bool {
matches!(self, Message::Quit)
}
}Option & Result
Option<T> replaces null — you must explicitly handle the None case, eliminating NullPointerException-style bugs. Result<T, E> is for operations that can fail. Both have rich methods: map (transform), and_then (chain), unwrap_or (default), is_some/is_ok (check). The ? operator on Result propagates errors automatically. These two types are the backbone of Rust's error handling.
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
if id == 1 { Some(String::from("Alice")) }
else { None }
}
let user = find_user(1);
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));
// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
match parse_num("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}Traits & Trait Bounds
Traits define shared behavior (like interfaces in other languages). Types implement traits with 'impl Trait for Type'. Traits can have default method implementations. Trait bounds (<T: Trait>) constrain generics to types implementing certain traits. The 'where' clause improves readability for complex bounds. Traits enable polymorphism via both static dispatch (generics) and dynamic dispatch (trait objects &dyn Trait).
// define a trait (interface)
trait Summary {
fn summarize(&self) -> String;
// default method
fn author(&self) -> String {
String::from("Unknown")
}
}
struct Article { title: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
// author() uses default implementation
}
let a = Article {
title: "Rust".into(),
content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown
// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}Derive Macros & Trait Objects
#[derive(...)] auto-implements common traits: Debug (debug printing), Clone (deep copy), PartialEq/Eq (== comparison), Hash (for HashMap keys), Copy (stack copy instead of move). Trait objects (&dyn Trait or Box<dyn Trait>) enable runtime polymorphism via a vtable, at a small performance cost. Use generics for static dispatch (zero-cost) when possible, trait objects when you need heterogeneous collections.
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq -> true
// trait object: dynamic dispatch
trait Animal {
fn sound(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }
// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.sound());
}Error Handling
Result & ? Operator
The ? operator is the idiomatic way to propagate errors. On Ok(v), it unwraps to v. On Err(e), it returns Err(e) from the function immediately. ? also converts error types via the From trait, so functions returning Box<dyn Error> can use ? on any error type. On Option, ? returns None early. This makes error handling concise without sacrificing safety.
use std::fs;
use std::io;
use std::num::ParseIntError;
// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
let content = fs::read_to_string(path)?; // ? on io::Result
Ok(content.len() as i32)
}
// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // io::Error -> Box
let n: i32 = content.trim().parse()?; // ParseIntError -> Box
Ok(n)
}
// ? on Option too
fn first_char(s: &str) -> Option<char> {
s.lines().next()?.chars().next()
}Panic vs Result
Use panic! for unrecoverable errors (bugs, violated invariants) — it indicates a programming error. Use Result for expected, recoverable failures (user input, file I/O, network). unwrap()/expect() panic on error — acceptable in tests, prototypes, or when you can prove the value is valid. In production code, prefer proper error handling with ? and match.
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!"); // unwinds stack
}
a / b
}
// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap(); // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg
// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)Custom Error Types
Custom error types give you type-safe, structured error handling. Implement Display (human-readable) and Error (for source chaining). Implement From for each underlying error so ? converts automatically. Libraries like thiserror (derive macro) or anyhow (dynamic error boxes) reduce boilerplate. Use thiserror for libraries, anyhow for applications.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
}
// implement Display (required by Error)
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(name) => write!(f, "Not found: {}", name),
}
}
}
// implement Error
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
_ => None,
}
}
}
// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}Matching & Combining Results
Result combinators allow functional-style error handling without match. map transforms the Ok value, map_err transforms the error, and_then chains fallible operations (flatMap). unwrap_or provides a default on error. is_ok/is_err check without consuming. These methods make error handling chains readable and avoid deep nesting.
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
Ok(n) => println!("Got: {}", n),
Err(e) => println!("Error: {}", e),
}
// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2); // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));
// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });
// unwrap_or family
let val = r1.unwrap_or(0); // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default
// check variants
println!("{}", r1.is_ok()); // true
println!("{}", r1.is_err()); // falseConverting Errors with From
The ? operator uses From to convert errors. Box<dyn Error> implements From for all standard errors, making it a convenient catch-all. The anyhow crate provides anyhow::Result which adds context (e.g., .context("failed to read config")?). For libraries, define a specific error enum with thiserror; for applications, use anyhow for simplicity.
use std::fs;
use std::num::ParseIntError;
// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
let content = fs::read_to_string(path)
.map_err(|e| e.to_string())?; // manual convert
let n: i32 = content.trim().parse()
.map_err(|e| e.to_string())?; // manual convert
Ok(n)
}
// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // auto-converts
let n: i32 = content.trim().parse()?; // auto-converts
Ok(n)
}
// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
// let n: i32 = something()?; // any error auto-converted
// Ok(n)
// }Modules & Crates
Module System
Rust's module system organizes code. 'mod' declares a module (inline or via file). 'pub' makes items public (default is private). 'use' creates shortcuts to paths. 'pub use' re-exports items (useful for API design). The file system mirrors the module tree: mod network can be src/network.rs or src/network/mod.rs. Crate root is lib.rs (libraries) or main.rs (binaries).
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
pub mod connection {
pub fn connect() -> bool { true }
fn disconnect() {} // private
}
}
// use: bring paths into scope
use network::connection::connect;
// calling with full path
fn main() {
network::connection::connect();
connect(); // after 'use'
}
// re-export with pub use
pub use network::connection::connect as open_connection;
// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;Privacy & Visibility
Privacy in Rust is module-scoped. Items are private by default — only accessible within their defining module and descendants. 'pub' makes them public. 'pub(crate)' restricts to the current crate (useful for library internals). 'pub(super)' restricts to the parent module. Struct fields have individual visibility — a pub struct can have private fields, requiring a constructor.
mod my_module {
// private by default
fn internal_helper() {}
// public: accessible from outside
pub fn public_api() {
internal_helper(); // can call private within module
}
// pub(crate): visible within this crate only
pub(crate) fn crate_wide() {}
// pub(super): visible to parent module
pub(super) fn parent_visible() {}
// struct fields are private even if struct is pub
pub struct Config {
pub name: String, // public field
secret: String, // private field
}
// enum variants inherit the enum's visibility
pub enum Status {
Active, // public because Status is public
Inactive,
}
}Use Statements & Aliasing
use statements bring items into scope, reducing path verbosity. Grouped imports (use std::io::{self, Read}) are cleaner than multiple lines. Traits must be in scope to use their methods — this is why you sometimes need 'use std::io::Read' even if you don't reference Read by name. Glob imports (*) are discouraged except for preludes.
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;
// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;
// aliasing with 'as'
use std::collections::HashMap as Map;
// glob import (use sparingly)
use std::prelude::v1::*;
// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait methodCargo & External Crates
Cargo is Rust's package manager and build system. Dependencies go in Cargo.toml under [dependencies]. Features enable optional functionality (reducing compile time/binary size). 'cargo add' auto-edits Cargo.toml. Crates are published to crates.io. Edition (2021) controls language features. Cargo handles compilation, testing, documentation, and publishing.
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;
#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }
fn main() {
let mut rng = rand::thread_rng();
let n: i32 = rng.gen_range(1..=100);
println!("{}", n);
}
# CLI commands:
# cargo new my_app # create new project
# cargo build # compile
# cargo run # compile + run
# cargo test # run tests
# cargo add serde # add dependency
# cargo update # update dependenciesTesting
Tests use #[test] attribute. assert_eq!/assert_ne! compare values. #[should_panic] verifies a panic occurs. Tests can return Result for error-based assertions. #[cfg(test)] ensures the tests module only compiles during testing. Unit tests live alongside code; integration tests go in tests/ directory. Run with 'cargo test'. Use #[ignore] to skip flaky tests.
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_ne!(add(2, 3), 6);
}
#[test]
fn test_with_message() {
let result = add(1, 1);
assert_eq!(result, 2, "Expected 2, got {}", result);
}
#[test]
#[should_panic(expected = "division by zero")]
fn test_panic() {
panic!("division by zero");
}
#[test]
fn test_result() -> Result<(), String> {
if add(2, 2) == 4 { Ok(()) }
else { Err(String::from("math is broken")) }
}
}
// Integration tests: tests/integration_test.rs
// Run: cargo testConcurrency & File I/O
Threads
std::thread::spawn creates OS threads. The closure must be 'move' if it captures variables, transferring ownership to the thread (preventing use-after-free). join() blocks until the thread completes, returning a Result. Rust's ownership system prevents data races at compile time — you cannot share mutable data between threads without synchronization (Arc<Mutex<T>>).
use std::thread;
use std::time::Duration;
// spawn a thread
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// main thread continues
for i in 0..3 {
println!("main thread: {}", i);
}
// wait for spawned thread to finish
handle.join().unwrap();
// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();Channels (Message Passing)
Channels enable thread communication via message passing (Go's motto: 'Share memory by communicating'). mpsc allows multiple senders (tx.clone()) but one receiver. send() returns Result (Err if receiver dropped). The receiver implements Iterator, so for loops work naturally. For multiple receivers, use crossbeam-channel or async channels. Message passing avoids the complexity of shared mutable state.
use std::sync::mpsc;
use std::thread;
// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();
// clone transmitter for multiple producers
let tx2 = tx.clone();
thread::spawn(move || {
let vals = vec!["a", "b", "c"];
for v in vals {
tx.send(v).unwrap();
}
});
thread::spawn(move || {
tx2.send("from tx2").unwrap();
});
// receiver is an iterator
for received in rx {
println!("Got: {}", received);
}Mutex & Arc (Shared State)
Arc (Atomic Reference Counted) enables shared ownership across threads (thread-safe Rc). Mutex provides exclusive access — lock() blocks until acquired, returns a MutexGuard that releases the lock on drop. RwLock allows multiple readers or one writer. The combination Arc<Mutex<T>> is the standard pattern for shared mutable state. unwrap() on lock() handles poison (a thread panicked while holding the lock).
use std::sync::{Arc, Mutex};
use std::thread;
// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
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, auto-unlocks when dropped
let mut num = counter.lock().unwrap();
*num += 1;
}); // lock released here
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10
// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap(); // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 droppedFile I/O
fs::read_to_string is convenient for small files. For large files, use BufReader/BufWriter to reduce system calls. Read/Write traits provide low-level byte operations. BufRead adds lines() and read_line() for text. Always handle errors with ? (files can be missing, permissions denied, disk full). flush() ensures buffered data reaches the OS (though not necessarily the disk).
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;
// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;
// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
println!("{}", line?);
}
// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to diskAsync/Await (Tokio)
Async/await enables efficient concurrency without OS threads — tasks run on a thread pool and yield at .await points. tokio is the most popular async runtime. async fn returns a Future that must be .awaited. tokio::join! runs futures concurrently and waits for all. tokio::select! races futures. Async is ideal for I/O-bound work (network, files); use threads for CPU-bound work. The .await doesn't block the thread — it yields control back to the runtime.
use tokio::{fs, task, time};
use std::time::Duration;
// async fn returns a Future
async fn fetch_data() -> String {
time::sleep(Duration::from_secs(1)).await;
String::from("data")
}
// spawn concurrent tasks
#[tokio::main]
async fn main() {
// run tasks concurrently
let (a, b, c) = tokio::join!(
fetch_data(),
fetch_data(),
fetch_data()
);
println!("{} {} {}", a, b, c);
// spawn a background task
let handle = tokio::spawn(async {
let data = fs::read_to_string("file.txt").await.unwrap();
println!("Read {} bytes", data.len());
});
handle.await.unwrap();
// select: first to complete wins
tokio::select! {
val = fetch_data() => println!("Got: {}", val),
_ = time::sleep(Duration::from_millis(500)) => {
println!("Timeout!");
}
}
}Lifetimes Deep Dive
Named Lifetimes in Functions
Lifetimes are the compiler's way of tracking reference validity. The 'a is a generic lifetime parameter — it doesn't change runtime behavior, only compile-time checking. When a function takes multiple references and returns one, you must annotate lifetimes so the compiler knows the returned reference won't outlive its inputs. This prevents dangling pointers at compile time.
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y
let s1 = String::from("long string");
let result;
{
let s2 = String::from("hi");
result = longest(s1.as_str(), s2.as_str());
// result valid here
println!("{}", result);
}
// result NOT valid here — s2 is droppedLifetime Elision Rules
Lifetime elision rules let you omit explicit lifetime annotations in common cases. Rule 1 assigns a distinct lifetime to each reference parameter. Rule 2 assigns that lifetime to the output if there's exactly one input reference. Rule 3 applies to methods — the output gets &self's lifetime. When none of these resolve all references, you must write explicit lifetimes. Most idiomatic Rust code rarely needs explicit lifetime annotations.
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime
// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' { return &s[0..i]; }
}
&s[..]
}
// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a strThe 'static Lifetime
'static is the longest lifetime — it lasts for the entire program duration. All string literals have this lifetime because they're embedded in the binary. When you see T: 'static as a bound, it doesn't mean T must live forever — it means T must not contain any references shorter than 'static (i.e., owned types like String, Vec, i64 always satisfy this). Threads require 'static because they may outlive the calling function.
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str
// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);
// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
std::thread::spawn(move || {
drop(t);
});
}Lifetimes in Structs
When a struct holds a reference (not an owned type), it needs a lifetime parameter to declare how long that reference is valid. The struct instance cannot outlive the data it borrows. This is common for zero-copy parsers, iterators over borrowed data, and views. The lifetime must be declared on both the struct and its impl block. Prefer owning data (String, Vec) unless you have a specific reason to borrow.
// Structs holding references need lifetime annotations
struct Parser<'a> {
text: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(text: &'a str) -> Self {
Parser { text, pos: 0 }
}
fn peek(&self) -> Option<char> {
self.text[self.pos..].chars().next()
}
fn advance(&mut self) {
self.pos += 1;
}
}
// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();Multiple Lifetimes & Subtyping
Functions with multiple references that interact differently need multiple lifetime parameters. Lifetime subtyping means a longer lifetime can substitute for a shorter one (covariance) — &'static can be used wherever &'a is expected. This is why 'static is a subtype of all lifetimes. Use multiple lifetimes when the output's lifetime depends on only some inputs, giving the compiler more flexibility and the caller fewer constraints.
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
// returns something tied to src, not ctx
src
}
// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
"constant" // 'static coerces to 'a
}
// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long); // OK: 'static coerces to any 'shortSmart Pointers (Box, Rc, Arc, RefCell)
Box<T> — Heap Allocation
Box<T> is Rust's simplest smart pointer — it heap-allocates a value with single ownership. Use it for recursive types (whose size can't be known at compile time), large values you don't want to move on the stack, and trait objects (Box<dyn Trait>) for dynamic dispatch. Box derefs to T so it behaves like the inner value. It has essentially zero overhead beyond the heap allocation itself.
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b); // derefs automatically
// Recursive types NEED Box (size unknown at compile time)
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Box has zero runtime overhead when dereferencingRc<T> — Reference Counting (Single Thread)
Rc<T> (Reference Counted) enables multiple ownership for single-threaded scenarios. Rc::clone increments the reference count rather than copying data — cheap pointer copy. The value is dropped when the last Rc is dropped. Use Rc for shared graph nodes, parent-child trees, or any structure where multiple parts need to own the same data. Rc is NOT thread-safe — use Arc for multithreading. Rc is immutable — you can't mutate through it directly.
use std::rc::Rc;
// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments count, doesn't copy
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
// Data dropped when count reaches 0
// Shared graph/tree structures
struct Node {
children: Vec<Rc<Node>>,
value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
children: vec![Rc::clone(&leaf)],
value: 2,
});Arc<T> — Atomic Reference Counting (Thread-Safe)
Arc<T> (Atomically Reference Counted) is the thread-safe counterpart of Rc. It uses atomic operations for reference counting, making it safe to share across threads. The trade-off is slightly more overhead than Rc. Use Arc whenever you need shared ownership across multiple threads. To mutate shared data, combine Arc with Mutex (for exclusive access) or RwLock (for read-heavy access). Arc::clone is cheap — it just increments an atomic counter.
use std::sync::Arc;
use std::thread;
// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let handles: Vec<_> = (0..3).map(|i| {
let data = Arc::clone(&data); // atomic increment
thread::spawn(move || {
println!("Thread {} sees: {:?}", i, *data);
})
}).collect();
for h in handles { h.join().unwrap(); }
// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threadsRefCell<T> — Interior Mutability
RefCell<T> provides interior mutability — you can mutate through a shared reference, with borrow rules enforced at runtime instead of compile time. borrow() returns an immutable reference, borrow_mut() a mutable one. Violating the rules (e.g., two mutable borrows) causes a panic at runtime. Use RefCell when the compiler can't prove borrow safety (e.g., graph structures, mock objects in tests). Rc<RefCell<T>> is the classic pattern for mutable shared single-threaded data.
use std::cell::RefCell;
// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);
// Multiple immutable borrows OR one mutable borrow
{
let mut borrowed = cell.borrow_mut();
borrowed.push(4);
} // borrow released here
{
let r1 = cell.borrow(); // OK
let r2 = cell.borrow(); // OK — multiple immutable
println!("{:?} {:?}", r1, r2);
}
// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphsWeak<T> — Breaking Reference Cycles
Weak<T> is a non-owning reference that doesn't affect the strong reference count. This is essential for breaking reference cycles: if a parent owns children (Rc) and children own the parent (Rc), neither will ever be freed — a memory leak. The solution is to make the back-reference Weak. upgrade() returns Option<Rc<T>> — None if the value was already dropped. Use Weak for child→parent links, caches, and observer patterns where you don't want to keep data alive.
use std::rc::{Rc, Weak, RefCell};
// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
parent: RefCell<Weak<Node>>, // weak: child → parent
children: RefCell<Vec<Rc<Node>>>, // strong: parent → children
}
let leaf = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
println!("parent exists");
}Trait Objects & Dynamic Dispatch
dyn Trait — Dynamic Dispatch
dyn Trait enables dynamic dispatch — the concrete type is erased at compile time and method calls go through a vtable at runtime. This lets you store heterogeneous types in a single collection (Vec<Box<dyn Animal>>). The trade-off: a small runtime cost (vtable indirection, no inlining) and the type can't be known at compile time. Use trait objects when the set of concrete types isn't known at compile time or when you need to group different types together.
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Meow".into() }
}
// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Rex".into() }),
Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
println!("{} says {}", a.name(), a.sound());
}Object Safety Rules
A trait is object-safe only if the compiler can build a vtable for it. Two rules: (1) methods must not return Self (the concrete type is erased, so it can't be known), and (2) methods must not have generic type parameters (the vtable would need an entry for every possible type). Traits with Sized as a supertrait are also not object-safe. If you need object safety, refactor Self-returning methods to return Box<dyn Trait> or use a separate factory function.
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
fn draw(&self); // OK: &self, no generics
}
// NOT object-safe:
trait Bad {
fn create() -> Self; // returns Self — needs known type
fn process<T>(&self, x: T); // generic method — vtable can't cover all T
const SIZE: usize; // associated const (sometimes OK)
}
// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
fn new_boxed() -> Box<dyn GoodFactory>;
}
// Sized bound makes trait non-object-safe
// trait Foo: Sized {} // NOT object-safeTrait Objects vs Generics
Generics use monomorphization — the compiler generates a separate copy of the function for each concrete type, enabling static dispatch and full optimization (inlining). This has zero runtime cost but increases binary size. Trait objects (dyn) use a single function with vtable lookup — smaller binary but a small runtime cost per call. Choose generics when performance matters and the type set is small/known; choose dyn when you need heterogeneous collections or don't know all types upfront.
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.
// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
// can't return — don't know size at compile time
a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}
// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dynAny Trait & Downcasting
The Any trait lets you store values of any type and recover the concrete type at runtime via downcasting. downcast_ref::<T>() returns Option<&T>, downcast::<T>() returns Result<Box<T>, Box<dyn Any>>. This is Rust's escape hatch for when you truly don't know the type at compile time (plugin systems, dynamic configs). However, prefer enums when the set of possible types is known — they're safer, faster, and more idiomatic. Any relies on TypeId, which is implemented for all 'static types.
use std::any::Any;
// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);
// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
println!("It's an i32: {}", n);
}
// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
println!("Recovered: {}", s);
}
// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is knownDefault Trait Methods & Supertraits
Traits can provide default method implementations that implementors can override or use as-is. Supertraits (trait Named: Shape) require the implementing type to also implement the supertrait — this creates a hierarchy where Named types are guaranteed to have area() and describe(). Default methods reduce boilerplate and enable the 'extension method' pattern where adding a method to a trait automatically benefits all existing implementors without breaking them.
trait Shape {
fn area(&self) -> f64;
// Default method — can be overridden
fn describe(&self) -> String {
format!("Shape with area {:.2}", self.area())
}
}
// Supertrait: trait that requires another trait
trait Named: Shape {
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
fn name(&self) -> &str { "Circle" }
}
let c = Circle { radius: 2.0 };
println!("{}", c.describe()); // uses default methodMacros (Declarative & Procedural)
Declarative Macros (macro_rules!)
macro_rules! creates declarative macros that expand at compile time via pattern matching. The $(...),* syntax is a repetition matcher — it matches zero or more comma-separated expressions. $x:expr means 'match any expression and bind it to x'. Macros are expanded before type checking, so they can generate code that works with any type. Use macros for reducing boilerplate that generics can't handle (e.g., variadic arguments, syntax extension).
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
let nums = vec_of!(1, 2, 3, 4);
// Recursive macro: build a HashMap
macro_rules! hashmap {
($($k:expr => $v:expr),*) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}};
}
let config = hashmap!("host" => "localhost", "port" => 8080);Macro Fragment Types
Fragment specifiers determine what kind of syntax a macro argument matches. :ident matches identifiers (names), :expr matches expressions (values), :ty matches types, :block matches brace-delimited blocks, :stmt matches statements, :literal matches literals. Choosing the right specifier matters — :expr is most common but :ident is needed when you want to create a function/variable name. The macro system is hygienic: identifiers introduced by macros don't collide with surrounding code.
// Common fragment specifiers:
macro_rules! build_fn {
// $name:ident — identifier (function/variable name)
// $body:block — a block { ... }
// $ty:ty — a type
// $expr:expr — an expression
// $stmt:stmt — a statement
// $lit:literal — a literal (string, number)
($name:ident, $ret:ty, $body:block) => {
fn $name() -> $ret $body
};
}
build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());
// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibilityBuilt-in Standard Macros
Rust ships with many built-in macros. println!/eprintln! print to stdout/stderr. dbg! prints an expression's value with file/line info — great for debugging (it also returns the value). assert!/assert_eq!/assert_ne! are for tests and invariants. todo!/unimplemented! mark incomplete code with a panic. file!/line!/module! give compile-time location info. env!/option_env! read environment variables at compile time — useful for embedding version info.
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops"); // stderr
format!("{}-{}", "a", "b"); // returns String
// Debug helpers
dbg!(2 + 2); // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2); // panic if false
assert_eq!(2 + 2, 4); // panic if not equal
assert_ne!(1, 2); // panic if equal
// Code generation
todo!("not implemented yet"); // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");
// Environment & file info
println!("{}:{}", file!(), line!()); // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));Procedural Macros Overview
Procedural macros (proc macros) are Rust functions that take TokenStreams as input and produce TokenStreams as output — full code-to-code transformation. Unlike declarative macros, they can do arbitrary computation. Three types: derive macros (add trait implementations via #[derive]), attribute macros (annotate items), and function-like macros (custom syntax like sqlx::query!). They must live in a separate crate with proc-macro = true. The syn crate parses Rust syntax, quote! generates code. Popular examples: serde, tokio, thiserror.
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):
// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }
// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}
// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");
// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0" # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"Macro Hygiene & Common Patterns
Rust macros are hygienic — identifiers created inside a macro exist in a separate 'syntax context' and won't accidentally capture or shadow variables in the calling scope. This prevents subtle bugs where a macro's internal variable name collides with the caller's. stringify! converts any token stream into a string literal at compile time (useful for error messages). cfg_debug! shows a common pattern: conditionally compiling code based on build configuration using the cfg! system.
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
($e:expr) => {
let temp = $e; // this 'temp' is distinct from caller's
println!("{}", temp);
};
}
let temp = 10;
using_temp!(temp + 5); // no conflict — hygienic
// Conditional compilation macro
macro_rules! cfg_debug {
($($e:tt)*) => {
#[cfg(debug_assertions)]
{ $($e)* }
};
}
cfg_debug! {
println!("Debug mode on");
}
// stringify! converts tokens to a string literal
let s = stringify!(a + b * c); // "a + b * c"Unsafe Rust
Raw Pointers
Raw pointers (*const T, *mut T) are Rust's escape hatch from the borrow checker. Unlike references, they can be null, can alias (multiple pointers to same data), and don't track lifetimes. Creating them is safe, but dereferencing requires unsafe because the compiler can't guarantee validity. Use raw pointers for FFI (interfacing with C), implementing low-level data structures (linked lists, vectors), and performance-critical code where you manually ensure safety. Always document why unsafe is sound.
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x; // coerce from reference
let r2: *mut i32 = x as *mut i32; // cast
// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();
// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
println!("r1 = {}", *r1);
}
// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); } // little-endian intUnsafe Blocks & Functions
unsafe doesn't turn off the borrow checker — it lets you do five specific things: (1) dereference raw pointers, (2) call unsafe functions, (3) implement unsafe traits, (4) access/mutate static mut, (5) access union fields. unsafe blocks make the unsafe operations explicit and localized. unsafe fn declares that calling the function requires upholding invariants the compiler can't check. get_unchecked skips bounds checking for performance — only safe if you've verified the index. Minimize unsafe surface area and encapsulate it behind a safe API.
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };
// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
*ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };
// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
let first = v.get_unchecked(0); // no bounds check
let last = v.get_unchecked(len - 1);
println!("{} {}", first, last);
}FFI — Calling C Functions
FFI (Foreign Function Interface) lets Rust call C functions and vice versa. extern "C" blocks declare external C functions — calling them is unsafe because the compiler can't verify their behavior. #[no_mangle] prevents Rust from renaming the function so C can find it by name. #[repr(C)] guarantees the struct layout matches C's memory layout (Rust may reorder fields by default for efficiency). Use FFI for system calls, legacy libraries, and performance-critical bindings. The bindgen crate auto-generates FFI declarations from C headers.
// extern "C" declares foreign functions
extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
let x = -5;
let positive = unsafe { abs(x) };
println!("{}", positive); // 5
}
// Exporting Rust functions to C
#[no_mangle] // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
a + b
}
// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }Implementing Unsafe Traits
Unsafe traits (like Send, Sync) require the implementor to uphold invariants the compiler can't verify. Send means a type can safely move between threads; Sync means &T can be shared between threads. The compiler auto-derives these for most types, but raw pointers aren't Send/Sync by default. When you manually implement them, you take responsibility for thread safety. static mut requires unsafe access because multiple threads could race on it — prefer atomics (AtomicU64) or Mutex instead. Always document the safety rationale with a SAFETY comment.
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;
// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);
// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}
// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally
// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; } // unsafe: data races possibleUnions & Inline Assembly
Unions allow different types to share the same memory location — reading a field that wasn't last written is undefined behavior, hence unsafe. They're mainly for FFI with C. transmute reinterpret-casts the bit pattern of one type to another of the same size — extremely dangerous if sizes differ or types are incompatible. Inline assembly (asm!) lets you embed CPU instructions directly, useful for kernel development and extreme optimization. All of these are sharp tools: use only when no safe alternative exists, and encapsulate behind a safe abstraction.
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
i: i32,
f: f32,
}
let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!
// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
std::arch::asm!("hlt", options(nostack));
}
// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB; // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };Iterators Deep Dive
Iterator Trait & Creation
The Iterator trait requires only a next() method that returns Option<Item> — None signals exhaustion. Everything else (map, filter, collect) is built on top. iter() borrows elements (&T), into_iter() consumes the collection (yields owned T), iter_mut() yields &mut T. Ranges (1..5, 1..=5) are iterators directly. Strings iterate by chars (Unicode scalar values) or bytes. Iterators are lazy — nothing runs until you consume them.
// The Iterator trait: one required method
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... many provided methods (map, filter, etc.)
}
// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter(); // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut(); // mutably borrows: &mut i32
// Range is an iterator
for i in 1..=5 { print!("{} ", i); } // 1 2 3 4 5
// String iteration
for c in "héllo".chars() { print!("{} ", c); } // h é l l o
for b in "hi".bytes() { print!("{} ", b); } // 104 105Adapter Methods (Lazy)
Adapter methods transform iterators and return new iterators — they're lazy, so chaining map().filter().map() creates zero intermediate collections. map applies a function to each element. filter keeps elements where the predicate returns true. take(n) stops after n elements (useful for infinite iterators). skip(n) discards the first n. flat_map maps and flattens nested iterators. enumerate pairs each element with its index. Nothing executes until a consumer (collect, sum, for loop) drives the iterator.
let nums = vec![1, 2, 3, 4, 5, 6];
// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect(); // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect(); // [3,4,5,6]
// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
.flat_map(|s| s.split(' '))
.collect(); // ["a","b","c"]
// enumerate: add index
for (i, v) in nums.iter().enumerate() {
println!("{}: {}", i, v);
}Consumer Methods
Consumer methods drive the lazy iterator chain to actually execute. collect() gathers results into any collection implementing FromIterator (Vec, HashMap, String, etc.). sum/product/count/fold reduce the iterator to a single value. find/any/all short-circuit — they stop as soon as the answer is known, so they're efficient on infinite iterators. min/max return Option (None for empty iterators). The combination of lazy adapters + a final consumer means iterator chains are as efficient as hand-written loops after optimization.
let nums = vec![1, 2, 3, 4, 5];
// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();
// sum, product, count
let total: i32 = nums.iter().sum(); // 15
let prod: i32 = nums.iter().product(); // 120
let count = nums.iter().count(); // 5
// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max); // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15
// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0); // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0); // false
let all_pos = nums.iter().all(|&x| *x > 0); // true
// min / max
nums.iter().copied().min(); // Some(1)
nums.iter().copied().max(); // Some(5)Custom Iterators
To create a custom iterator, implement the Iterator trait with a next() method. Once you do, you get all 70+ adapter and consumer methods for free. The iterator should track its own state (current position, etc.) and return None when exhausted. Implementing IntoIterator for your collection type enables for-loop syntax. For bidirectional or random access, also implement DoubleEndedIterator or ExactSizeIterator. This is how Vec, HashMap, Range, and all standard collections provide iteration.
// Build a custom iterator for a counter
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let val = self.current;
self.current += 1;
Some(val)
} else {
None
}
}
}
// Now all iterator methods work!
let sum: usize = Counter::new(5).sum(); // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();Infinite & Chained Iterators
Rust iterators can be infinite — (1..) generates natural numbers forever, repeat(x) repeats endlessly. These are safe because adapters are lazy: take(n) limits consumption. cycle() repeats a finite iterator infinitely. chain() concatenates iterators sequentially. zip() pairs elements positionally (stops at the shorter). peekable() lets you look at the next element without consuming it — useful for parsers. The lazy design means infinite iterators cost nothing until consumed, and the compiler optimizes chains into tight loops.
use std::iter;
// Infinite iterators (use with take!)
let naturals = (1..).take(10); // 1..10
let zeros = iter::repeat(0).take(5); // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());
// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]
// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]
// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]
// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }Error Handling Deep Dive
Result & Option Combinators
Combinators let you chain fallible operations without nested match expressions. map transforms the Ok value, map_err transforms the error. and_then chains operations that themselves return Result (flatmap for errors). ok_or converts Option→Result. unwrap_or/unwrap_or_else/unwrap_or_default provide fallback values. These compose elegantly: parse().map().and_then().map_err() creates a pipeline where each step can fail, and the first failure short-circuits. Prefer combinators over unwrap() in production code.
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>().map(|n| n * 2)
}
// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate); // Ok(5)
// map_err: transform the error type
let r = "abc".parse::<i32>()
.map_err(|e| format!("Parse failed: {}", e));
// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value"); // Err("missing value")
// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);Custom Error Types
Custom error types let you represent domain-specific failures. The key pattern: implement From for each underlying error type so the ? operator auto-converts. This means you can use ? with std::io::Error, ParseIntError, etc. without explicit map_err. Implementing Display makes the error user-friendly; Debug is for developers. Enum-based errors are idiomatic in Rust — they're exhaustive (the compiler warns about missing cases) and zero-cost (no heap allocation). This is the foundation before using thiserror.
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
Unauthorized,
}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(s) => write!(f, "Not found: {}", s),
AppError::Unauthorized => write!(f, "Unauthorized"),
}
}
}
// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
let s = std::fs::read_to_string("config.txt")?; // io → AppError
let n: i32 = s.trim().parse()?; // parse → AppError
Ok(n)
}The Error Trait & Box<dyn Error>
std::error::Error is the standard library trait for error types (requires Debug + Display). Box<dyn Error> is the simplest error type — it accepts any error via ? and is great for prototyping or applications where you don't need to handle specific errors programmatically. The downside: you lose the concrete error type, so matching on specific variants requires downcast_ref. For libraries, prefer a concrete enum error type (with thiserror). For applications, anyhow is a better choice than Box<dyn Error> because it preserves backtraces and error chains.
use std::error::Error;
// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
let f = std::fs::read_to_string("file.txt")?; // io::Error
let n: i32 = f.parse()?; // ParseIntError
println!("{}", n);
Ok(())
}
// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info
// downcast to recover specific error type
match do_something() {
Err(e) => {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
println!("IO: {}", io_err);
}
}
_ => {}
}thiserror Crate (Library Errors)
thiserror is the standard crate for library error types. The #[derive(Error)] macro generates Display (from #[error("...")]) and From (from #[from]) implementations automatically. #[from] makes ? convert the underlying error into your enum variant. This eliminates boilerplate while keeping a strongly-typed, exhaustive error enum. Use thiserror for libraries (where callers need to match on specific errors). The {0} placeholder inserts the inner error's Display; named fields like {id} insert struct fields.
use thiserror::Error;
// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("parse failed: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("item {id} not found")]
NotFound { id: u32 },
#[error("invalid state: {msg}")]
Invalid { msg: String },
}
// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
let s = std::fs::read_to_string("data.txt")?; // auto-converts
let n: i32 = s.trim().parse()?;
Ok(n)
}anyhow Crate (Application Errors)
anyhow is the standard crate for application/binaries error handling. anyhow::Error wraps any error implementing std::error::Error and adds context, backtraces, and error chaining. context() attaches a human-readable message to each fallible step, creating a chain like 'Failed to read config: IO error: No such file'. This makes debugging much easier — you see exactly which step failed and why. Use anyhow for main() and application code where you just need to report errors, not match on them. Use thiserror for libraries where callers need typed errors.
use anyhow::{Context, Result, anyhow};
// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("Failed to read config.toml")?; // add context
Ok(content)
}
fn main() -> Result<()> {
let config = load_config()?;
if config.is_empty() {
// bail! / anyhow! create errors with format! syntax
return Err(anyhow!("config is empty"));
}
println!("{}", config);
Ok(())
}
// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtraceCargo & Crates Deep Dive
Cargo.toml Structure
Cargo.toml is the manifest for a Rust project. [package] describes the crate metadata. [dependencies] lists external crates — version strings use semver (^1.0 means >=1.0, <2.0). features enable optional functionality (serde's derive feature turns on #[derive(Serialize)]). [dev-dependencies] are only for tests/benchmarks. [features] define conditional compilation flags. [profile.release] controls optimization settings. The edition field (2015/2018/2021) determines language features — always use the latest.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"
[dev-dependencies]
criterion = "0.5" # only for tests/benches
[features]
default = ["tokio"]
async-mode = ["dep:tokio"]
[[bin]]
name = "myapp"
path = "src/main.rs"
[profile.release]
opt-level = 3
lto = true
strip = trueDependencies & Feature Flags
Feature flags enable conditional compilation. Each dependency can expose features (e.g., serde's derive). default-features = false strips default features to reduce binary size. Optional dependencies (optional = true) are only compiled when a feature enables them via dep:name. Features are additive — they turn things on, never off. This ensures feature unification: if two dependencies enable different features of serde, Cargo compiles serde once with the union of all features. Use #[cfg(feature = "x")] to conditionally compile code.
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0" # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false } # disable defaults
# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }
[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]
# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }Workspaces (Multi-Crate Projects)
Workspaces group multiple related crates that share a Cargo.lock and target directory. This speeds up builds (shared compilation cache) and ensures all crates use the same dependency versions. Members can depend on each other via path = "../core". [workspace.dependencies] centralizes version management — member crates reference them with { workspace = true }. The resolver = "2" (default in edition 2021) uses feature unification per-target, avoiding some build issues. Use workspaces for monorepos, libraries with multiple components, or projects splitting core/CLI/server.
# Root Cargo.toml — workspace manifest
[workspace]
members = [
"core",
"cli",
"server",
"utils",
]
resolver = "2"
# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"
# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" } # local path dependencyBuild Profiles & Optimization
Profiles control how cargo builds your project. dev (default for cargo build) prioritizes compile speed. release (cargo build --release) prioritizes runtime performance. Key knobs: opt-level (0-3, 's' for size, 'z' for min size), lto (link-time optimization across crate boundaries), codegen-units (1 = best optimization but slowest compile), strip (remove symbols for smaller binaries), panic = 'abort' (disables unwinding, smaller binary). For production, use lto = true, codegen-units = 1, strip = true. Custom profiles inherit from existing ones.
# Cargo.toml
[profile.dev]
opt-level = 0 # no optimization (fast compile)
debug = true # include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # max optimization
lto = "fat" # link-time optimization across crates
codegen-units = 1 # single codegen unit (better opt, slower compile)
strip = true # strip debug symbols from binary
panic = "abort" # smaller binary, no unwinding
[profile.release.package."*"]
opt-level = 2 # optimize dependencies less than your code
# Custom profile
[profile.bench]
inherits = "release"
debug = true # keep symbols for profiling
# Usage: cargo build --release --profile benchPublishing & Documentation
Publishing to crates.io is permanent — versions can't be overwritten or deleted (only yanked, which prevents new dependents). Ensure name, version, description, license, and repository are set. cargo package validates the manifest and shows what would be published. cargo doc generates HTML documentation from /// doc comments — doc tests (code in ``` blocks) are compiled and run by cargo test. Good doc comments with examples are both documentation and tests. Use #[doc(hidden)] to hide internal items.
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token> (token from crates.io)
# 2. Check the package
# $ cargo package # creates .crate file, checks metadata
# $ cargo publish --dry-run
# 3. Publish
# $ cargo publish # uploads to crates.io (irreversible!)
# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository
# Documentation:
# $ cargo doc # generate docs for your crate + deps
# $ cargo doc --open # generate and open in browser
# $ cargo doc --no-deps # only your crate
# Doc tests run automatically:
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }Trait Objects & Dynamic Dispatch
dyn Trait Basics
Trait objects (dyn Trait) enable runtime polymorphism: a single variable can hold different concrete types implementing the same trait. The compiler generates a vtable (virtual method table) per type, and method calls go through the vtable (dynamic dispatch). This has a small runtime cost but enables heterogeneous collections. Use trait objects when the concrete type is unknown at compile time or varies.
trait Draw {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Draw for Circle {
fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
fn draw(&self) { println!("Square s={}", self.side); }
}
// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }Object Safety
A trait is object-safe (can be used as dyn Trait) only if: it has no methods returning Self, no methods taking Self by value, no generic methods, and all methods are dispatchable. Clone, Default, and From are NOT object-safe. Workarounds include using Box<Self> returns (box_clone pattern), splitting traits, or using static dispatch with enums. The compiler clearly reports object safety violations.
// Object-safe trait (can be made into dyn)
trait Animal {
fn sound(&self) -> String;
fn name(&self) -> &str;
}
// NOT object-safe: returns Self
trait Clone {
fn clone(&self) -> Self; // Self is unknown for dyn
}
// NOT object-safe: takes Self by value
trait Add {
fn add(&self, other: Self) -> Self;
}
// NOT object-safe: generic method
trait From {
fn from<T>(t: T) -> Self;
}
// Fix: use where clauses or separate traits
trait AnimalSafe {
fn sound(&self) -> String;
fn box_clone(&self) -> Box<dyn AnimalSafe>;
}Static vs Dynamic Dispatch
Static dispatch (generics with trait bounds) monomorphizes: the compiler generates a specialized version per concrete type, enabling inlining and maximum performance at the cost of binary size. Dynamic dispatch (dyn Trait) uses vtable lookups at runtime, smaller binary but slower calls (preventing inlining). Prefer static dispatch for performance-critical code; use dynamic dispatch for heterogeneous collections and plugin systems.
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.
// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
// a > b // Cannot use operators on dyn
false
}
// Trait bound (static)
fn process<T: Display>(item: &T) {
println!("{}", item);
}
// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
println!("{}", item);
}
// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
println!("{}", item);
}Trait Objects with Lifetimes
Trait objects can carry lifetime bounds: Box<dyn Trait + 'a> means the trait object (and the concrete type behind it) must live at least 'a. By default, Box<dyn Trait> implies 'static. When storing trait objects that may hold references, add the lifetime explicitly. The + syntax combines trait bounds with lifetimes. This is common in plugin systems and event handlers.
trait Parser {
fn parse(&self, input: &str) -> &str;
}
// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}
// Trait object holding references
struct Runner<'a> {
parsers: Vec<Box<dyn Parser + 'a>>,
}
impl<'a> Runner<'a> {
fn add(&mut self, p: Box<dyn Parser + 'a>) {
self.parsers.push(p);
}
}
// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}Downcasting & Any
The Any trait enables runtime type checking and downcasting. Any is automatically implemented for all 'static types. downcast_ref and downcast_mut return Option, allowing safe type recovery. This is useful for plugin systems, dynamic configurations, and heterogeneous containers. Use Any sparingly—it bypasses the type system. Prefer enums for known alternatives and generics for type-safe polymorphism.
use std::any::Any;
// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);
// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
println!("Got i32: {}", n);
}
// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
Box::new(42_i32),
Box::new("hello".to_string()),
Box::new(3.14_f64),
];
for item in &bag {
if let Some(s) = item.downcast_ref::<String>() {
println!("String: {}", s);
} else if let Some(n) = item.downcast_ref::<i32>() {
println!("i32: {}", n);
}
}Declarative Macros
macro_rules! Basics
macro_rules! defines declarative macros that match patterns and expand to code. $( $x:expr ),* matches a comma-separated list of expressions, repeated zero or more times. The $() ... * block is repeated for each match. Macros are expanded at compile time before type checking. They are hygienic: identifiers introduced by the macro do not collide with surrounding code. Use macros to reduce boilerplate (vec!, println!, format!).
macro_rules! vec_of {
( $( $x:expr ),* ) => {
{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}
};
}
let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");
// Multiple patterns
macro_rules! greet {
() => { println!("Hello!") };
($name:expr) => { println!("Hello, {}!", $name) };
($name:expr, $greeting:expr) => {
println!("{}, {}!", $greeting, $name)
};
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");Fragment Types
Macro fragments have specific types: expr (expressions), stmt (statements), ty (types), pat (patterns), ident (identifiers), tt (token trees, the most flexible), literal (literals), and more. The fragment type determines what the macro accepts and how it parses. tt is the most general—any valid token sequence. Use the most specific type possible for better error messages. The parser follows the Most-Recently-Added-Ambiguity rule.
macro_rules! items {
// $x:expr - expression (1 + 2, foo())
// $x:stmt - statement (let x = 5;)
// $x:ty - type (Vec<i32>, &str)
// $x:pat - pattern (Some(x), (a, b))
// $x:path - path (std::vec::Vec, Module::Type)
// $x:ident - identifier (foo, Bar)
// $x:literal - literal (42, "hello")
// $x:tt - token tree (anything)
// $x:block - block ({ ... })
// $x:item - item (fn, struct)
($e:expr, $t:ty, $i:ident) => {
let $i: $t = $e;
};
}
items!(42, i32, my_num);
items!("hi", &str, greeting);Repetition Patterns
Repetition in macros: $(...)* matches zero or more, $(...)+ matches one or more, $(...)? matches zero or one. Separators like commas go between matches. Nested repetitions handle multi-dimensional data (matrices, lists of lists). The $() ... * expansion block repeats for each match. Multiple variables in the same repetition must match the same number of times. Use @prefix internal rules for accumulator patterns.
macro_rules! sum {
// Zero or more: $(...)*
( $( $x:expr ),* ) => {
{
let mut total = 0;
$(
total += $x;
)*
total
}
};
// One or more: $(...)+
( first $(, $x:expr )+ ) => {
println!("First and more");
};
// With separator and count
( $( $x:expr ),+ $(; $sep:expr )? ) => {
println!("List with optional separator");
};
}
// Nested repetition
macro_rules! matrix {
( $( [ $( $x:expr ),* ] ),* ) => {
vec![ vec![ $( $x ),* ],* ]
};
}Hygiene & Exporting
Macro hygiene prevents identifier collisions: variables introduced by a macro live in their own scope and do not capture or shadow caller variables. Use $crate to refer to items in the crate where the macro is defined, ensuring it works after re-export. The @prefix convention marks internal helper rules that users should not call directly. #[macro_export] publishes the macro at the crate root.
// Export at crate root
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
// Hygienic: this T does not collide with caller's T
let _ts: std::time::Instant = std::time::Instant::now();
eprintln!("[{}] {}", "LOG", format!($($arg)*));
};
}
// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
($msg:expr) => {
$crate::Error::new($msg)
};
}
// Internal helper rule (convention: @ prefix)
macro_rules! count {
(@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
(@count $x:expr) => { 1 };
() => { 0 };
}Common Macro Patterns
Macros excel at building DSLs and reducing boilerplate. Common patterns: builder DSLs (html!, sql!), test assertions (assert_approx!), configuration (config!), and code generation (derive-like macros). Macros are hygienic and compile-time, so they have no runtime cost. Limitations: no recursion depth beyond 64, complex error messages, and difficulty with non-trivial parsing. For complex metaprogramming, use procedural macros.
// 1. Builder DSL
macro_rules! html {
($tag:ident { $($body:tt)* }) => {
format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
};
}
// 2. Test assertion
macro_rules! assert_approx {
($a:expr, $b:expr, $eps:expr) => {
assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
};
}
// 3. Configuration
macro_rules! config {
( $( $key:ident : $val:expr ),* ) => {
{
let mut m = std::collections::HashMap::new();
$(
m.insert(stringify!($key).to_string(), $val.to_string());
)*
m
}
};
}
let cfg = config! { host: "localhost", port: 8080 };Cargo & Workspaces
Workspace Setup
Workspaces group multiple crates that share dependencies and a target directory. Members are listed explicitly or via globs. [workspace.package] defines shared package metadata, inherited with .workspace = true. [workspace.dependencies] centralizes dependency versions, ensuring all crates use the same version. This prevents version conflicts and speeds up builds (single Cargo.lock). Use workspaces for multi-crate projects like CLI + library + server.
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = trueBuild Profiles
Profiles control compilation settings. dev prioritizes fast compilation (opt-level 0, debug symbols). release maximizes runtime performance (opt-level 3, LTO, single codegen unit). LTO (Link-Time Optimization) enables cross-crate inlining. panic = "abort" produces smaller binaries but disables unwinding. Per-package overrides (profile.dev.package."*") optimize dependencies even in dev. Custom profiles inherit from existing ones.
# Cargo.toml
[profile.dev]
opt-level = 0 # No optimization (fast compile)
debug = true # Include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # Max optimization
debug = false
lto = "fat" # Link-time optimization
codegen-units = 1 # Single unit (better opt, slower compile)
panic = "abort" # Smaller binary, no unwinding
strip = true # Strip symbols
[profile.dev.package."*"]
opt-level = 2 # Optimize dependencies in dev
# Custom profile
[profile.bench]
inherits = "release"
debug = true
# Usage: cargo build --release, cargo build --profile=benchFeatures & Conditional Compilation
Features enable conditional compilation. Optional dependencies become features automatically. cfg(feature = "...") gates code by feature. The default feature set is enabled unless --no-default-features is passed. Features should be additive (enabling more, not less). Use features to reduce binary size, support multiple backends, or gate experimental code. Combine with cfg_attr for conditional derive macros. Avoid mutually exclusive features.
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]
[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }
# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
serde_json::from_str(s)
}
#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
Err(Error::FeatureNotEnabled)
}Build Scripts (build.rs)
build.rs runs before compilation, enabling code generation, environment embedding, and C library linking. cargo:rerun-if-* directives control when the script reruns. Generate code with println! macros, then include! it in your crate. Common uses: embedding version info, generating bindings (bindgen), compiling protobuf/SQL schemas, and linking system libraries. Keep build scripts fast—they run on every build.
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;
fn main() {
// Tell Cargo to rerun if env changes
println!("cargo:rerun-if-env-changed=DATABASE_URL");
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("config.rs");
let db_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite://default.db".to_string());
// Generate Rust code at build time
fs::write(&dest, format!(
"pub const DATABASE_URL: &str = \"{}\";",
db_url
)).unwrap();
// Link a C library
println!("cargo:rustc-link-lib=static=mylib");
println!("cargo:rustc-link-search=native=/usr/local/lib");
}
// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));Publishing & Documentation
cargo publish uploads a crate to crates.io. Use --dry-run to verify before publishing. cargo doc generates HTML documentation from doc comments (///). Code blocks in doc comments are tested with cargo test --doc. Include Examples, Panics, and Errors sections. Metadata (description, repository, keywords) improves discoverability. Once published, a version cannot be reused or deleted—use yank to prevent new projects from depending on it.
# Publish to crates.io
cargo login <token> # One-time authentication
cargo publish # Publish current crate
# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package # Inspect the .crate file
# Documentation
cargo doc # Generate docs
cargo doc --open # Generate and open
cargo doc --no-deps # Only this crate
# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]Procedural Macros
Macro Types
Procedural macros generate Rust code at compile time, operating on token streams. Three types: function-like (custom!()), derive (#[derive(Custom)]), and attribute (#[custom]). They require a separate crate with proc-macro = true. The syn crate parses Rust syntax, quote generates code, and proc-macro2 enables testing. Proc-macros are powerful but complex—use them for derive macros, DSLs, and code generation that declarative macros cannot handle.
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]
// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true
// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> i32 { 42 }".parse().unwrap()
}
// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }Derive Macro
Derive macros add trait implementations to types annotated with #[derive(MyMacro)]. syn parses the input into a DeriveInput AST. quote! generates code with # interpolation for variables. Helper attributes (attributes(hello)) allow customization on fields or variants. Common derive macros: Debug, Clone, Serialize, Deserialize. The generated code is appended to the module, so it cannot modify the original type.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[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 expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
expanded.into()
}
// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello(); // "Hello from Pancakes!"
// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }Attribute Macro
Attribute macros (#[my_attr]) transform the item they annotate, potentially replacing it entirely. They receive both the attribute arguments and the annotated item. Common uses: logging, caching, async wrappers (#[tokio::main]), and routing (#[get("/path")]). Attribute macros can change the item's signature, add code, or generate additional items. They are more flexible than derive macros but harder to use correctly.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let expanded = quote! {
fn #fn_name() {
println!("Calling {}", stringify!(#fn_name));
let __result = (|| #fn_block)();
println!("Finished {}", stringify!(#fn_name));
__result
}
};
expanded.into()
}
// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }Function-Like Macro
Function-like procedural macros (my_macro!()) accept arbitrary token streams, enabling custom DSLs. Implement Parse to define accepted syntax. The macro can validate, transform, or generate code based on the input. Common uses: SQL queries (sqlx), HTML templates (maud), and configuration DSLs. Unlike declarative macros, proc-macros can parse complex syntax and perform arbitrary computation at compile time. Keep them fast to avoid slow builds.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};
// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
query: String,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> syn::Result<Self> {
let query = input.to_string();
Ok(SqlQuery { query })
}
}
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let SqlQuery { query } = parse_macro_input!(input as SqlQuery);
let expanded = quote! {
{
static QUERY: &str = #query;
// Compile-time SQL validation could go here
QUERY
}
};
expanded.into()
}Testing & Debugging
Testing proc-macros: trybuild runs UI tests comparing compiler output (success or error messages) against expected files. For derive macros, test that the generated code compiles and behaves correctly. Debug with eprintln! (printed during compilation) or cargo expand (shows expanded macro output). Macro development is iterative: write the macro, use cargo expand to inspect output, fix issues. Document the macro's syntax and supported features clearly.
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"
// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }
// tests/ui/my_macro.stderr - expected error
// error: ...
// Test runner
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/pass_*.rs");
t.compile_fail("tests/ui/fail_*.rs");
}
// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
eprintln!("Input tokens: {}", input);
let ast = syn::parse2(input.clone().into()).unwrap();
eprintln!("Parsed AST: {:#?}", ast);
TokenStream::new()
}Macros
macro_rules!
macro_rules! defines declarative macros. $x is a capture, expr matches expressions. $(...)* repeats. Macros are expanded at compile time. Useful for reducing boilerplate. The standard vec! macro works similarly.
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$(v.push($x);)*
v
}};
}
let nums = vec_of!(1, 2, 3);Procedural Macros
Procedural macros generate code at compile time. Three types: derive (#[derive(Debug)]), attribute (#[my_attr]), function-like (my_macro!). More powerful than macro_rules! but require a separate crate. Used by serde, tokio, and diesel.
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Generate impl HelloMacro for the type
// Returns new TokenStream
}Common Macros
Built-in macros: println!/format! for output, vec! for vectors, assert!/assert_eq! for tests, dbg! for debugging, todo!/unreachable! for control flow. All are macro_rules! based. dbg! returns the value for chaining.
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable); // Debug print
todo!("Not implemented");
unreachable!();Macro Hygiene
Rust macros are hygienic: identifiers introduced by the macro do not conflict with identifiers in the calling scope. This prevents subtle bugs. The temp inside the macro is different from the outer temp. Declarative macros are always hygienic.
macro_rules! swap {
($a:expr, $b:expr) => {
let temp = $a;
$a = $b;
$b = temp;
};
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x); // Works correctlyRepetition
Repetition in macros: $(...)* matches zero or more, $(...)+ one or more. The separator (comma) can be specified. $x captures each value. Useful for variadic-like functions. The standard println! uses this for multiple arguments.
macro_rules! sum {
($($x:expr),*) => {
0 $(+ $x)*
};
}
let total = sum!(1, 2, 3, 4); // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing commaAsync Deep Dive
async/await
async fn returns a Future. .await suspends until the future is ready. Futures are lazy: nothing runs until awaited. The compiler transforms async fn into a state machine. Use tokio or async-std runtime to execute.
async fn fetch_data() -> String {
// Simulate async work
String::from("data")
}
async fn process() {
let data = fetch_data().await;
println!("{}", data);
}Tokio Runtime
tokio::main enables async main. spawn creates a task (like a green thread). join! waits for multiple futures concurrently. Tokio provides I/O, timers, and scheduling. The most popular async runtime in Rust.
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async { work1().await });
let task2 = tokio::spawn(async { work2().await });
let (r1, r2) = tokio::join!(task1, task2);
}Channels (async)
mpsc (multi-producer, single-consumer) channels enable async communication. send/recv are async. The channel has a buffer (32 messages). When all senders drop, recv returns None. Useful for producer-consumer patterns.
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}Select
select! waits for the first of multiple futures to complete. Other futures are dropped. Useful for timeouts and racing operations. The pattern is common in network servers. Each branch can have a guard pattern.
tokio::select! {
result = task1 => {
println!("Task1 done: {:?}", result);
}
result = task2 => {
println!("Task2 done: {:?}", result);
}
_ = tokio::time::sleep(Duration::from_secs(5)) => {
println!("Timeout");
}
}Stream
Stream is the async equivalent of Iterator. next().await gets the next item. StreamExt provides map, filter, for_each. Useful for processing chunks of data from network or files. The async-stream crate simplifies creating streams.
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
println!("{}", x);
}).await;Cargo & Crates
Cargo.toml
Cargo.toml is the manifest file. [package] defines metadata. [dependencies] lists external crates. Features enable optional functionality. [dev-dependencies] are for tests only. Edition 2021 is the latest stable. Versions use semver.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1"Cargo Commands
cargo new creates a binary project (--lib for library). build compiles to target/. --release enables optimizations. check is faster than build (no codegen). clippy catches common mistakes. fmt formats code. doc generates HTML documentation. add inserts a dependency.
cargo new myapp # Create new project
cargo build # Compile
cargo build --release # Optimized build
cargo run # Build and run
cargo test # Run tests
cargo check # Fast type-check
cargo fmt # Format code
cargo clippy # Lint
cargo doc --open # Generate docs
cargo add serde # Add dependencyWorkspaces
Workspaces group multiple crates that share a target directory and Cargo.lock. Members are individual crates. [workspace.dependencies] centralizes dependency versions. Each crate references them with workspace = true. Faster builds due to shared compilation. Used by large projects like rust-analyzer.
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]
# Shared dependencies
[workspace.dependencies]
serde = "1.0"
# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }Features
Features enable conditional compilation. default features are enabled unless --no-default-features. cfg(feature = ...) gates code. dep: syntax in dependencies avoids feature unification. Useful for optional functionality and platform-specific code. Crates can expose features to consumers.
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]
# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }Publishing
crates.io is the Rust package registry. cargo login authenticates. --dry-run catches issues. Once published, a version cannot be republished (yank only hides from search). Follow semver: patch for fixes, minor for features, major for breaking changes. README and license are required.
# Login (one time)
cargo login <token>
# Check before publishing
cargo publish --dry-run
# Publish to crates.io
cargo publish
# Version bumping
cargo bump patch # 0.1.0 -> 0.1.1
cargo bump minor # 0.1.0 -> 0.2.0
cargo bump major # 0.1.0 -> 1.0.0Testing Rust
Unit Tests
Tests live in a #[cfg(test)] module. use super::* imports the parent. #[test] marks test functions. assert_eq! checks equality. #[should_panic] expects a panic. Tests run with cargo test. Unit tests are colocated with code. The cfg(test) attribute ensures tests are not compiled in release builds.
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic]
fn test_panic() {
panic!("expected");
}
}Integration Tests
Integration tests live in the tests/ directory. Each file is compiled as a separate crate. They can only test the public API. Useful for end-to-end testing. Run specific tests with --test <name>. Integration tests are slower to compile but test the real interface.
// tests/integration_test.rs
use myapp::add;
#[test]
fn test_add_integration() {
assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crateTest Organization
#[ignore] skips a test unless --ignored is passed. Filter tests by name pattern. --nocapture shows println! output. Tests run in parallel by default. Use --test-threads=1 for sequential. Custom harnesses can replace the default test runner. Useful for benchmarks and property tests.
#[test]
fn it_works() { /* ... */ }
// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }
// Run only ignored tests
cargo test -- --ignored
// Filter by name
cargo test test_add
// Show output
cargo test -- --nocaptureAssertions
assert! checks a boolean. assert_eq!/assert_ne! compare values with debug output on failure. Custom messages help debugging. For floating point, use approx crate. For partial equality, implement PartialEq. The debug output shows both values when assertion fails.
assert!(true); // Boolean
assert_eq!(2 + 2, 4); // Equality
assert_ne!(3, 4); // Inequality
assert!(x > 0, "x must be positive"); // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);Property Testing
proptest generates random inputs to find failing cases. Strategies (a in range) define input generators. prop_assert! reports failures with minimal counterexamples. Shrinking finds the smallest failing input. Better than hand-written tests for edge cases. Similar to QuickCheck in Haskell.
// Cargo.toml: proptest = "1"
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
prop_assert_eq!(add(a, b), add(b, a));
}
#[test]
fn test_string_len(s in ".{0,100}") {
prop_assert!(s.len() <= 100);
}
}Related Rust snippets
Copy-paste ready code for common tasks.
Struct with Methods
Define a Rust struct and implement methods with self and Self.
Ownership
Rust ownership system.
Borrowing and References
References and mutable borrows.
Lifetimes
Explicit lifetime annotations.
Trait
Define and implement traits.
Generics
Generic functions and structs.
Enum
Enums and Option.
Pattern Matching
match and destructuring.
Error Handling
Result and the ? operator.
Iterator
Iterator adapters and consumers.
Closures
Closures and Fn traits.
Module System
Modules, paths, and visibility.
Concurrent Programming
Threads and channels.
Smart Pointers
Box, Rc, RefCell.
Macros
Declarative and procedural macros.
Unsafe Rust
Unsafe operations.
Was this helpful?