The Rust Programming Language
by Steve Klabnik, Carol Nichols, and Chris Krycho, with contributions from the Rust Community
This version of the text assumes you’re using Rust 1.85.0 (released 2025-02-17)
or later with edition = "2024"
in the Cargo.toml file of all projects to
configure them to use Rust 2024 edition idioms. See the “Installation” section
of Chapter 1 to install or update Rust.
The HTML format is available online at
https://doc.rust-lang.org/stable/book/
and offline with installations of Rust made with rustup
; run rustup doc --book
to open.
Several community translations are also available.
This text is available in paperback and ebook format from No Starch Press.
🚨 Want a more interactive learning experience? Try out a different version of the Rust Book, featuring: quizzes, highlighting, visualizations, and more: https://rust-book.cs.brown.edu
Foreword
It wasn’t always so clear, but the Rust programming language is fundamentally about empowerment: no matter what kind of code you are writing now, Rust empowers you to reach farther, to program with confidence in a wider variety of domains than you did before.
Take, for example, “systems-level” work that deals with low-level details of memory management, data representation, and concurrency. Traditionally, this realm of programming is seen as arcane, accessible only to a select few who have devoted the necessary years learning to avoid its infamous pitfalls. And even those who practice it do so with caution, lest their code be open to exploits, crashes, or corruption.
Rust breaks down these barriers by eliminating the old pitfalls and providing a friendly, polished set of tools to help you along the way. Programmers who need to “dip down” into lower-level control can do so with Rust, without taking on the customary risk of crashes or security holes, and without having to learn the fine points of a fickle toolchain. Better yet, the language is designed to guide you naturally towards reliable code that is efficient in terms of speed and memory usage.
Programmers who are already working with low-level code can use Rust to raise their ambitions. For example, introducing parallelism in Rust is a relatively low-risk operation: the compiler will catch the classical mistakes for you. And you can tackle more aggressive optimizations in your code with the confidence that you won’t accidentally introduce crashes or vulnerabilities.
But Rust isn’t limited to low-level systems programming. It’s expressive and ergonomic enough to make CLI apps, web servers, and many other kinds of code quite pleasant to write — you’ll find simple examples of both later in the book. Working with Rust allows you to build skills that transfer from one domain to another; you can learn Rust by writing a web app, then apply those same skills to target your Raspberry Pi.
This book fully embraces the potential of Rust to empower its users. It’s a friendly and approachable text intended to help you level up not just your knowledge of Rust, but also your reach and confidence as a programmer in general. So dive in, get ready to learn—and welcome to the Rust community!
— Nicholas Matsakis and Aaron Turon
Introduction
Note: This edition of the book is the same as The Rust Programming Language available in print and ebook format from No Starch Press.
The Rust Programming Language provides a systems programming approach that combines memory safety with zero-cost abstractions. Unlike TypeScript’s runtime type checking or Node.js’s garbage-collected memory management, Rust enforces safety at compile time without runtime overhead.
Rust addresses common pain points in systems programming: memory leaks, race conditions, and undefined behavior—issues that higher-level languages like TypeScript abstract away but become critical in systems-level development.
Why Rust for Experienced Engineers
Memory Safety Without Performance Cost
Rust’s ownership system eliminates entire classes of bugs common in C/C++ while matching their performance. Think of it as compile-time guarantees for what you’d normally handle with careful code reviews and runtime checks in other languages.
Modern Tooling Ecosystem
Rust’s development experience rivals modern web development:
- Cargo: Dependency management and build system (similar to npm/yarn but for systems programming)
- Rustfmt: Code formatting (like Prettier)
- rust-analyzer: LSP implementation providing IDE features comparable to TypeScript’s language server
- Clippy: Static analysis tool beyond basic linting
Concurrency Without Data Races
Rust’s ownership model prevents data races at compile time—eliminating a major source of production bugs in concurrent systems. This is particularly relevant coming from Node.js’s event loop model where race conditions are less common but not impossible.
Production Adoption
Major companies use Rust for performance-critical infrastructure: Discord (voice/video), Dropbox (storage), Facebook (source control), Microsoft (Windows components), and Mozilla (Firefox). These are systems where TypeScript/Node.js wouldn’t be appropriate due to performance requirements.
Target Use Cases
- CLI tools: Better performance than Node.js scripts with similar ergonomics
- Web services: Lower latency and memory usage than Node.js/TypeScript backends
- Systems programming: Device drivers, embedded systems, OS components
- Performance-critical components: Computational algorithms, real-time systems
- Infrastructure tooling: Networking tools, databases, container runtimes
Book Structure
This book assumes software engineering experience. Chapters are divided into concept and project chapters:
Core Concepts (Chapters 1-11):
- Ch 1: Installation and tooling setup
- Ch 2: Hands-on number guessing game (quick practical intro)
- Ch 3: Basic syntax and types
- Ch 4: Ownership system (Rust’s key differentiator—most important chapter)
- Ch 5-6: Structs and enums (similar to TypeScript interfaces/unions but with more capabilities)
- Ch 7: Module system (package organization)
- Ch 8: Collections (Vec, HashMap, etc.)
- Ch 9: Error handling (Result types—different from exceptions)
- Ch 10: Generics and traits (similar to TypeScript generics but more powerful)
- Ch 11: Testing
Applied Concepts (Chapters 12-21):
- Ch 12: CLI tool project (grep implementation)
- Ch 13: Functional programming features (closures, iterators)
- Ch 14: Cargo ecosystem and publishing
- Ch 15: Smart pointers (memory management primitives)
- Ch 16: Concurrency (threads, channels, async)
- Ch 17: Async/await (different from JavaScript promises but similar concepts)
- Ch 18: Object-oriented patterns in Rust
- Ch 19: Pattern matching (more powerful than switch statements)
- Ch 20: Advanced features (unsafe code, macros)
- Ch 21: Multithreaded web server project
Recommended approach: Read sequentially through Chapter 4 (ownership is fundamental), then adapt based on your specific interests.
Error Messages and Learning Process
Rust’s compiler provides detailed error messages with suggested fixes. Unlike TypeScript’s sometimes cryptic errors, Rust errors are designed to be educational. The compiler often suggests exactly what to change.
Code examples use Ferris icons to indicate expected behavior:
Icon | Meaning |
---|---|
Compilation failure (intentional for learning) | |
Runtime panic (controlled failure) | |
Runs but produces incorrect results |
Source Code
The source files from which this book is generated can be found on GitHub.
Getting Started
This chapter covers essential setup and tooling for Rust development:
- Installation: Rust toolchain via
rustup
- Hello World: Basic compilation and execution
- Cargo: Package manager and build system (Rust’s equivalent to npm/yarn)
Installation
Install Rust via rustup
, the official toolchain manager that handles Rust versions, targets, and associated tools.
Install rustup
Unix/macOS:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Windows: Download and run the installer from https://www.rust-lang.org/tools/install
The installer includes:
rustc
(compiler)cargo
(package manager/build system)rustfmt
(code formatter)clippy
(linter)- Local documentation
Dependencies
macOS:
$ xcode-select --install
Ubuntu/Debian:
$ sudo apt install build-essential
Windows: Visual Studio Build Tools or Visual Studio with C++ development tools.
Verification
$ rustc --version
$ cargo --version
Management Commands
$ rustup update # Update to latest stable
$ rustup self uninstall # Remove rustup and Rust
$ rustup doc # Open local documentation
IDE Setup
Recommended: VS Code with rust-analyzer
extension (provides LSP features comparable to TypeScript’s language server).
Offline Development
For projects requiring external dependencies:
$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.8.5 trpl@0.2.0
Use --offline
flag with cargo commands to use cached dependencies.
Hello, World!
Direct Compilation
Create main.rs
:
fn main() { println!("Hello, world!"); }
Compile and run:
$ rustc main.rs
$ ./main # Linux/macOS
$ .\main.exe # Windows
Program Structure
fn main() { // Entry point (like main() in C) println!("Hello, world!"); // Macro call (note the !) }
Key differences from TypeScript/Node.js:
- Ahead-of-time compilation: Unlike Node.js’s JIT, Rust compiles to native binaries
- Macros:
println!
is a macro (indicated by!
), not a function - No runtime: Unlike Node.js, compiled binaries run without a runtime environment
Compilation Model
Unlike interpreted languages (JavaScript) or VM languages with JIT (Java), Rust uses ahead-of-time compilation:
- Source → Binary: Direct compilation to machine code
- Zero dependencies: Binaries run without Rust installation
- Static linking: All dependencies bundled in the executable
This is similar to Go’s compilation model but with stronger memory safety guarantees.
For larger projects, use Cargo instead of direct rustc
compilation.
Hello, Cargo!
Cargo is Rust’s integrated build system and package manager—equivalent to npm/yarn but with built-in compilation, testing, and project management.
Project Creation
$ cargo new hello_cargo
$ cd hello_cargo
Generated structure:
hello_cargo/
├── Cargo.toml # Package manifest (like package.json)
├── .gitignore # Git ignore file
└── src/
└── main.rs # Source code
Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024" # Rust edition (language version)
[dependencies] # Dependencies (like package.json dependencies)
vs package.json:
- TOML format instead of JSON
edition
field specifies Rust language features- Dependencies are automatically compiled, not interpreted
Essential Commands
$ cargo build # Compile (creates target/debug/)
$ cargo run # Build + execute
$ cargo check # Type check without generating binary
$ cargo build --release # Optimized build (target/release/)
$ cargo test # Run tests
$ cargo doc --open # Generate and open documentation
Development Workflow
Debug builds (default):
- Fast compilation
- Includes debugging symbols
- No optimizations
- Located in
target/debug/
Release builds:
- Slower compilation
- Full optimizations
- Smaller binaries
- Located in
target/release/
Key Differences from npm/yarn
Feature | npm/yarn | Cargo |
---|---|---|
Installation | Runtime dependency resolution | Compile-time linking |
Execution | Requires Node.js runtime | Standalone binary |
Versioning | Semantic versioning | Semantic versioning + edition system |
Lock file | package-lock.json/yarn.lock | Cargo.lock |
Scripts | package.json scripts | Built-in commands |
Cargo.lock
Automatically generated dependency lock file (like package-lock.json). Ensures reproducible builds across environments. Commit to version control for applications, not for libraries.
Integration with Existing Projects
$ git clone <repo>
$ cd <project>
$ cargo build # Downloads deps, compiles everything
No separate dependency installation step—Cargo handles everything in the build process.
Cargo becomes essential for:
- Dependency management
- Multi-file projects
- Testing and documentation
- Publishing to crates.io (Rust’s package registry)
Programming a Guessing Game
This chapter demonstrates core Rust concepts through a practical project: a CLI guessing game. Unlike Node.js scripts, this compiles to a self-contained binary with no runtime dependencies.
Key concepts covered:
- Variables and mutability (
let
vslet mut
) - Pattern matching (
match
expressions) - Error handling (
Result
type vs exceptions) - External dependencies (Cargo vs npm)
- Type system (compile-time safety vs runtime checks)
Project Setup
$ cargo new guessing_game
$ cd guessing_game
Generated structure matches standard Rust conventions:
# Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
Input Handling
Unlike Node.js’s readline
or process.stdin
, Rust uses explicit imports and error handling:
use std::io; fn main() { println!("Guess the number!"); println!("Please input your guess."); let mut guess = String::new(); // Mutable variable (explicit) io::stdin() .read_line(&mut guess) // Borrows mutable reference .expect("Failed to read line"); // Error handling (no try/catch) println!("You guessed: {guess}"); }
Key differences from TypeScript/Node.js:
- Explicit mutability:
let mut
vslet
(immutable by default) - Borrowing:
&mut guess
passes a mutable reference, not the value - No exceptions:
Result
type forces explicit error handling - No garbage collection: Memory managed through ownership system
External Dependencies
Add the rand
crate to Cargo.toml
:
[dependencies]
rand = "0.8.5"
vs npm/package.json:
- Dependencies compile into the binary (no
node_modules
at runtime) - Semantic versioning with automatic compatibility resolution
Cargo.lock
ensures reproducible builds (like package-lock.json)
$ cargo build # Downloads, compiles, and links dependencies
Random Number Generation
use rand::Rng; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); // Unlike Math.random(), this is cryptographically secure by default println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); println!("You guessed: {guess}"); println!("The secret number was: {secret_number}"); }
Pattern Matching and Comparison
Rust’s match
is more powerful than TypeScript’s switch
:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { let secret_number = rand::thread_rng().gen_range(1..=100); println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); // Type conversion with error handling let guess: u32 = guess.trim().parse().expect("Please type a number!"); println!("You guessed: {guess}"); // Pattern matching (exhaustive) match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
Key concepts:
- Type conversion:
parse()
returnsResult<T, E>
, notT | undefined
- Exhaustive matching: Compiler ensures all cases are handled
- No type coercion: Must explicitly convert
String
tou32
Game Loop with Robust Error Handling
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { // Infinite loop (like while(true) but more idiomatic) println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); // Graceful error handling instead of throwing exceptions let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Please type a number!"); continue; // Skip to next iteration } }; println!("You guessed: {guess}"); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; // Exit loop } } } }
Final Implementation
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("You guessed: {guess}"); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
Key Rust Concepts Demonstrated
Memory Safety:
- No null pointer dereferences (no
null
orundefined
) - Borrowing prevents data races at compile time
- No manual memory management needed
Type System:
- Static typing with inference (like TypeScript but stricter)
Result<T, E>
for error handling (instead of exceptions)- Pattern matching ensures exhaustive case handling
Performance:
- Zero-cost abstractions (no runtime overhead)
- Compiled binary runs without runtime environment
- No garbage collection pauses
Error Handling:
- Explicit error handling with
Result
andOption
types match
expressions for control flow- Compile-time guarantees about error handling
This approach differs significantly from TypeScript/Node.js patterns and demonstrates Rust’s emphasis on safety, performance, and explicit error handling.
Common Programming Concepts
This chapter covers Rust’s implementation of core programming concepts: variables, types, functions, comments, and control flow. While these concepts exist in most languages, Rust’s approach has specific characteristics around ownership, memory safety, and type strictness.
Keywords
Rust reserves specific keywords that cannot be used as identifiers. See Appendix A for the complete list.
Variables and Mutability
Variables are immutable by default in Rust. This design choice enforces memory safety and prevents data races in concurrent contexts.
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
This produces a compile-time error:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
To allow mutation, use the mut
keyword:
fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
Constants
Constants differ from immutable variables in several ways:
- Always immutable (no
mut
allowed) - Declared with
const
keyword - Type annotation required
- Must be set to compile-time constant expressions
- Can be declared in any scope, including global
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Constants use SCREAMING_SNAKE_CASE
by convention and are valid for the entire program duration within their scope.
Shadowing
Rust allows variable shadowing - declaring a new variable with the same name:
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x is: {x}"); }
Output:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Shadowing differs from mutation:
- Creates a new variable (enables type changes)
- Requires
let
keyword - Variable remains immutable after transformations
fn main() { let spaces = " "; let spaces = spaces.len(); }
This is invalid with mut
:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
Error:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
Data Types
Rust is statically typed - all variable types must be known at compile time. The compiler can usually infer types, but explicit annotations are required when multiple types are possible:
let guess: u32 = "42".parse().expect("Not a number!");
Without the type annotation:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
Scalar Types
Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.
Integer Types
Table 3-1: Integer Types in Rust
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Signed variants store numbers from −(2n − 1) to 2n − 1 − 1, unsigned from 0 to 2n − 1. isize
and usize
depend on target architecture (64-bit on 64-bit systems).
Integer literals:
Table 3-2: Integer Literals in Rust
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
Default integer type is i32
. Use isize
/usize
for collection indexing.
Integer Overflow
In debug mode, integer overflow causes panics. In release mode (--release
), Rust performs two’s complement wrapping. Handle overflow explicitly with:
wrapping_*
methods (e.g.,wrapping_add
)checked_*
methods (returnOption
)overflowing_*
methods (return value + overflow boolean)saturating_*
methods (clamp to min/max)
Floating-Point Types
Two floating-point types: f32
and f64
(default). Both are signed and follow IEEE-754 standard.
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
Numeric Operations
Standard mathematical operations: +
, -
, *
, /
, %
. Integer division truncates toward zero.
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results in -1 // remainder let remainder = 43 % 5; }
Boolean Type
bool
type with values true
and false
. One byte in size.
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
Character Type
char
type represents Unicode scalar values (4 bytes). Use single quotes for char
literals, double quotes for strings.
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
Valid Unicode scalar values: U+0000
to U+D7FF
and U+E000
to U+10FFFF
.
Compound Types
Tuple Type
Fixed-length collection of values with heterogeneous types:
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
Access elements by destructuring:
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }
Or by index:
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }
Empty tuple ()
is called unit and represents an empty value or return type.
Array Type
Fixed-length collection of homogeneous types, allocated on the stack:
fn main() { let a = [1, 2, 3, 4, 5]; }
Type annotation syntax:
let a: [i32; 5] = [1, 2, 3, 4, 5];
Initialize with repeated values:
let a = [3; 5]; // [3, 3, 3, 3, 3]
Use Vec<T>
for dynamic arrays. Arrays are better when size is known at compile time.
Array Access
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
Out-of-bounds access causes runtime panic:
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
Output:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Rust prevents buffer overflows by bounds checking at runtime.
Functions
Functions in Rust use snake_case convention and require explicit type annotations for parameters (unlike TypeScript’s optional inference).
Filename: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }
Functions are defined with fn
and can be declared in any order within scope. The compiler doesn’t care about declaration order, similar to hoisting in JavaScript.
Parameters
Unlike TypeScript, parameter types are mandatory:
Filename: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }
Multiple parameters:
Filename: src/main.rs
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
Statements vs Expressions
Critical distinction for Rust (unlike TypeScript where nearly everything is an expression):
- Statements: Perform actions, return no value (
let y = 6;
) - Expressions: Evaluate to values (
5 + 6
, function calls, blocks)
fn main() { let y = 6; }
This won’t compile (unlike JavaScript/TypeScript):
Filename: src/main.rs
fn main() {
let x = (let y = 6);
}
Blocks are expressions:
Filename: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }
The block evaluates to 4
because x + 1
lacks a semicolon. Adding a semicolon converts an expression to a statement.
Return Values
Return type annotation required after ->
. Functions return the last expression implicitly (no return
keyword needed):
Filename: src/main.rs
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}"); }
Parameter and return example:
Filename: src/main.rs
fn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 }
Critical: Adding a semicolon to the return expression breaks the return:
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
This fails because statements return ()
(unit type), not the expected i32
. The semicolon behavior is fundamentally different from TypeScript where semicolons are purely syntactic.
Comments
Rust uses //
for line comments and /* */
for block comments:
// Line comment /* Block comment */
Multi-line comments require //
on each line:
// Multi-line comment // continues here
Comments can be placed at line end or above code:
fn main() { let lucky_number = 7; // I'm feeling lucky today }
fn main() { // I'm feeling lucky today let lucky_number = 7; }
Documentation comments (///
and //!
) are covered in Chapter 14.
Control Flow
if
Expressions
if
is an expression in Rust, not just a statement:
fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }
Conditions must be bool
- no automatic type conversion:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
Error:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Explicit comparison required:
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }
Multiple Conditions
fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }
if
in let
Statements
Since if
is an expression, it can assign values:
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
All branches must return the same type:
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
Error:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Loops
loop
Infinite loop until explicit break
:
fn main() {
loop {
println!("again!");
}
}
Return values from loops:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
Loop Labels
Label loops for nested control:
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }
Output:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while
Conditional loops:
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
for
Preferred for collection iteration. Compare while
approach:
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
With safer for
loop:
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
Using ranges:
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
for
loops are preferred in Rust for safety and performance - no bounds checking overhead and elimination of index errors.
Understanding Ownership
Ownership is Rust’s core memory management system that enables memory safety without garbage collection. This chapter covers ownership, borrowing, slices, and memory layout—concepts that differentiate Rust from garbage-collected languages like JavaScript/TypeScript.
What Is Ownership?
Ownership is Rust’s memory management system with compile-time rules that prevent memory safety issues without runtime overhead. Unlike garbage-collected languages, Rust determines memory cleanup at compile time.
Stack vs Heap
Stack: LIFO structure for fixed-size data. Fast allocation/deallocation, automatic cleanup. Heap: Runtime allocation for dynamic-size data. Returns pointer, requires explicit management.
Stack access is faster due to locality. Heap access requires pointer dereferencing. Ownership primarily manages heap data.
Ownership Rules
- Each value has exactly one owner
- Only one owner at a time
- Value is dropped when owner goes out of scope
Variable Scope
let s = "hello";
String literals are immutable and stack-allocated. The variable s
is valid from declaration until scope end.
fn main() { { // s is not valid here, since it's not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid }
The String
Type
String
manages heap-allocated, mutable text data:
let s = String::from("hello");
Unlike string literals, String
can be mutated and has dynamic size:
fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{s}"); // this will print `hello, world!` }
Memory and Allocation
String
requires heap allocation for dynamic content. Rust automatically calls drop
when the owner goes out of scope—similar to RAII in C++.
fn main() { { let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no // longer valid }
Variables and Data Interacting with Move
Simple stack values are copied:
fn main() { let x = 5; let y = x; }
Heap-allocated values are moved:
fn main() { let s1 = String::from("hello"); let s2 = s1; }
String
contains three parts on the stack: pointer, length, capacity. Assignment copies these metadata but not heap data. Rust invalidates the original variable to prevent double-free errors.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
Error: value used after move. This prevents the double-free memory safety issue.
Scope and Assignment
Assigning a new value immediately drops the previous value:
fn main() { let mut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!"); }
Variables and Data Interacting with Clone
For explicit deep copying:
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
clone()
is expensive—it copies heap data.
Stack-Only Data: Copy
Types implementing Copy
trait are duplicated instead of moved:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
Copy
types include:
- Integer types (
u32
,i32
, etc.) - Boolean (
bool
) - Floating-point types (
f64
) - Character (
char
) - Tuples of
Copy
types
Types with Drop
trait cannot implement Copy
.
Ownership and Functions
Function calls behave like assignment—values are moved or copied:
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // Because i32 implements the Copy trait, // x does NOT move into the function, // so it's okay to use x afterward. } // Here, x goes out of scope, then s. However, because s's value was moved, // nothing special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
Return Values and Scope
Functions can transfer ownership via return values:
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns a String. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
Returning ownership for every function parameter is verbose. Rust provides references for this use case:
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
References and Borrowing
References allow using values without taking ownership. A reference is guaranteed to point to a valid value for its lifetime.
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
The &
syntax creates a reference. &s1
refers to s1
without owning it. When the reference goes out of scope, the value isn’t dropped because the reference doesn’t own it.
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because s does not have ownership of what // it refers to, the String is not dropped.
Creating a reference is called borrowing. References are immutable by default:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Mutable References
Use &mut
for mutable references:
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
Borrowing rules:
- Only one mutable reference per value at a time
- Cannot mix mutable and immutable references in overlapping scopes
This prevents data races at compile time:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
Multiple mutable references are allowed in separate scopes:
fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
Cannot combine mutable and immutable references:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
Reference scopes end at their last usage, not at the end of the block:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // Variables r1 and r2 will not be used after this point. let r3 = &mut s; // no problem println!("{r3}"); }
Dangling References
Rust prevents dangling references through compile-time checks:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Error: function returns a borrowed value with no value to borrow from.
Solution—return the value directly:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
The Rules of References
- At any time: either one mutable reference OR any number of immutable references
- References must always be valid
The Slice Type
Slices reference contiguous sequences of elements in collections without taking ownership.
Problem: Write a function returning the first word from a space-separated string.
Without slices, we might return an index:
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
Issues with index-based approach:
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but s no longer has any content that we // could meaningfully use with the value 5, so word is now totally invalid! }
The index becomes invalid after s.clear()
, but the compiler can’t catch this error.
String Slices
A string slice (&str
) references a portion of a String
:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
Slice syntax: &s[start..end]
where end
is exclusive.
Shortcuts:
let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; // Same as above let slice = &s[3..len]; let slice = &s[3..]; // Same as above let slice = &s[0..len]; let slice = &s[..]; // Entire string
Improved first_word
using slices:
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
Slices prevent the previous bug by enforcing borrowing rules:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {word}");
}
Compile error: cannot borrow as mutable while immutable reference exists.
String Literals as Slices
String literals are &str
types—slices pointing to binary data:
let s = "Hello, world!"; // s is &str
String Slices as Parameters
Better function signature accepts both &String
and &str
:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
Usage:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // `first_word` works on slices of `String`s, whether partial or whole. let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also works on references to `String`s, which are equivalent // to whole slices of `String`s. let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` works on slices of string literals, whether partial or // whole. let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
Other Slices
Array slices work similarly:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // Type: &[i32] assert_eq!(slice, &[2, 3]);
Summary
Ownership, borrowing, and slices provide compile-time memory safety without runtime overhead. These concepts affect many other Rust features throughout the language.
Using Structs to Structure Related Data
Structs are custom data types that group related values with named fields. Unlike tuples, structs provide semantic meaning through field names and don’t rely on positional access.
This chapter covers:
- Struct definition and instantiation syntax
- Associated functions and methods
- Ownership considerations with struct data
Structs and enums form the foundation of Rust’s type system, enabling compile-time guarantees and zero-cost abstractions.
Defining and Instantiating Structs
Structs provide named fields for data grouping, offering better semantics than tuples when field order shouldn’t matter.
Basic Syntax
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() {}
Instantiation uses key-value pairs in any order:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; }
Access and mutation require the entire instance to be mutable:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let mut user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
Functions can return struct instances as the last expression:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username: username, email: email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("someone@example.com"), String::from("someusername123"), ); }
Field Init Shorthand
When parameter names match field names, use shorthand syntax:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username, email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("someone@example.com"), String::from("someusername123"), ); }
Struct Update Syntax
Create instances from existing ones with selective field updates:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; }
The ..
operator copies remaining fields:
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), ..user1 }; }
Ownership Note: Struct update uses move semantics. If any moved field contains non-Copy data (like String
), the original instance becomes unusable. Fields implementing Copy
(like primitives) are copied, not moved.
Tuple Structs
Tuple structs provide struct semantics with positional fields:
struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
Different tuple struct types are incompatible even with identical field types. Access uses dot notation with indices: origin.0
, origin.1
, etc.
Unit-Like Structs
Structs without fields are useful for trait implementations without data:
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
Ownership in Structs
Structs typically own their data using owned types like String
rather than references like &str
. References in structs require lifetime parameters:
struct User {
active: bool,
username: &str, // Error: missing lifetime specifier
email: &str, // Error: missing lifetime specifier
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "someone@example.com",
sign_in_count: 1,
};
}
Compiler output:
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors
Lifetimes are covered in Chapter 10. For now, use owned types to avoid complexity.
An Example Program Using Structs
This example demonstrates the progression from loose parameters to structured data using a rectangle area calculator.
Initial Implementation
fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
The function signature fn area(width: u32, height: u32) -> u32
doesn’t express the relationship between parameters. This creates maintenance issues and potential parameter ordering mistakes.
Refactoring with Tuples
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
Tuples group related data but lose semantic meaning. Index-based access (dimensions.0
, dimensions.1
) reduces code clarity and introduces potential errors.
Refactoring with Structs
Structs provide both grouping and semantic meaning:
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Benefits:
- Clear function signature expressing intent
- Named field access (
rectangle.width
,rectangle.height
) - Borrowing instead of ownership transfer
- Type safety preventing parameter confusion
Debug Output
Structs don’t implement Display
by default. Use Debug
trait for development output:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
Error:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
Enable debug output with #[derive(Debug)]
:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
Pretty-print with {:#?}
:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug Macro
The dbg!
macro provides file/line information and returns ownership:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
Output:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Key Differences:
println!
takes references, outputs to stdoutdbg!
takes ownership (returns it), outputs to stderr with location info
Other derivable traits are listed in Appendix C. Custom trait implementation is covered in Chapter 10.
Method Syntax
Methods are functions defined within a struct’s context using impl
blocks. The first parameter is always self
, representing the instance being called.
Defining Methods
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
Key Points:
impl Rectangle
creates an implementation block&self
is shorthand forself: &Self
whereSelf
aliases the impl type- Methods use dot notation:
rect1.area()
- Borrowing rules apply:
&self
(immutable),&mut self
(mutable),self
(take ownership)
Using &self
preserves ownership in the caller, similar to function parameters. Methods consuming self
are rare and typically used for transformations.
Methods vs Fields
Methods can have the same name as fields:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
rect1.width()
calls the methodrect1.width
accesses the field
This pattern enables getter methods for controlled field access, supporting public methods with private fields.
Automatic Referencing
Rust automatically handles referencing/dereferencing for method calls:
#[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); // Equivalent
This eliminates the need for manual *
or ->
operators found in C/C++.
Methods with Parameters
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Expected output:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Implementation:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
Parameters after self
work like regular function parameters. Use borrowing for parameters you don’t need to own.
Associated Functions
Functions in impl
blocks without self
are associated functions (similar to static methods):
Filename: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
Called using ::
syntax: Rectangle::square(3)
. Common for constructors like String::from()
.
Multiple impl
Blocks
Structs can have multiple impl
blocks:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
Multiple blocks are useful with generics and traits (Chapter 10).
Summary
Structs provide custom types with named fields and associated behavior through methods. Key concepts:
- Field access: Dot notation with borrowing considerations
- Methods: Functions with
self
parameter for instance behavior - Associated functions: Type-scoped functions for construction/utilities
impl
blocks: Organize type-related functionality- Automatic referencing: Eliminates manual pointer dereferencing
Next: Enums provide another approach to custom types with variant-based data modeling.
Enums and Pattern Matching
Enums define types by enumerating possible variants, enabling type-safe variant handling through pattern matching. This chapter covers:
- Enum definition and variant syntax
- The
Option<T>
enum as Rust’s null safety mechanism match
expressions for exhaustive pattern matchingif let
andlet else
for concise single-pattern matching
Enums combined with pattern matching provide powerful abstractions for modeling domain data and eliminating entire classes of runtime errors.
Defining an Enum
Enums model data that can be one of several distinct variants. Each variant can carry different types and amounts of data.
Basic Syntax
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Enum values are created using namespace syntax:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Both values have type IpAddrKind
, enabling uniform function parameters:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
Data in Variants
Variants can carry associated data directly:
fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
Variant names become constructor functions: IpAddr::V4()
takes a String
and returns an IpAddr
.
Each variant can have different data types:
fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
The standard library’s IpAddr
uses structs within variants:
struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), }
Complex Variants
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
This enum replaces what would require multiple struct types:
struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct fn main() {}
Enums enable functions accepting any variant as a single type parameter, unlike separate structs.
Methods on Enums
Enums support methods via impl
blocks:
fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call(); }
The Option<T>
Enum
Option<T>
encodes nullable values safely in the type system:
enum Option<T> { None, Some(T), }
Option<T>
is included in the prelude. Variants Some
and None
are directly accessible without the Option::
prefix.
Examples:
fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; }
Option vs Null Safety
Unlike TypeScript’s null
and undefined
, Rust prevents direct operations on Option<T>
values:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
Compiler error:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
This compile-time check eliminates null pointer exceptions. You must explicitly handle both Some(T)
and None
cases before accessing the inner value.
Key Benefits:
- Explicit opt-in for nullable types via
Option<T>
- Compile-time guarantee that non-
Option
types are never null - Forces explicit null checking at usage sites
To extract values from Option<T>
, use pattern matching (match
) or Option<T>
methods. Pattern matching provides exhaustive case handling and value extraction from variants.
The match
Control Flow Construct
match
enables pattern matching against enum variants with compile-time exhaustiveness checking. Each pattern can extract data from variants and execute corresponding code.
Basic Syntax
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
Structure:
match
keyword followed by expression to match- Arms with pattern
=>
code syntax - Arms evaluated in order until match found
- Matching arm’s code becomes the expression’s return value
For multi-line arm code, use curly braces:
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
Pattern Binding
Match arms can bind to data within enum variants:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() {}
Extract data using variable binding in patterns:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } } fn main() { value_in_cents(Coin::Quarter(UsState::Alaska)); }
The state
variable binds to the inner value from Coin::Quarter(UsState::Alaska)
, making it accessible within the arm.
Matching Option<T>
Common pattern for safe null handling:
fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
Execution flow:
Some(5)
matchesSome(i)
, bindingi = 5
None
matchesNone
pattern directly- Each variant must be handled explicitly
Exhaustiveness
Match expressions must handle all possible variants:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Compiler error:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:572:1
::: /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:576:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Exhaustiveness prevents runtime errors from unhandled cases, especially critical for Option<T>
null safety.
Catch-All Patterns
Handle specific values with catch-all for remaining cases:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} }
Use _
placeholder when catch-all value isn’t needed:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {} }
Use unit value ()
for no-op catch-all:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} }
Key Points:
- Catch-all patterns must be last (evaluated in order)
_
ignores the value without binding()
performs no operation- Catch-all ensures exhaustiveness for large value spaces
Concise Control Flow with if let
and let else
if let
provides concise pattern matching for single patterns, trading exhaustiveness checking for brevity.
Basic if let
Syntax
Instead of verbose match
for single-pattern cases:
fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {max}"), _ => (), } }
Use if let
for cleaner code:
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {max}"); } }
Syntax: if let pattern = expression { body }
- Pattern matching works identically to
match
- Only executes when pattern matches
- Loses exhaustiveness checking
if let
with else
Handle non-matching cases with else
:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {state:?}!"), _ => count += 1, } }
Equivalent using if let
:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {state:?}!"); } else { count += 1; } }
let...else
for Early Returns
let...else
binds values or returns early, maintaining linear control flow:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { if let Coin::Quarter(state) = coin { if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } else { None } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let state = if let Coin::Quarter(state) = coin { state } else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
Cleaner with let...else
:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let Coin::Quarter(state) = coin else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
let...else
Properties:
- Pattern matches: binds value in outer scope and continues
- Pattern fails: executes
else
block, which must diverge (return, panic, etc.) - Maintains “happy path” flow without nested conditionals
When to Use Each
Pattern | Use Case | Trade-offs |
---|---|---|
match | Multiple patterns, exhaustiveness required | Verbose for single patterns |
if let | Single pattern, optional else case | No exhaustiveness checking |
let...else | Extract value or early return | Must diverge on mismatch |
Summary
Enums with pattern matching provide type-safe variant modeling:
- Enums: Model exclusive variants with associated data
Option<T>
: Eliminates null pointer errors through type safetymatch
: Exhaustive pattern matching with data extractionif let
/let...else
: Concise syntax for common patterns
These constructs enable robust APIs where invalid states are unrepresentable, moving error handling from runtime to compile time.
Managing Growing Projects with Packages, Crates, and Modules
Rust’s module system provides tools for organizing code, controlling scope, and managing visibility—essential for building maintainable applications. Unlike JavaScript/TypeScript’s file-based modules, Rust uses an explicit module hierarchy with compile-time visibility controls.
Core Concepts
Rust’s module system consists of:
- Packages: Cargo’s build units containing one or more crates
- Crates: Compilation units producing libraries or executables
- Modules: Organizational units controlling scope and privacy
- Paths: Item addressing within the module tree
Key Differences from JavaScript/TypeScript
Unlike ES modules where each file is implicitly a module, Rust modules are explicitly declared and can span multiple files or exist inline. Privacy is explicit—items are private by default, unlike JavaScript’s implicit public exports.
The module system enforces encapsulation at compile time, preventing access to private implementation details that could be accessed through reflection or direct property access in JavaScript.
Packages and Crates
Crates
A crate is Rust’s compilation unit—the smallest code unit the compiler processes. Crates come in two forms:
- Binary crates: Executables with a
main
function (like Node.js applications) - Library crates: Reusable code without
main
(like npm packages)
The crate root is the source file where compilation begins, forming the root module of the crate’s module tree.
Packages
A package contains one or more crates plus a Cargo.toml
manifest. Think of it as similar to a Node.js project with package.json
.
Package rules:
- Must contain at least one crate
- Can contain at most one library crate
- Can contain unlimited binary crates
Cargo Conventions
$ cargo new my-project
Created binary (application) `my-project` package
Cargo follows these conventions:
src/main.rs
→ binary crate root (package name)src/lib.rs
→ library crate root (package name)src/bin/
→ additional binary crates (one per file)
A package with both src/main.rs
and src/lib.rs
contains two crates: a binary and library, both named after the package. This pattern is common for CLI tools that expose both an executable and library API.
Defining Modules to Control Scope and Privacy
Module System Quick Reference
- Crate root: Compiler starts at
src/lib.rs
(library) orsrc/main.rs
(binary) - Module declaration:
mod garden;
looks for code in:- Inline:
mod garden { ... }
- File:
src/garden.rs
- Directory:
src/garden/mod.rs
- Inline:
- Submodules: Declared in parent module files, found in subdirectories
- Paths: Access items via
crate::module::item
(absolute) ormodule::item
(relative) - Privacy: Items are private by default; use
pub
to expose them use
keyword: Creates shortcuts to reduce path repetition
Example Structure
backyard
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
pub mod vegetables;
#[derive(Debug)]
pub struct Asparagus {}
Organizing Code with Modules
Modules provide namespace organization and privacy control. Unlike JavaScript imports which expose everything explicitly exported, Rust items are private by default.
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
This creates a module tree:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
The crate root forms an implicit crate
module. Child modules can access parent items, but parents cannot access private child items without explicit pub
declarations.
Paths for Referring to an Item in the Module Tree
Paths specify item locations within the module tree using ::
syntax:
- Absolute paths: Start from crate root with
crate::
(like absolute file paths) - Relative paths: Start from current module using
self
,super
, or module names
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
This code fails to compile because hosting
module is private:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Privacy Rules
Rust’s privacy model differs from JavaScript/TypeScript:
- Default private: All items are private to parent modules by default
- Child access: Child modules can access ancestor module items
- Parent restriction: Parent modules cannot access private child items
This enforces encapsulation—internal implementation details remain hidden unless explicitly exposed.
Exposing Paths with the pub
Keyword
Making the hosting
module public isn’t sufficient:
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:13:30
|
13 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
The pub
keyword on a module only allows access to the module itself, not its contents. Both the module and the function must be public:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Starting Relative Paths with super
Use super
to access parent module items (like ../
in file paths):
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
Making Structs and Enums Public
Structs: Fields remain private by default even when struct is public
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast.
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like.
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal.
// meal.seasonal_fruit = String::from("blueberries");
}
Structs with private fields require constructor functions since you cannot directly instantiate them.
Enums: All variants become public when enum is public
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Bringing Paths into Scope with the use
Keyword
The use
keyword creates shortcuts to paths, reducing repetition. Think of it as creating local aliases, similar to import statements in JavaScript/TypeScript.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
The use
statement creates a shortcut valid only within its scope:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
Idiomatic use
Patterns
Functions: Import the parent module, not the function directly
// Idiomatic
use crate::front_of_house::hosting;
hosting::add_to_waitlist();
// Less clear where function is defined
use crate::front_of_house::hosting::add_to_waitlist;
add_to_waitlist();
Structs/Enums/Types: Import the full path
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Name conflicts: Use parent modules to disambiguate
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
Providing New Names with the as
Keyword
Create aliases to resolve naming conflicts:
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
Re-exporting Names with pub use
Make imported items available to external code (similar to TypeScript’s export { ... } from ...
):
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
Re-exporting allows exposing a different public API structure than your internal organization.
Using External Packages
Add dependencies to Cargo.toml
:
rand = "0.8.5"
Then import and use:
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Standard library (std
) is available automatically but still requires explicit use
statements.
Nested Paths
Combine multiple imports from the same crate/module:
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Use self
to import both the parent and child:
use std::io::{self, Write};
The Glob Operator
Import all public items (use sparingly):
use std::collections::*;
Commonly used in test modules and when implementing prelude patterns. Can make code less clear and cause naming conflicts.
Separating Modules into Different Files
As modules grow, separate them into individual files for better organization. Unlike JavaScript/TypeScript where files are implicit modules, Rust requires explicit module declarations.
Moving Modules to Files
Starting from inline modules, extract them to separate files:
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
pub mod hosting {
pub fn add_to_waitlist() {}
}
The mod
declaration loads the file once—it’s not an “include” operation. Other files reference the loaded module via its declared path.
Extracting Submodules
For submodules, create a directory structure:
pub mod hosting;
pub fn add_to_waitlist() {}
The file location must match the module hierarchy—hosting
goes in src/front_of_house/hosting.rs
because it’s a child of front_of_house
.
File Path Conventions
Rust supports two file organization styles:
Modern style (recommended):
src/front_of_house.rs
src/front_of_house/hosting.rs
Legacy style (still supported):
src/front_of_house/mod.rs
src/front_of_house/hosting/mod.rs
Don’t mix styles within the same project to avoid confusion.
Summary
Rust’s module system provides explicit control over code organization and visibility:
- Packages contain crates with
Cargo.toml
manifests - Crates are compilation units (binary or library)
- Modules organize code with privacy controls (private by default)
- Paths address items using
::
syntax (absolute withcrate::
or relative) use
creates local shortcuts to reduce path repetitionpub
exposes items across module boundaries
This system enforces encapsulation at compile time, unlike JavaScript’s runtime-based privacy patterns.
Common Collections
Rust’s standard library provides heap-allocated collections that can grow or shrink at runtime. The three most commonly used are:
- Vector (
Vec<T>
) - Dynamic arrays for storing values of the same type - String - UTF-8 encoded text collections
- HashMap (
HashMap<K, V>
) - Key-value mappings using hash tables
Each collection has different performance characteristics and trade-offs. This chapter covers creation, updating, and reading operations for each type.
Storing Lists of Values with Vectors
Vec<T>
is a growable array type that stores values contiguously in memory. All elements must be of the same type.
Creating Vectors
// Empty vector with type annotation let v: Vec<i32> = Vec::new(); // Using the vec! macro with initial values let v = vec![1, 2, 3];
Updating Vectors
let mut v = Vec::new(); v.push(5); v.push(6); v.push(7);
The mut
keyword is required for modification. Rust infers the type from the data.
Reading Elements
Two approaches for accessing elements:
let v = vec![1, 2, 3, 4, 5]; // Index syntax - panics on invalid index let third: &i32 = &v[2]; // get method - returns Option<&T> let third: Option<&i32> = v.get(2);
Use indexing when you want the program to crash on invalid access, use get()
when you want to handle the error gracefully.
Borrowing Rules
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6); // Error: cannot borrow as mutable
println!("The first element is: {first}");
Adding elements may require reallocation, invalidating existing references. The borrow checker prevents use-after-free errors.
Iteration
let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } // Mutable iteration let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; }
Storing Multiple Types with Enums
Vectors can only store one type, but enums allow storing variants of different types:
enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ];
For truly dynamic types unknown at compile time, use trait objects (covered in Chapter 18).
Memory Management
Vectors are automatically freed when they go out of scope, along with their contents. The borrow checker ensures references remain valid.
{ let v = vec![1, 2, 3, 4]; // v is valid here } // v goes out of scope and is freed
Storing UTF-8 Encoded Text with Strings
Rust has two main string types: str
(string slice, usually seen as &str
) and String
(owned, growable). Both are UTF-8 encoded.
&str
- Immutable reference to string data, often stored in binaryString
- Growable, heap-allocated, owned string type
Creating Strings
// Empty string let mut s = String::new(); // From string literal let s = "initial contents".to_string(); let s = String::from("initial contents"); // UTF-8 support let hello = String::from("Здравствуйте"); let hello = String::from("नमस्ते");
Updating Strings
let mut s = String::from("foo"); // Append string slice s.push_str("bar"); // Append single character s.push('l'); // Concatenation with + operator let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1 is moved, s2 can still be used // Multiple concatenation with format! macro let s = format!("{s1}-{s2}-{s3}");
The +
operator uses the add
method with signature fn add(self, s: &str) -> String
, taking ownership of the left operand.
String Indexing is Not Allowed
let s1 = String::from("hello");
let h = s1[0]; // Error: doesn't implement Index<{integer}>
Reasons:
- Variable byte encoding - UTF-8 characters can be 1-4 bytes
- Performance - Would require O(n) time to find nth character
- Ambiguity - What should be returned? Byte, scalar value, or grapheme cluster?
Internal Representation
Strings are Vec<u8>
wrappers with UTF-8 guarantees:
let hello = "Здравствуйте"; // 12 chars, 24 bytes let hello = "नमस्ते"; // 4 graphemes, 6 scalar values, 18 bytes
String Slicing
Use ranges carefully - slicing must occur at valid UTF-8 boundaries:
let hello = "Здравствуйте"; let s = &hello[0..4]; // "Зд" (each char is 2 bytes) // This would panic at runtime: // let s = &hello[0..1]; // Invalid UTF-8 boundary
Iterating Over Strings
// By Unicode scalar values (char) for c in "Зд".chars() { println!("{c}"); // З, д } // By bytes for b in "Зд".bytes() { println!("{b}"); // 208, 151, 208, 180 } // For grapheme clusters, use external crates
String Methods
Key methods for string manipulation:
let s = String::from("hello world"); // Search s.contains("world"); // true // Replace s.replace("world", "rust"); // "hello rust" // Split s.split_whitespace(); // iterator over words
Unlike JavaScript strings, Rust strings are not indexed due to UTF-8 complexity. Use iteration methods or careful slicing instead.
Storing Keys with Associated Values in Hash Maps
HashMap<K, V>
stores key-value pairs using a hashing function. Keys must implement Eq
and Hash
traits.
Creating Hash Maps
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50);
Hash maps are not included in the prelude, require explicit use
. No built-in macro like vec!
.
Accessing Values
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); // Returns Option<&V> let score = scores.get("Blue").copied().unwrap_or(0); // Iterate over key-value pairs for (key, value) in &scores { println!("{key}: {value}"); }
Ownership Rules
use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are moved, no longer valid // For Copy types like i32, values are copied // For references, values must live as long as the hash map
Updating Hash Maps
Overwriting values:
scores.insert(String::from("Blue"), 25); // Replaces existing value
Insert only if key doesn’t exist:
scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); // Won't insert, Blue exists
Update based on old value:
let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; // Dereference to modify the value }
The entry
method returns an Entry
enum that represents a value that might or might not exist. or_insert
returns a mutable reference to the value.
Performance Notes
- Default hasher is SipHash (cryptographically secure, DoS resistant)
- For performance-critical code, consider alternative hashers via
BuildHasher
trait - All keys must be same type, all values must be same type
Common Patterns
// Check if key exists if scores.contains_key("Blue") { /* ... */ } // Remove key-value pair scores.remove("Blue"); // Get mutable reference to value if let Some(score) = scores.get_mut("Blue") { *score += 10; } // Merge two hash maps for (k, v) in other_map { scores.insert(k, v); }
Hash maps provide O(1) average-case performance for insertions, deletions, and lookups, making them suitable for caching, counting, and indexing operations common in web applications.
Summary
Vectors, strings, and hash maps will provide a large amount of functionality necessary in programs when you need to store, access, and modify data. Here are some exercises you should now be equipped to solve:
- Given a list of integers, use a vector and return the median (when sorted, the value in the middle position) and mode (the value that occurs most often; a hash map will be helpful here) of the list.
- Convert strings to pig latin. The first consonant of each word is moved to the end of the word and ay is added, so first becomes irst-fay. Words that start with a vowel have hay added to the end instead (apple becomes apple-hay). Keep in mind the details about UTF-8 encoding!
- Using a hash map and vectors, create a text interface to allow a user to add employee names to a department in a company; for example, “Add Sally to Engineering” or “Add Amir to Sales.” Then let the user retrieve a list of all people in a department or all people in the company by department, sorted alphabetically.
The standard library API documentation describes methods that vectors, strings, and hash maps have that will be helpful for these exercises!
We’re getting into more complex programs in which operations can fail, so it’s a perfect time to discuss error handling. We’ll do that next!
Error Handling
Rust categorizes errors into two types:
- Recoverable errors - Use
Result<T, E>
to handle expected failures (file not found, network timeout) - Unrecoverable errors - Use
panic!
macro for bugs and invariant violations (array bounds, null pointer dereference)
Unlike languages with exceptions, Rust makes error handling explicit through the type system. This forces you to decide how to handle each potential failure point, improving reliability and preventing silent failures.
This chapter covers when to use panic!
vs Result<T, E>
and how to effectively propagate and handle errors.
Unrecoverable Errors with panic!
The panic!
macro immediately terminates the program when encountering unrecoverable errors. By default, it unwinds the stack, cleaning up data from each function before exiting.
Panic Behavior Configuration
[profile.release]
panic = 'abort' # Skip unwinding, let OS clean up (smaller binary)
Explicit Panic
fn main() { panic!("crash and burn"); }
Output:
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
Panic from Invalid Operations
fn main() { let v = vec![1, 2, 3]; v[99]; // Index out of bounds triggers panic }
Rust prevents buffer overreads by panicking on invalid array access, unlike C where this causes undefined behavior.
Debugging with Backtraces
Set RUST_BACKTRACE=1
to get a stack trace:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
1: core::panicking::panic_fmt
2: core::panicking::panic_bounds_check
3: <usize as core::slice::index::SliceIndex<[T]>>::index
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
6: panic::main // <-- Your code here
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
Start debugging from the first line mentioning your code (line 6 above). Lines above are library code that called your code; lines below are code that called your function.
When to Use Panic
Use panic!
for:
- Programming errors (bugs in your logic)
- Violated invariants that should never happen
- Security vulnerabilities from invalid data
- Contract violations in functions
Example:
fn get_user(id: u32) -> User { if id == 0 { panic!("User ID cannot be zero"); // Contract violation } // ... rest of function }
For recoverable errors (network failures, file not found), use Result<T, E>
instead.
Recoverable Errors with Result
The Result<T, E>
enum handles recoverable errors:
enum Result<T, E> { Ok(T), Err(E), }
Basic Usage
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening file: {error:?}"), }; }
Handling Different Error Types
use std::fs::File;
use std::io::ErrorKind;
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating file: {error:?}");
})
} else {
panic!("Problem opening file: {error:?}");
}
});
Shortcuts: unwrap
and expect
// unwrap - panic with default message on error let f = File::open("hello.txt").unwrap(); // expect - panic with custom message let f = File::open("hello.txt") .expect("hello.txt should be included in this project");
Use expect
over unwrap
in production code for better debugging.
Error Propagation
Manual propagation:
fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } }
The ?
Operator
The ?
operator simplifies error propagation:
fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } // Even more concise with method chaining fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } // Using standard library convenience function fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") }
The ?
operator:
- If
Ok(value)
- returnsvalue
- If
Err(e)
- returns early withErr(e)
converted viaFrom
trait
?
Operator Constraints
?
can only be used in functions returning compatible types:
fn main() {
let greeting_file = File::open("hello.txt")?; // Error: main returns ()
}
Fix by changing return type:
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Using ?
with Option
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() }
Common Patterns
Convert between Result and Option:
// Result to Option let opt: Option<String> = File::open("hello.txt").ok(); // Option to Result let result: Result<String, &str> = some_option.ok_or("value was None");
Chaining operations:
let processed = input .parse::<i32>()? .checked_mul(2) .ok_or("overflow")?;
Early returns in main:
fn main() -> Result<(), Box<dyn Error>> { let config = load_config()?; let data = fetch_data(&config)?; process_data(data)?; Ok(()) }
The ?
operator makes error handling ergonomic while maintaining explicit control flow, similar to how async/await improves Promise handling in TypeScript.
To panic!
or Not to panic!
Default choice: Return Result
- gives calling code flexibility to decide how to handle errors.
Use panic!
when:
- Examples/prototypes -
unwrap()
andexpect()
are acceptable placeholders - Tests - failed assertions should terminate the test
- Impossible states - when you have more information than the compiler
- Contract violations - invalid inputs that should never happen
- Security vulnerabilities - continuing could be dangerous
Examples and Prototyping
// Acceptable in examples and prototypes let config: Config = std::env::args().collect().parse().unwrap(); let file = File::open("config.toml").expect("config file must exist");
Use expect()
over unwrap()
to provide context for debugging.
When You Know More Than the Compiler
use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid");
The string is hardcoded and valid, but the compiler can’t guarantee this.
Guidelines for Library Design
Panic for programming errors:
pub fn get_element(slice: &[i32], index: usize) -> i32 { if index >= slice.len() { panic!("Index {} out of bounds for slice of length {}", index, slice.len()); } slice[index] }
Return Result for expected failures:
pub fn parse_config(data: &str) -> Result<Config, ConfigError> { // Handle malformed data gracefully } pub fn fetch_url(url: &str) -> Result<Response, NetworkError> { // Network calls can fail in normal operation }
Type-Based Validation
Create custom types to encode invariants:
pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess must be between 1 and 100, got {}", value); } Guess { value } } pub fn value(&self) -> i32 { self.value } } // Functions can now safely assume valid ranges fn process_guess(guess: Guess) { // No need to validate - type system guarantees validity }
Decision Matrix
Scenario | Choice | Rationale |
---|---|---|
User input validation | Result | Expected to be invalid sometimes |
Network requests | Result | Can fail due to external factors |
File operations | Result | Files may not exist or lack permissions |
Array bounds checking | panic! | Programming error, should never happen |
Parsing hardcoded strings | expect() | Invalid data indicates programming error |
Library contract violations | panic! | Caller passed invalid arguments |
Modern Error Handling Patterns
Layered error handling:
// Low-level: specific errors fn read_config_file() -> Result<String, io::Error> { /* ... */ } // Mid-level: domain errors fn parse_config(content: String) -> Result<Config, ConfigError> { /* ... */ } // High-level: application errors fn initialize() -> Result<App, Box<dyn Error>> { let content = read_config_file()?; let config = parse_config(content)?; Ok(App::new(config)) }
Error context chaining:
use anyhow::{Context, Result}; fn load_settings() -> Result<Settings> { let content = fs::read_to_string("settings.toml") .context("Failed to read settings file")?; toml::from_str(&content) .context("Failed to parse settings TOML") }
The goal is to make error handling predictable and maintainable, similar to how TypeScript’s strict null checks prevent runtime errors by making nullability explicit.
Generic Types, Traits, and Lifetimes
Rust’s generics provide compile-time polymorphism through abstract type parameters. This chapter covers three key concepts: generic types for code reuse, traits for shared behavior contracts, and lifetimes for memory safety guarantees.
You’ve already used generics with Option<T>
, Vec<T>
, HashMap<K, V>
, and Result<T, E>
. Now you’ll learn to define your own generic types, functions, and methods.
Removing Duplication by Extracting a Function
Before exploring generics, let’s examine the pattern of extracting common functionality. Consider finding the largest number in a list:
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); assert_eq!(*largest, 100); }
To handle multiple lists without code duplication, extract the logic into a function:
fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 6000); }
The refactoring process:
- Identify duplicate code
- Extract into function body with appropriate signature
- Replace duplicated code with function calls
This same pattern applies to generics—replacing specific types with abstract placeholders that work across multiple types.
Generic Data Types
Generics allow you to write code that works with multiple types while maintaining type safety. You can use generics in function signatures, structs, enums, and methods.
Function Definitions
Generic functions use type parameters in angle brackets. Consider these two functions that find the largest value:
fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {result}"); assert_eq!(*result, 'y'); }
Combine them using a generic type parameter T
:
fn largest<T>(list: &[T]) -> &T {
This declares a function generic over type T
, taking a slice of T
and returning a reference to T
.
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
This fails to compile because not all types support comparison:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
The error indicates you need trait bounds (covered in the next section) to constrain T
to comparable types.
Struct Definitions
Structs can be generic over one or more types:
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
Both fields must be the same type T
. This won’t compile:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
For different types, use multiple type parameters:
struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; }
Enum Definitions
Enums can hold generic data types:
enum Option<T> { Some(T), None, }
Multiple type parameters are common:
enum Result<T, E> { Ok(T), Err(E), }
Method Definitions
Implement methods on generic structs by declaring the type parameter after impl
:
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
You can implement methods only for specific concrete types:
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
Method type parameters can differ from struct type parameters:
struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
Performance
Generics have zero runtime cost due to monomorphization—the compiler generates specific versions for each concrete type used:
let integer = Some(5); let float = Some(5.0);
The compiler generates specialized versions:
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
This eliminates runtime overhead while maintaining type safety.
Traits: Defining Shared Behavior
Traits define shared behavior contracts that types can implement. They’re similar to interfaces in other languages but with some unique features.
Defining a Trait
A trait groups method signatures that define required behavior:
pub trait Summary {
fn summarize(&self) -> String;
}
The trait declaration uses the trait
keyword followed by method signatures. Each implementing type must provide its own implementation.
Implementing a Trait on a Type
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Use the trait like any other method:
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
Orphan rule: You can only implement a trait on a type if either the trait or the type (or both) are local to your crate. This prevents conflicting implementations.
Default Implementations
Traits can provide default method implementations:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Use an empty impl
block to accept defaults:
impl Summary for NewsArticle {}
Default implementations can call other trait methods:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
Traits as Parameters
Use the impl Trait
syntax for function parameters:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
Trait Bound Syntax
The full trait bound syntax is more verbose but more powerful:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Use trait bounds when you need multiple parameters of the same type:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
Multiple Trait Bounds
Specify multiple trait bounds with +
:
pub fn notify(item: &(impl Summary + Display)) {
Or with generic syntax:
pub fn notify<T: Summary + Display>(item: &T) {
Where Clauses
For complex trait bounds, use where
clauses for clarity:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
Returning Types That Implement Traits
Return trait implementations without specifying concrete types:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
Limitation: You can only return a single concrete type, not different types conditionally.
Conditional Implementation
Implement methods conditionally based on trait bounds:
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Blanket implementations implement a trait for any type that satisfies trait bounds:
impl<T: Display> ToString for T {
// --snip--
}
This allows calling to_string()
on any type implementing Display
:
let s = 3.to_string();
Traits enable compile-time polymorphism while maintaining type safety and performance.
Validating References with Lifetimes
Lifetimes ensure references remain valid for their required duration. Every reference has a lifetime—the scope where it’s valid. Most lifetimes are inferred, but explicit annotation is required when relationships between reference lifetimes are ambiguous.
Preventing Dangling References
Lifetimes prevent dangling references by ensuring referenced data outlives the reference:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
This fails because x
is destroyed before r
is used:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| --- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
The Borrow Checker
The borrow checker compares scopes to validate borrows:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
The reference r
(lifetime 'a
) outlives the referenced data x
(lifetime 'b
).
Fixed version:
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
Generic Lifetimes in Functions
Functions returning references need lifetime parameters when the compiler can’t determine the relationship between input and output lifetimes:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
Error:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Lifetime Annotation Syntax
Lifetime parameters start with an apostrophe and are typically lowercase:
&i32 // a reference
&'a i32 // a reference with explicit lifetime
&'a mut i32 // a mutable reference with explicit lifetime
Lifetime Annotations in Function Signatures
Declare lifetime parameters in angle brackets:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
This signature means:
- Both parameters live at least as long as lifetime
'a
- The returned reference will live at least as long as lifetime
'a
- The actual lifetime is the smaller of the two input lifetimes
Example usage:
fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {result}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Invalid usage:
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Thinking in Terms of Lifetimes
Only specify lifetime parameters for references that affect the output. If a function always returns the first parameter, the second parameter doesn’t need a lifetime annotation:
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
Returning references to values created within the function creates dangling references:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
Lifetime Annotations in Struct Definitions
Structs holding references need lifetime annotations:
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
This ensures the struct instance can’t outlive the reference it holds.
Lifetime Elision
Three rules eliminate the need for explicit lifetime annotations in common cases:
- Input lifetimes: Each reference parameter gets its own lifetime
- Single input: If there’s exactly one input lifetime, it’s assigned to all outputs
- Methods: If one parameter is
&self
or&mut self
, its lifetime is assigned to all outputs
Examples:
// Rule 1: fn foo<'a>(x: &'a i32)
fn first_word(s: &str) -> &str {
// Rule 2: fn foo<'a>(x: &'a i32) -> &'a i32
fn first_word<'a>(s: &'a str) -> &'a str {
For multiple inputs without self
, explicit annotations are required:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { // Error: can't determine output lifetime
Lifetime Annotations in Method Definitions
Struct lifetime parameters must be declared after impl
:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
Third elision rule example:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
The Static Lifetime
The 'static
lifetime indicates data living for the entire program duration:
let s: &'static str = "I have a static lifetime.";
String literals are stored in the binary and have 'static
lifetime. Use 'static
only when data truly lives for the program’s entire duration.
Generics, Trait Bounds, and Lifetimes Together
Example combining all three concepts:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {result}"); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {ann}"); if x.len() > y.len() { x } else { y } }
This function:
- Is generic over type
T
(withDisplay
trait bound) - Has lifetime parameter
'a
for references - Ensures the returned reference lives as long as both inputs
Lifetimes provide compile-time memory safety guarantees without runtime overhead.
Writing Automated Tests
Testing validates that code behaves as intended. Rust’s type system catches many bugs at compile time, but cannot verify business logic correctness. Automated tests ensure your code produces expected outputs for given inputs.
Rust provides built-in testing framework with attributes, macros, and execution control. This chapter covers test organization, writing effective tests, and test execution strategies.
Test Structure
Tests in Rust follow a standard pattern:
- Setup: Arrange necessary data or state
- Exercise: Execute the code under test
- Assert: Verify results match expectations
The #[test]
attribute marks functions as tests, and cargo test
runs all tests in parallel by default.
How to Write Tests
Test functions use the #[test]
attribute and typically follow the arrange-act-assert pattern. Rust provides several assertion macros and the #[should_panic]
attribute for testing error conditions.
Test Function Basics
Generate a new library project to explore test structure:
$ cargo new adder --lib
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Run tests with cargo test
:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Test functions panic on failure. Each test runs in its own thread—when the main thread detects a test thread has died, the test is marked as failed.
Example with passing and failing tests:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
Assertion Macros
assert!
Macro
Verifies a condition evaluates to true
:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
assert_eq!
and assert_ne!
Macros
Test equality and inequality with better error messages:
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
These macros use debug formatting, so tested values must implement PartialEq
and Debug
traits. Use #[derive(PartialEq, Debug)]
for custom types.
Custom Failure Messages
Add custom messages with format strings:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
Testing Error Conditions
#[should_panic]
Attribute
Test that code panics under certain conditions:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
For more precise testing, specify expected panic message:
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
Using Result<T, E>
in Tests
Alternative to panicking—return Result
types:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
Return Ok(())
for success, Err
for failure. Enables use of the ?
operator for error propagation. Cannot be combined with #[should_panic]
—use assert!(value.is_err())
instead.
Controlling How Tests Are Run
cargo test
compiles and runs tests with customizable behavior through command-line options. Options for cargo test
come before --
, while options for the test binary come after --
.
Parallel vs Sequential Execution
Tests run in parallel by default using threads. To run sequentially or control thread count:
$ cargo test -- --test-threads=1
Showing Function Output
By default, passing tests capture output (e.g., println!
). Failed tests show captured output. To see output from passing tests:
$ cargo test -- --show-output
Example test with output:
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
Running Subset of Tests
Single Tests
Run specific test by name:
$ cargo test one_hundred
Filtering Tests
Run multiple tests matching a pattern:
$ cargo test add // Runs tests with "add" in the name
Module names are part of test names, so you can filter by module.
Ignoring Tests
Mark expensive tests to skip during normal runs:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
Run only ignored tests:
$ cargo test -- --ignored
Run all tests (including ignored):
$ cargo test -- --include-ignored
Test Organization
Rust categorizes tests as unit tests (small, focused, test one module in isolation, can test private interfaces) and integration tests (external to your library, use public API only, test multiple modules together).
Unit Tests
Place unit tests in each file with the code they test, in a tests
module annotated with #[cfg(test)]
.
The Tests Module and #[cfg(test)]
The #[cfg(test)]
annotation compiles and runs test code only with cargo test
, not cargo build
:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Testing Private Functions
Rust allows testing private functions since tests are just Rust code in the same crate:
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
The use super::*;
brings parent module items into scope.
Integration Tests
Create integration tests in a tests
directory at the project root (alongside src
). Each file in tests
is compiled as a separate crate.
The tests Directory
Project structure:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
Each test file gets its own section in test output. Run specific integration test file:
$ cargo test --test integration_test
Submodules in Integration Tests
Files in tests
directory are separate crates and don’t share the same behavior as src
files.
For shared helper functions, use subdirectories instead of top-level files:
├── tests
├── common
│ └── mod.rs // Shared code
└── integration_test.rs
Use from integration tests:
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
Integration Tests for Binary Crates
Binary crates with only src/main.rs
cannot have integration tests that import functions with use
statements. Structure binary projects with logic in src/lib.rs
and a thin src/main.rs
that calls the library functions.
Summary
Rust’s testing features support both detailed unit testing and broader integration testing. Unit tests verify individual modules (including private functions), while integration tests validate public API behavior. This testing strategy helps ensure code correctness as you refactor and extend functionality.
Building a Command Line Tool: grep Implementation
This chapter demonstrates Rust concepts through building a grep
clone. The project combines:
- Code organization and module system
- Collections (vectors, strings)
- Error handling patterns
- Traits and lifetimes
- Testing strategies
- Closures and iterators (introduced here, detailed in Chapter 13)
Project Overview
We’ll build minigrep
- a simplified version of the Unix grep
utility that searches files for text patterns. The tool will:
- Accept command line arguments (query string and file path)
- Read file contents
- Filter matching lines
- Handle errors gracefully
- Support case-insensitive search via environment variables
- Write errors to stderr, results to stdout
This implementation showcases Rust’s performance, safety, and cross-platform capabilities for CLI tools, while demonstrating idiomatic patterns for real-world applications.
Command Line Arguments
Create the project and handle command line arguments:
$ cargo new minigrep
$ cd minigrep
Target usage: cargo run -- searchstring example-filename.txt
Reading Arguments
Use std::env::args
to access command line arguments:
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
Key points:
env::args()
returns an iterator over argumentscollect()
converts the iterator toVec<String>
- First argument (
args[0]
) is always the binary name - Invalid Unicode in arguments will panic; use
env::args_os()
forOsString
if needed
Storing Arguments
Extract the required arguments into variables:
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
This basic implementation lacks error handling for insufficient arguments - we’ll address that in the refactoring section.
File Reading
Add file reading capability to complete the basic functionality:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
use std::env;
use std::fs;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
Key points:
fs::read_to_string()
returnsResult<String, std::io::Error>
- Currently using
expect()
for error handling (will improve in next section) - The
main
function is handling multiple responsibilities (argument parsing, file reading)
Current issues to address:
- Poor separation of concerns
- Inadequate error handling
- No validation of input arguments
The next section addresses these through refactoring.
Refactoring: Error Handling and Modularity
Current issues:
main
function has multiple responsibilities- Configuration variables mixed with business logic
- Generic error messages using
expect
- No input validation
Binary Project Organization Pattern
Standard Rust pattern for CLI applications:
- main.rs: Command line parsing, configuration, calling
run()
, error handling - lib.rs: Application logic, testable functions
Extracting Configuration Parser
Extract argument parsing into a dedicated function:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
Grouping Configuration Data
Use a struct instead of tuple for better semantics:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
Note: Using clone()
for simplicity. Chapter 13 covers more efficient approaches using iterators.
Constructor Pattern
Convert to associated function following Rust conventions:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
Error Handling
Replace panic!
with proper error handling:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
Returning Results
Better practice: return Result
instead of panicking:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Key pattern: unwrap_or_else
with closure for custom error handling and controlled exit codes.
Extracting Business Logic
Separate program logic from main:
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Propagating Errors
Make run
return Result
for proper error propagation:
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Using Box<dyn Error>
trait object for flexible error types. The ?
operator propagates errors instead of panicking.
Handle the returned Result
in main:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Creating Library Crate
Move business logic to lib.rs for better testability:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
use std::env;
use std::error::Error;
use std::fs;
use std::process;
// --snip--
use minigrep::search;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
This structure enables:
- Unit testing of business logic
- Reusable library components
- Clear separation of concerns
- Better error handling patterns
Test-Driven Development
With logic separated into lib.rs, we can implement the search functionality using TDD.
Writing the Test
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Implementing to Pass
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Lifetime Annotations
The function signature requires explicit lifetime annotation because the returned string slices reference the contents
parameter, not query
. This tells Rust that the returned data lives as long as the contents
input.
Implementation Steps
Iterating Through Lines
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Filtering Lines
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Collecting Results
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
The implementation works with basic text search. Chapter 13 will show how to improve this using iterators for better performance and cleaner code.
Environment Variables
Add case-insensitive search controlled by environment variables.
Test for Case-Insensitive Search
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Implementation
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Key points:
to_lowercase()
creates newString
data, changing the type from&str
toString
- Need
&
when passing tocontains()
since it expects&str
- This handles basic Unicode but isn’t 100% accurate for all cases
Configuration Integration
Add environment variable support to the Config
struct:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Update the run
function to use the appropriate search function:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Environment Variable Detection
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Usage patterns:
env::var()
returnsResult<String, VarError>
is_ok()
returnstrue
if variable is set (regardless of value)- We only care about presence, not the actual value
Testing
$ cargo run -- to poem.txt
Are you nobody, too?
How dreary to be somebody!
$ IGNORE_CASE=1 cargo run -- to poem.txt
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
PowerShell:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
PS> Remove-Item Env:IGNORE_CASE # To unset
This pattern allows flexible configuration without requiring command-line flags for every option.
Error Output to stderr
Proper CLI tools distinguish between regular output (stdout) and error messages (stderr) for better shell integration.
Current Problem
Our error messages currently go to stdout:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
The error appears in output.txt
instead of the terminal, making debugging difficult when output is redirected.
Solution: eprintln!
Macro
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Testing the Fix
Error case with redirection:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
Now the error appears on terminal while output.txt
remains empty.
Success case with redirection:
$ cargo run -- to poem.txt > output.txt
Terminal shows nothing, output.txt
contains results:
Are you nobody, too?
How dreary to be somebody!
Summary
This implementation demonstrates:
- Code organization: Clean separation between main.rs and lib.rs
- Error handling: Proper Result types and error propagation
- Testing: TDD approach with isolated, testable functions
- CLI patterns: Command line args, environment variables, proper output streams
- Rust features: Ownership, lifetimes, traits, and error handling
The resulting CLI tool follows Unix conventions and demonstrates production-ready patterns for Rust applications.
Functional Language Features: Iterators and Closures
Rust incorporates functional programming concepts including closures and iterators, both offering zero-cost abstractions with compile-time optimizations.
This chapter covers:
- Closures: Anonymous functions that capture their environment
- Iterators: Lazy evaluation for processing data sequences
- Performance: Both compile to equivalent machine code as hand-written loops
- Practical application: Refactoring I/O operations using functional patterns
These features enable expressive, high-performance code while maintaining Rust’s safety guarantees.
Closures: Anonymous Functions That Capture Their Environment
Closures are anonymous functions that capture values from their enclosing scope. Unlike regular functions, closures can access variables from the environment where they’re defined.
Environment Capture
Consider this scenario: a shirt giveaway system that assigns colors based on user preferences or inventory levels.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
The closure || self.most_stocked()
captures an immutable reference to self
. The unwrap_or_else
method calls this closure only when the Option
is None
, demonstrating lazy evaluation.
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
Type Inference and Annotations
Closures don’t require explicit type annotations in most cases—the compiler infers types from usage:
use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!("Today, do {} pushups!", expensive_closure(intensity)); println!("Next, do {} situps!", expensive_closure(intensity)); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } } fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout(simulated_user_specified_value, simulated_random_number); }
Syntax comparison:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
Once a closure’s types are inferred from first usage, they’re locked in:
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected `String`, found integer
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
Capture Modes
Closures automatically choose the minimal capture mode needed:
Immutable borrow:
fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let only_borrows = || println!("From closure: {list:?}"); println!("Before calling closure: {list:?}"); only_borrows(); println!("After calling closure: {list:?}"); }
Mutable borrow:
fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling closure: {list:?}"); }
Move semantics:
Use move
to force ownership transfer, commonly needed for thread boundaries:
use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); thread::spawn(move || println!("From thread: {list:?}")) .join() .unwrap(); }
Fn Traits
Closures implement one or more of these traits based on how they handle captured values:
FnOnce
: Callable once, consumes captured values. All closures implement this.FnMut
: Callable multiple times, may mutate captured values.Fn
: Callable multiple times without mutation, safe for concurrent access.
Example from Option<T>
:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
sort_by_key
requires FnMut
since it calls the closure multiple times:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!("{list:#?}"); }
This won’t compile because the closure moves value
out, implementing only FnOnce
:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
Correct approach using mutable reference:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!("{list:#?}, sorted in {num_sort_operations} operations"); }
Processing a Series of Items with Iterators
Iterators implement lazy evaluation—they do nothing until consumed. This enables efficient chaining of transformations without intermediate allocations.
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
The Iterator
Trait and next
Method
All iterators implement the Iterator
trait:
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided }
The associated type Item
defines what the iterator yields. Only next
requires implementation—all other methods have default implementations.
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
Note: v1_iter
must be mutable because next
consumes items. The for
loop handles this automatically by taking ownership.
Iterator creation methods:
iter()
: Immutable references (&T
)into_iter()
: Owned values (T
)iter_mut()
: Mutable references (&mut T
)
Consuming Adapters
Methods that call next
and consume the iterator:
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
Iterator Adapters
Methods that transform iterators into other iterators. Must be consumed to execute:
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
Closures with Environment Capture
Iterator adapters commonly use closures that capture environment variables:
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
The filter
closure captures shoe_size
from the environment, demonstrating how iterators can access external state while maintaining functional programming patterns.
Improving Our I/O Project
This section demonstrates refactoring imperative code to use iterators, eliminating clone
calls and improving readability through functional patterns.
Removing clone
with Iterator Ownership
Original implementation with inefficient cloning:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Refactoring to Accept an Iterator
Change main.rs
to pass the iterator directly:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Update the function signature to accept any iterator returning String
:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Using Iterator Methods Instead of Indexing
Replace indexing with next()
calls:
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
This eliminates cloning by taking ownership of iterator values, while improving error handling with explicit None
checks.
Functional Style with Iterator Adapters
Original imperative search function:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Functional refactor using iterator adapters:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Benefits:
- Eliminates mutable state (
results
vector) - Enables potential parallelization
- More concise and declarative
- Better composability
Performance note: Both implementations compile to equivalent machine code due to Rust’s zero-cost abstractions.
Iterator vs. Loop Performance
Iterator adapters are zero-cost abstractions—they compile to the same assembly as hand-written loops. The functional style often provides better readability and composability without performance penalties.
For high-performance applications, prefer iterators for their:
- Compile-time optimizations (loop unrolling, bounds check elimination)
- Reduced cognitive load through declarative style
- Built-in parallelization support with external crates
Comparing Performance: Loops vs. Iterators
Benchmark results comparing explicit for
loops vs. iterator implementation searching for “the” in The Adventures of Sherlock Holmes:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
Key findings:
- Iterator version performs slightly better
- Performance difference is negligible
- Both compile to nearly identical assembly
Zero-Cost Abstractions
Iterators exemplify Rust’s zero-cost abstractions—high-level constructs that impose no runtime overhead. The compiler applies optimizations including:
- Loop unrolling
- Bounds check elimination
- Inline expansion
- Dead code elimination
These optimizations often produce assembly equivalent to hand-optimized code.
Summary
Closures and iterators provide functional programming patterns with systems-level performance. They enable expressive, maintainable code without sacrificing runtime efficiency—a core principle of Rust’s design philosophy.
Use iterators and closures confidently in performance-critical code. The abstractions compile away, leaving only the essential operations.
Now that we’ve improved the expressiveness of our I/O project, let’s look at
some more features of cargo
that will help us share the project with the
world.
More About Cargo and Crates.io
Cargo provides advanced project management features beyond basic building and testing:
- Release profiles: Customize build configurations for development and production
- Publishing: Share libraries on crates.io
- Workspaces: Manage multi-crate projects with shared dependencies
- Binary installation: Install command-line tools from the ecosystem
- Custom commands: Extend Cargo with additional functionality
For comprehensive documentation, see cargo docs.
Customizing Builds with Release Profiles
Release profiles control compiler optimization levels and debugging information. Cargo uses two main profiles:
dev
: Used bycargo build
(fast compilation, debugging info)release
: Used bycargo build --release
(optimized, production-ready)
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
Default Settings
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
controls optimization intensity (0-3):
- 0: No optimization, fastest compilation
- 3: Maximum optimization, slower compilation
Custom Configuration
Override defaults in Cargo.toml
:
[profile.dev]
opt-level = 1
This applies moderate optimization to development builds, trading compile time for runtime performance.
For complete configuration options, see Cargo’s profile documentation.
Publishing a Crate to Crates.io
We’ve used packages from crates.io as dependencies of our project, but you can also share your code with other people by publishing your own packages. The crate registry at crates.io distributes the source code of your packages, so it primarily hosts code that is open source.
Rust and Cargo have features that make your published package easier for people to find and use. We’ll talk about some of these features next and then explain how to publish a package.
Documentation Comments
Use ///
for API documentation that generates HTML:
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
Generate docs: cargo doc --open
Standard Sections
- Panics: Conditions that cause panics
- Errors: Error types and causes for
Result
returns - Safety: Safety requirements for
unsafe
functions
Documentation Tests
Code in documentation comments runs as tests with cargo test
:
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
Module Documentation
Use //!
for crate/module-level documentation:
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
Public API Design with pub use
Re-export items to create a clean public API regardless of internal structure:
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
unimplemented!();
}
}
Without re-exports, users must write:
use art::kinds::PrimaryColor;
use art::utils::mix;
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
Now users can write:
use art::{PrimaryColor, mix};
Publishing Workflow
- Create account: Log in at crates.io with GitHub
- Get API token: From account settings, store with
cargo login
- Add metadata to
Cargo.toml
:
[package]
name = "your_crate_name"
version = "0.1.0"
edition = "2024"
description = "A brief description of your crate's functionality."
license = "MIT OR Apache-2.0"
- Publish:
cargo publish
Version Management
- New versions: Update
version
inCargo.toml
, runcargo publish
- Yanking:
cargo yank --vers 1.0.1
(prevents new usage, doesn’t break existing) - Un-yanking:
cargo yank --vers 1.0.1 --undo
Note: Published versions are permanent and cannot be deleted.
License Options
Use SPDX identifiers. Common choices:
MIT
: PermissiveApache-2.0
: Permissive with patent grantMIT OR Apache-2.0
: Rust ecosystem standard
For custom licenses, use license-file
instead of license
.
Cargo Workspaces
Workspaces manage multiple related crates with shared dependencies and a unified build system.
Creating a Workspace
Create workspace structure:
$ mkdir add
$ cd add
Workspace Cargo.toml
:
[workspace]
resolver = "3"
Add binary crate:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
Workspace structure:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Key points:
- Single
target
directory for all workspace members - Shared
Cargo.lock
ensures consistent dependency versions - Build from workspace root with
cargo build
Adding Library Crates
$ cargo new add_one --lib
Created library `add_one` package
Library implementation:
// add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
Workspace dependencies:
# adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Using workspace dependencies:
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Workspace Commands
Build workspace:
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Run specific package:
$ cargo run -p adder
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
Test specific package:
$ cargo test -p add_one
External Dependencies
Dependencies must be explicitly added to each crate’s Cargo.toml
:
# add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
Workspace ensures all crates use the same version of shared dependencies.
Testing
Run all tests:
$ cargo test
Run specific crate tests:
$ cargo test -p add_one
Publishing
Each workspace member publishes independently:
$ cargo publish -p add_one
$ cargo publish -p adder
Benefits
- Shared dependencies: Consistent versions across all crates
- Unified build: Single command builds entire workspace
- Cross-crate development: Easy local dependencies between related crates
- Testing: Run tests across all crates simultaneously
- Code sharing: Common utilities accessible to all workspace members
Workspaces are ideal for:
- Multi-crate applications with shared libraries
- Plugin architectures
- Related tools that share common functionality
Installing Binaries with cargo install
Install command-line tools from crates.io for local development use.
Installation location: $HOME/.cargo/bin
(ensure it’s in your $PATH
)
Requirements: Crate must have binary targets (contains src/main.rs
or specified binary)
Example: Installing ripgrep
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v14.1.1
Installing ripgrep v14.1.1
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)
The installed binary (rg
) is immediately available if ~/.cargo/bin
is in your PATH.
Common Use Cases
- Development tools (
cargo-watch
,cargo-expand
) - Text processing utilities (
ripgrep
,fd
,bat
) - System administration tools
- Custom CLI applications from the Rust ecosystem
Note: cargo install
is for developer tools, not system-wide package management.
Extending Cargo with Custom Commands
Cargo supports custom subcommands through executable naming convention.
Creating Custom Commands
Naming convention: cargo-<command-name>
executable in $PATH
Usage: cargo <command-name>
(Cargo finds and executes cargo-<command-name>
)
Examples
Install extensions and use them as native Cargo commands:
$ cargo install cargo-watch
$ cargo watch # Runs cargo-watch executable
List all available commands (including custom ones):
$ cargo --list
Benefits
- Seamless integration with existing Cargo workflow
- Discoverable through
cargo --list
- Standard installation via
cargo install
- Follows Unix tool composition principles
Custom commands enable domain-specific tooling while maintaining consistent user experience with core Cargo functionality.
Summary
Cargo’s ecosystem features enable productive Rust development:
- Release profiles: Optimize builds for development vs. production
- Publishing: Share code via crates.io with proper documentation
- Workspaces: Manage complex multi-crate projects
- Binary installation: Access community tools easily
- Extensibility: Add custom functionality without modifying Cargo core
These features support the Rust ecosystem’s growth by making code sharing, discovery, and collaboration straightforward and reliable.
Smart Pointers
Smart pointers are data structures that act like pointers but provide additional metadata and capabilities beyond standard references. While references (&
) only borrow values with no overhead, smart pointers often own their data and implement specific memory management patterns.
Key smart pointers in Rust’s standard library:
Box<T>
- heap allocation with single ownershipRc<T>
- reference counting for multiple ownership (single-threaded)Ref<T>
andRefMut<T>
viaRefCell<T>
- runtime borrow checking
Smart pointers implement the Deref
and Drop
traits:
Deref
enables automatic dereferencing, allowing smart pointers to behave like referencesDrop
customizes cleanup behavior when values go out of scope
This chapter covers interior mutability patterns and reference cycle prevention using Weak<T>
.
Core Concepts
Interior Mutability: Modifying data through immutable references using types like RefCell<T>
that enforce borrowing rules at runtime rather than compile time.
Reference Cycles: Circular references that prevent memory cleanup, resolved using weak references (Weak<T>
) that don’t affect reference counts.
Using Box<T>
to Point to Data on the Heap
Box<T>
allocates data on the heap with single ownership. Primary use cases:
- Types with unknown compile-time size (recursive types)
- Large data that should move without copying
- Trait objects (covered in Chapter 18)
Basic Usage
fn main() { let b = Box::new(5); println!("b = {b}"); }
Box<T>
provides heap allocation with automatic cleanup when it goes out of scope.
Enabling Recursive Types
Recursive types require indirection since Rust needs to know type sizes at compile time.
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
This fails because List
has infinite size:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Size Calculation Issue
Non-recursive enums use the size of their largest variant. Recursive types create infinite size calculations since each Cons
variant contains another List
.
Solution with Box<T>
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
Box<T>
has a known size (pointer size), breaking the recursive chain. The heap-allocated data structure achieves the desired recursion without infinite compile-time size.
Box<T>
implements Deref
for reference-like behavior and Drop
for automatic cleanup, making it a true smart pointer.
Treating Smart Pointers Like Regular References with Deref
The Deref
trait customizes the dereference operator (*
) behavior, allowing smart pointers to behave like regular references.
Basic Dereferencing
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
Using Box<T>
Like a Reference
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Box<T>
can be dereferenced like a regular reference due to its Deref
implementation.
Implementing Custom Smart Pointers
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
This MyBox<T>
type won’t work with the dereference operator without implementing Deref
:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Implementing the Deref
Trait
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
The deref
method returns a reference to the inner data. When you write *y
, Rust actually executes *(y.deref())
.
Deref Coercion
Deref coercion automatically converts references to types implementing Deref
into references to other types. This enables seamless interaction between different smart pointer types.
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
Calling a function expecting &str
with &MyBox<String>
:
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
Rust automatically applies deref coercion: &MyBox<String>
→ &String
→ &str
.
Without deref coercion, you’d need explicit conversions:
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
Deref Coercion Rules
Rust performs deref coercion in three cases:
&T
to&U
whenT: Deref<Target=U>
&mut T
to&mut U
whenT: DerefMut<Target=U>
&mut T
to&U
whenT: Deref<Target=U>
Note: Immutable references cannot coerce to mutable references due to borrowing rules.
Running Code on Cleanup with the Drop
Trait
The Drop
trait customizes cleanup behavior when a value goes out of scope. It’s essential for smart pointers that manage resources like memory, files, or network connections.
Implementing Drop
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); }
The Drop
trait requires implementing a drop
method that takes &mut self
. Rust automatically calls drop
when values go out of scope.
Output:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
Variables are dropped in reverse order of creation (d
before c
).
Manual Cleanup with std::mem::drop
You cannot call the Drop
trait’s drop
method directly:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 | drop(c);
| +++++ ~
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
Use std::mem::drop
for early cleanup:
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); }
Output:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
std::mem::drop
takes ownership of the value, triggering the Drop
implementation immediately. This prevents double-free errors since Rust can’t call drop
again on a moved value.
Rc<T>
, the Reference Counted Smart Pointer
Rc<T>
(reference counting) enables multiple ownership of the same data by tracking reference counts. When the count reaches zero, the value is cleaned up. Single-threaded only.
Multiple Ownership Problem
Using Box<T>
with multiple ownership fails:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Solution with Rc<T>
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
Rc::clone
increments the reference count without deep-copying data. Use Rc::clone
instead of a.clone()
to distinguish reference counting from expensive deep copies.
Reference Count Tracking
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
Output:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
Rc::strong_count
returns the current reference count. The Drop
implementation automatically decrements the count when Rc<T>
values go out of scope.
Rc<T>
allows multiple readers but no mutable access. For mutable data with multiple owners, combine with RefCell<T>
(covered next).
RefCell<T>
and the Interior Mutability Pattern
Interior mutability allows mutating data through immutable references by deferring borrow checking to runtime. RefCell<T>
enforces borrowing rules at runtime instead of compile time.
Runtime vs Compile-time Borrow Checking
Type | Ownership | Borrow Check | Mutability |
---|---|---|---|
Box<T> | Single | Compile-time | Immutable or mutable |
Rc<T> | Multiple | Compile-time | Immutable only |
RefCell<T> | Single | Runtime | Immutable or mutable |
RefCell<T>
is single-threaded only. For multithreaded scenarios, use Mutex<T>
.
Interior Mutability Use Case
Testing scenario requiring mutation through immutable interface:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Attempting to implement a mock that tracks sent messages:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
This fails because send
takes &self
but we need to mutate sent_messages
:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
Solution with RefCell<T>
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T>
provides:
borrow()
returnsRef<T>
(immutable smart pointer)borrow_mut()
returnsRefMut<T>
(mutable smart pointer)
Both implement Deref
for transparent access.
Runtime Borrow Checking
RefCell<T>
tracks active borrows at runtime. Violating borrowing rules causes panics:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Combining Rc<T>
and RefCell<T>
Multiple ownership with mutability:
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
Output:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Rc<RefCell<T>>
enables multiple owners who can all mutate the shared data, checked at runtime rather than compile time.
Reference Cycles Can Leak Memory
Rc<T>
and RefCell<T>
can create reference cycles where items refer to each other, preventing cleanup since reference counts never reach zero.
Creating a Reference Cycle
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() {}
Creating circular references:
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); // Uncomment the next line to see that we have a cycle; // it will overflow the stack. // println!("a next item = {:?}", a.tail()); }
Output:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
Both a
and b
have reference count 2. When they go out of scope, counts drop to 1 but never reach 0, causing a memory leak.
Preventing Reference Cycles Using Weak<T>
Weak<T>
provides non-owning references that don’t affect reference counts.
Rc::downgrade
createsWeak<T>
fromRc<T>
Weak::upgrade
returnsOption<Rc<T>>
- Strong references control cleanup; weak references don’t
Tree Data Structure Example
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
Adding parent references with Weak<T>
:
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
Reference Count Monitoring
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( "branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); }
Rc::strong_count
tracks owning referencesRc::weak_count
tracks non-owning references- Only strong count reaching zero triggers cleanup
When branch
goes out of scope, its strong count drops to 0 and gets cleaned up despite having weak references pointing to it.
Summary
Smart pointers provide different memory management patterns:
Box<T>
: Single ownership, heap allocationRc<T>
: Multiple ownership via reference countingRefCell<T>
: Interior mutability with runtime borrow checkingWeak<T>
: Non-owning references to prevent cycles
Use Weak<T>
for parent-child relationships where children should not own their parents, preventing reference cycles and memory leaks.
Fearless Concurrency
Rust’s ownership and type systems provide compile-time guarantees for concurrent programming. Unlike runtime concurrency bugs common in other languages, Rust’s approach moves many concurrency errors to compile time, eliminating data races and memory safety issues through static analysis.
Rust supports multiple concurrency paradigms:
- Message-passing concurrency: Channels for thread communication
- Shared-state concurrency: Mutexes and atomic types for shared data access
- Thread creation: OS threads with the
thread::spawn
API - Send/Sync traits: Compile-time thread safety guarantees
The standard library provides a 1:1 threading model (one OS thread per language thread), with async/await as an alternative concurrency model covered in the next chapter.
This chapter covers:
- Thread creation and management
- Channel-based message passing
- Shared state with mutexes and atomic reference counting
- The
Send
andSync
traits for extensible concurrency
Using Threads to Run Code Simultaneously
Rust uses a 1:1 threading model where each language thread maps to an OS thread. Thread creation uses thread::spawn
with closures containing the code to execute.
Creating Threads with spawn
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } }
Key behaviors:
- Main thread completion terminates all spawned threads
- Thread execution order is non-deterministic
thread::sleep
yields execution to other threads
Waiting for Thread Completion with join
thread::spawn
returns a JoinHandle<T>
that provides a join
method to wait for thread completion:
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
Calling join
blocks the current thread until the target thread terminates. Placement of join
calls affects concurrency behavior—early joins eliminate parallelism.
Using move
Closures for Data Transfer
To transfer ownership of variables from one thread to another, use move
closures:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
This fails because Rust cannot guarantee the lifetime of borrowed data across thread boundaries:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
The move
keyword forces the closure to take ownership:
use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {v:?}"); }); handle.join().unwrap(); }
This transfers ownership to the spawned thread, preventing use-after-free errors and ensuring thread safety through compile-time checks.
Using Message Passing to Transfer Data Between Threads
Message passing enables thread communication by sending data through channels. Rust implements channels via std::sync::mpsc
(multiple producer, single consumer).
Channel Basics
Channels provide a transmitter/receiver pair for unidirectional data flow:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
mpsc::channel()
returns (Sender<T>, Receiver<T>)
. The mpsc
design allows multiple senders but only one receiver.
Sending Data Between Threads
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); }
The send
method returns Result<(), SendError<T>>
. It fails when the receiver is dropped.
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {received}"); }
Receiver methods:
recv()
: Blocks until a value arrives, returnsResult<T, RecvError>
try_recv()
: Non-blocking, returnsResult<T, TryRecvError>
immediately
Channels and Ownership
Channels transfer ownership of sent values, preventing use-after-send errors:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
Compilation fails with:
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:26
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
The send
operation moves ownership to the receiver, preventing data races.
Multiple Values and Iterator Pattern
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
The receiver implements Iterator
, terminating when the channel closes (all senders dropped).
Multiple Producers
Clone the transmitter to create multiple senders:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// --snip--
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
// --snip--
}
Each cloned sender can send independently. Message order depends on thread scheduling and is non-deterministic.
Shared-State Concurrency
Unlike message passing’s single ownership model, shared-state concurrency allows multiple threads to access the same memory locations simultaneously. Rust provides thread-safe primitives through mutexes and atomic reference counting.
Mutexes for Exclusive Data Access
A mutex (mutual exclusion) guards data with a locking mechanism, ensuring only one thread accesses the data at a time.
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {m:?}"); }
Key Mutex<T>
characteristics:
lock()
returnsLockResult<MutexGuard<T>>
MutexGuard<T>
implementsDeref
to access inner dataDrop
implementation automatically releases the lock when guard goes out of scope- Lock acquisition blocks until available or panics if poisoned
Sharing Mutexes Between Threads
Attempting to share a mutex across threads fails without proper ownership handling:
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Error: cannot move counter
into multiple threads:
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
Multiple Ownership with Rc<T>
Limitation
Rc<T>
provides multiple ownership but is not thread-safe:
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Error: Rc<T>
lacks Send
trait for thread safety:
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/std/src/thread/mod.rs:728:1
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
Rc<T>
uses non-atomic reference counting, making it unsafe for concurrent access.
Atomic Reference Counting with Arc<T>
Arc<T>
(atomically reference-counted) provides thread-safe multiple ownership:
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Arc<T>
uses atomic operations for reference counting, ensuring thread safety. Atomic operations have performance overhead, so use only when needed for concurrent access.
Interior Mutability Pattern
Mutex<T>
provides interior mutability similar to RefCell<T>
:
RefCell<T>
/Rc<T>
: Single-threaded shared ownership with runtime borrow checkingMutex<T>
/Arc<T>
: Multi-threaded shared ownership with locking
Both patterns allow mutation through immutable references, but Mutex<T>
includes deadlock risks when acquiring multiple locks.
Extensible Concurrency with the Send
and Sync
Traits
Rust’s concurrency safety is enforced through two marker traits: Send
and Sync
. These traits are implemented automatically for most types but can be manually implemented for custom concurrent types.
The Send
Trait
Send
indicates that ownership of a type can be transferred between threads safely. Almost all Rust types implement Send
, with notable exceptions:
Rc<T>
: NotSend
because non-atomic reference counting creates race conditions- Raw pointers: Lack safety guarantees
Types composed entirely of Send
types automatically implement Send
. When we attempted to send Rc<Mutex<i32>>
between threads in Listing 16-14, the compiler prevented this due to Rc<T>
’s lack of Send
implementation.
The Sync
Trait
Sync
indicates that a type is safe to access from multiple threads simultaneously. A type T
implements Sync
if &T
(an immutable reference to T
) implements Send
.
Types that don’t implement Sync
:
Rc<T>
: Non-atomic reference counting unsafe for concurrent accessRefCell<T>
andCell<T>
: Runtime borrow checking not thread-safeMutex<T>
: ImplementsSync
and enables multi-threaded shared access
Primitive types implement both Send
and Sync
. Types composed entirely of Send
and Sync
types automatically implement these traits.
Manual Implementation Safety
Manually implementing Send
and Sync
requires unsafe
code and careful consideration of concurrency invariants. Building new concurrent types outside the Send
/Sync
ecosystem demands deep understanding of thread safety guarantees.
For production systems, prefer established concurrent data structures from crates like crossbeam
or tokio
rather than implementing custom concurrent types.
Summary
Rust’s concurrency model combines:
- Channels: Message passing with ownership transfer
- Mutexes: Shared state with exclusive access
- Atomic types: Lock-free concurrent operations
- Send/Sync traits: Compile-time thread safety guarantees
The type system and borrow checker eliminate data races and memory safety issues at compile time, enabling fearless concurrency. Concurrent programming becomes a design decision rather than a source of runtime bugs.
Asynchronous Programming: Async, Await, Futures, and Streams
Modern applications require handling concurrent operations efficiently. Rust provides async programming through Futures, Streams, and the async
/await
syntax. This chapter covers async fundamentals and practical patterns for building concurrent systems.
You’ll learn:
async
/await
syntax and the Future trait- Concurrent programming patterns with async
- Working with multiple futures and streams
- Trait details:
Future
,Pin
,Unpin
,Stream
- Combining async with threads
Parallelism vs Concurrency
Concurrency: Single worker switches between tasks before completion (time-slicing). Parallelism: Multiple workers execute tasks simultaneously. Serial: Tasks must complete in sequence due to dependencies.
Concurrent: A1 → B1 → A2 → B2 → A3 → B3
Parallel: A1 → A2 → A3
B1 → B2 → B3
CPU-bound operations benefit from parallelism. IO-bound operations benefit from concurrency. Most real systems need both.
In async Rust, concurrency is primary. The runtime may use parallelism underneath, but your async code provides concurrency semantics.
Blocking vs Non-blocking
Traditional IO operations block execution until completion. Async operations yield control to the runtime when waiting, allowing other tasks to progress.
// Blocking - thread stops here
let data = std::fs::read("file.txt")?;
// Non-blocking - runtime can schedule other work
let data = tokio::fs::read("file.txt").await?;
Async enables writing sequential-looking code that’s actually concurrent:
let data = fetch_data_from(url).await;
println!("{data}");
This syntax compiles to a state machine that yields control at await points.
Futures and the Async Syntax
Rust’s async programming centers on futures - values that represent work that may complete later. The Future
trait provides a common interface for async operations, similar to Promises in JavaScript.
Key concepts:
async
functions/blocks return futuresawait
polls futures to completion- Futures are lazy - they do nothing until awaited
- Each await point is where execution can pause/resume
async fn fetch_title(url: &str) -> Option<String> {
let response = http_get(url).await; // Yield control here
let html = response.text().await; // And here
parse_title(&html)
}
Setup
Create a new project and add the trpl
crate, which provides runtime and utility functions:
$ cargo new hello-async && cd hello-async
$ cargo add trpl
First Async Program: Web Scraper
extern crate trpl; // required for mdbook test fn main() { // TODO: we'll add this next! } use trpl::Html; async fn page_title(url: &str) -> Option<String> { let response = trpl::get(url).await; let response_text = response.text().await; Html::parse(&response_text) .select_first("title") .map(|title_element| title_element.inner_html()) }
Key points:
async fn
returnsimpl Future<Output = ReturnType>
await
is postfix:future.await
notawait future
- Network and parsing operations yield control at await points
- Chaining with
await
creates readable async code:
extern crate trpl; // required for mdbook test use trpl::Html; fn main() { // TODO: we'll add this next! } async fn page_title(url: &str) -> Option<String> { let response_text = trpl::get(url).await.text().await; Html::parse(&response_text) .select_first("title") .map(|title_element| title_element.inner_html()) }
Async Function Compilation
The compiler transforms:
async fn page_title(url: &str) -> Option<String> { /* body */ }
Into roughly:
fn page_title(url: &str) -> impl Future<Output = Option<String>> { async move { /* body */ } }
Running Async Code
main
cannot be async
- you need a runtime to execute futures:
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title_element| title_element.inner_html())
}
Fix with trpl::run
:
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title_element| title_element.inner_html())
}
The runtime manages the state machine that Rust creates from async blocks. Each await point represents where the state machine can pause and resume.
Racing Multiple Futures
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title is: '{title}'"),
None => println!("Its title could not be parsed."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let text = trpl::get(url).await.text().await;
let title = Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
trpl::race
returns Either<Left(A), Right(B)>
indicating which future completed first. Unlike threads, the order of argument evaluation affects which future starts first (non-fair scheduling).
Runtime and State Machines
Async blocks compile to state machines. The runtime (executor) manages these machines:
enum PageTitleState { Start, WaitingForResponse, WaitingForText, Complete, }
At each await point, the future yields control back to the runtime. The runtime can then:
- Schedule other futures
- Handle IO completion
- Resume futures when their dependencies are ready
This cooperative multitasking requires explicit yield points (await) - CPU-intensive work without await points will block other futures.
Concurrency with Async
Async provides different APIs and performance characteristics compared to threads, but many patterns are similar. Key differences:
- Tasks are lightweight compared to OS threads
- Fair vs unfair scheduling
- Cooperative vs preemptive multitasking
Spawning Tasks
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { trpl::spawn_task(async { for i in 1..10 { println!("hi number {i} from the first task!"); trpl::sleep(Duration::from_millis(500)).await; } }); for i in 1..5 { println!("hi number {i} from the second task!"); trpl::sleep(Duration::from_millis(500)).await; } }); }
Like thread::spawn
, trpl::spawn_task
runs code concurrently. Unlike threads:
- Tasks run on the same thread (unless using a multi-threaded runtime)
- Much lower overhead - millions of tasks are feasible
- Terminated when the runtime shuts down
Joining Tasks
Tasks return join handles that implement Future
:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let handle = trpl::spawn_task(async { for i in 1..10 { println!("hi number {i} from the first task!"); trpl::sleep(Duration::from_millis(500)).await; } }); for i in 1..5 { println!("hi number {i} from the second task!"); trpl::sleep(Duration::from_millis(500)).await; } handle.await.unwrap(); }); }
Joining Multiple Futures
trpl::join
combines futures and waits for all to complete:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let fut1 = async { for i in 1..10 { println!("hi number {i} from the first task!"); trpl::sleep(Duration::from_millis(500)).await; } }; let fut2 = async { for i in 1..5 { println!("hi number {i} from the second task!"); trpl::sleep(Duration::from_millis(500)).await; } }; trpl::join(fut1, fut2).await; }); }
Key points about join
:
- Fair scheduling: alternates between futures equally
- Deterministic ordering: consistent execution pattern
- Structured concurrency: all futures complete before continuing
Compare this to threads where the OS scheduler determines execution order non-deterministically.
Message Passing
Async channels work similarly to sync channels but with async methods:
extern crate trpl; // required for mdbook test fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let val = String::from("hi"); tx.send(val).unwrap(); let received = rx.recv().await.unwrap(); println!("Got: {received}"); }); }
The recv
method returns a Future
that resolves when a message arrives.
Multiple Messages with Delays
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
This runs serially - all sends complete before receives start. For concurrency, separate into different futures:
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
Channel Cleanup
Channels need proper cleanup to avoid infinite loops. Move the sender into an async block so it gets dropped when sending completes:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_millis(500)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; trpl::join(tx_fut, rx_fut).await; }); }
The move
keyword transfers ownership into the async block. When the block completes, tx
is dropped, closing the channel.
Multiple Producers
Clone the sender for multiple producers:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_millis(500)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_millis(1500)).await; } }; trpl::join3(tx1_fut, tx_fut, rx_fut).await; }); }
Use trpl::join3
for three futures, or the join!
macro for variable numbers.
Patterns Summary
Task Spawning: spawn_task
for fire-and-forget concurrency
Joining: join
/join!
for structured concurrency waiting on all futures
Channels: Async message passing with trpl::channel
Cleanup: Use async move
to ensure proper resource cleanup
Working with Any Number of Futures
Variable-Arity Joins
The join!
macro handles variable numbers of futures:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; trpl::join!(tx1_fut, tx_fut, rx_fut); }); }
For dynamic collections of futures, use join_all
:
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures = vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
Error: Each async block creates a unique anonymous type. Even identical blocks have different types.
Trait Objects for Dynamic Collections
Use trait objects to unify future types:
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
Pin Requirements: join_all
requires futures to implement Unpin
. Most async blocks don’t implement Unpin
automatically.
Pinning Futures
Use Pin<Box<T>>
to satisfy the Unpin
requirement:
extern crate trpl; // required for mdbook test use std::pin::Pin; // -- snip -- use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> = vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)]; trpl::join_all(futures).await; }); }
Performance optimization: Use pin!
macro to avoid heap allocation:
extern crate trpl; // required for mdbook test use std::pin::{Pin, pin}; // -- snip -- use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = pin!(async move { // --snip-- let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }); let rx_fut = pin!(async { // --snip-- while let Some(value) = rx.recv().await { println!("received '{value}'"); } }); let tx_fut = pin!(async move { // --snip-- let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }); let futures: Vec<Pin<&mut dyn Future<Output = ()>>> = vec![tx1_fut, rx_fut, tx_fut]; trpl::join_all(futures).await; }); }
Tradeoffs
Same types + dynamic count: Use join_all
with trait objects and pinning
Different types + known count: Use join!
macro or specific join
functions
Performance: Stack pinning (pin!
) vs heap allocation (Box::pin
)
extern crate trpl; // required for mdbook test fn main() { trpl::run(async { let a = async { 1u32 }; let b = async { "Hello!" }; let c = async { true }; let (a_result, b_result, c_result) = trpl::join!(a, b, c); println!("{a_result}, {b_result}, {c_result}"); }); }
The join!
macro handles heterogeneous types but requires compile-time knowledge of count and types.
Racing Futures
race
returns the first completed future, discarding others:
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let slow = async { println!("'slow' started."); trpl::sleep(Duration::from_millis(100)).await; println!("'slow' finished."); }; let fast = async { println!("'fast' started."); trpl::sleep(Duration::from_millis(50)).await; println!("'fast' finished."); }; trpl::race(slow, fast).await; }); }
Key point: Futures only yield at await points. CPU-intensive work without await points will block other futures (cooperative multitasking).
Yielding Control
Force yield points with yield_now()
:
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { // We will call `slow` here later }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let a = async { println!("'a' started."); slow("a", 30); slow("a", 10); slow("a", 20); trpl::sleep(Duration::from_millis(50)).await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); slow("b", 10); slow("b", 15); slow("b", 350); trpl::sleep(Duration::from_millis(50)).await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let one_ms = Duration::from_millis(1); let a = async { println!("'a' started."); slow("a", 30); trpl::sleep(one_ms).await; slow("a", 10); trpl::sleep(one_ms).await; slow("a", 20); trpl::sleep(one_ms).await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); trpl::sleep(one_ms).await; slow("b", 10); trpl::sleep(one_ms).await; slow("b", 15); trpl::sleep(one_ms).await; slow("b", 350); trpl::sleep(one_ms).await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let a = async { println!("'a' started."); slow("a", 30); trpl::yield_now().await; slow("a", 10); trpl::yield_now().await; slow("a", 20); trpl::yield_now().await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); trpl::yield_now().await; slow("b", 10); trpl::yield_now().await; slow("b", 15); trpl::yield_now().await; slow("b", 350); trpl::yield_now().await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
Performance comparison: yield_now()
is significantly faster than sleep()
for cooperation points.
extern crate trpl; // required for mdbook test use std::time::{Duration, Instant}; fn main() { trpl::run(async { let one_ns = Duration::from_nanos(1); let start = Instant::now(); async { for _ in 1..1000 { trpl::sleep(one_ns).await; } } .await; let time = Instant::now() - start; println!( "'sleep' version finished after {} seconds.", time.as_secs_f32() ); let start = Instant::now(); async { for _ in 1..1000 { trpl::yield_now().await; } } .await; let time = Instant::now() - start; println!( "'yield' version finished after {} seconds.", time.as_secs_f32() ); }); }
Building Async Abstractions
Compose futures to create higher-level abstractions:
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_millis(100)).await;
"I finished!"
};
match timeout(slow, Duration::from_millis(10)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_millis(10)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
extern crate trpl; // required for mdbook test use std::time::Duration; use trpl::Either; // --snip-- fn main() { trpl::run(async { let slow = async { trpl::sleep(Duration::from_secs(5)).await; "Finally finished" }; match timeout(slow, Duration::from_secs(2)).await { Ok(message) => println!("Succeeded with '{message}'"), Err(duration) => { println!("Failed after {} seconds", duration.as_secs()) } } }); } async fn timeout<F: Future>( future_to_try: F, max_time: Duration, ) -> Result<F::Output, Duration> { match trpl::race(future_to_try, trpl::sleep(max_time)).await { Either::Left(output) => Ok(output), Either::Right(_) => Err(max_time), } }
Pattern: Use race
to implement timeout logic. First argument gets priority due to non-fair scheduling.
Key Patterns
- Dynamic collections: Trait objects + pinning for runtime-determined future sets
- Static collections:
join!
macro for compile-time known futures - Cooperation: Insert
yield_now()
in CPU-intensive loops - Composition: Build complex async behavior from simpler primitives
- Performance: Choose between stack (
pin!
) and heap (Box::pin
) allocation
Streams: Async Iterators
Streams provide async iteration over sequences of values. They’re the async equivalent of Iterator
, yielding items over time rather than immediately.
Core Concepts
Stream: Async equivalent of Iterator
StreamExt: Extension trait providing utility methods (like IteratorExt
)
Basic stream from iterator:
extern crate trpl; // required for mdbook test
fn main() {
trpl::run(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
Error: Missing StreamExt
trait for the next()
method.
extern crate trpl; // required for mdbook test use trpl::StreamExt; fn main() { trpl::run(async { let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let iter = values.iter().map(|n| n * 2); let mut stream = trpl::stream_from_iter(iter); while let Some(value) = stream.next().await { println!("The value was: {value}"); } }); }
Stream Processing
Streams support familiar iterator patterns:
extern crate trpl; // required for mdbook test use trpl::StreamExt; fn main() { trpl::run(async { let values = 1..101; let iter = values.map(|n| n * 2); let stream = trpl::stream_from_iter(iter); let mut filtered = stream.filter(|value| value % 3 == 0 || value % 5 == 0); while let Some(value) = filtered.next().await { println!("The value was: {value}"); } }); }
Real-time Data Streams
Streams excel at processing real-time data like WebSocket messages or event streams:
extern crate trpl; // required for mdbook test use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = get_messages(); while let Some(message) = messages.next().await { println!("{message}"); } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for message in messages { tx.send(format!("Message: '{message}'")).unwrap(); } ReceiverStream::new(rx) }
ReceiverStream
converts async channel receivers into streams.
Timeouts on Streams
Apply timeouts to individual stream items:
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for message in messages { tx.send(format!("Message: '{message}'")).unwrap(); } ReceiverStream::new(rx) }
Add delays to test timeout behavior:
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) }
Key point: Use spawn_task
to avoid blocking the stream creation. The async delays happen concurrently with stream consumption.
Interval Streams
Create periodic streams for regular events:
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; tx.send(count).unwrap(); } }); ReceiverStream::new(rx) }
Merging Streams
Combine multiple streams into one:
extern crate trpl; // required for mdbook test
use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals();
let merged = messages.merge(intervals);
while let Some(result) = merged.next().await {
match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
}
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
Error: Type mismatch between streams. Need to align types:
extern crate trpl; // required for mdbook test
use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals);
let mut stream = pin!(merged);
while let Some(result) = stream.next().await {
match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
}
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
Stream Rate Control
Control stream throughput with throttle
and limit with take
:
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval: {count}")) .throttle(Duration::from_millis(100)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; tx.send(count).unwrap(); } }); ReceiverStream::new(rx) }
Throttle: Limits polling rate, not just output filtering Take: Limits total number of items processed Lazy evaluation: Unprocessed items are never generated (performance benefit)
Error Handling
Handle stream errors gracefully:
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval #{count}")) .throttle(Duration::from_millis(500)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(item) => println!("{item}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; if let Err(send_error) = tx.send(format!("Message: '{message}'")) { eprintln!("Cannot send message '{message}': {send_error}"); break; } } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; if let Err(send_error) = tx.send(count) { eprintln!("Could not send interval {count}: {send_error}"); break; }; } }); ReceiverStream::new(rx) }
Stream Patterns
Data Processing: Transform async data with map
, filter
, fold
Rate Limiting: Use throttle
to control processing rate
Merging: Combine multiple event sources with merge
Timeouts: Apply deadlines to individual stream items
Backpressure: Control memory usage with take
and buffering
Key differences from synchronous iterators:
- Items arrive over time (not immediately available)
- Support timeouts and cancellation
- Enable concurrent processing of multiple streams
- Natural fit for event-driven architectures
Performance consideration: Streams are lazy - items are only produced when polled. This enables efficient resource usage and backpressure handling.
Async Traits Deep Dive
Understanding the underlying traits enables effective async programming and troubleshooting.
The Future Trait
use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; }
Key components:
Output
: The value produced when the future completespoll()
: State machine driver, called by the runtimePoll<T>
: EitherReady(T)
orPending
enum Poll<T> { Ready(T), Pending, }
Runtime interaction: await
compiles to repeated poll()
calls in a loop:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll(cx) {
Ready(value) => return value,
Pending => {
// Runtime schedules other work, resumes later
}
}
}
The Context
parameter enables runtime coordination - futures register wake-up conditions.
Pin and Unpin
Problem: Async state machines can contain self-references. Moving such structures breaks internal pointers.
// Simplified state machine representation struct AsyncStateMachine { state: State, data: String, reference_to_data: Option<&String>, // Self-reference! }
Solution: Pin<T>
prevents moving the pointed-to value.
// Pin prevents the inner value from moving let pinned: Pin<Box<AsyncStateMachine>> = Box::pin(state_machine);
Unpin
marker trait: Types safe to move even when pinned. Most types implement Unpin
automatically.
// Most types can be moved freely let string = String::from("hello"); let pinned_string = Pin::new(&mut string); // OK - String: Unpin // Self-referential futures need pinning let future = async { /* complex state machine */ }; let pinned_future = Box::pin(future); // Required for join_all()
Practical implications:
- Most code doesn’t deal with
Pin
directly join_all()
requiresUnpin
because it stores futures in collections- Use
Box::pin()
orpin!()
macro when required - Stack allocation (
pin!
) vs heap allocation (Box::pin
)
use std::pin::pin; // Stack pinning (preferred when lifetime allows) let fut = pin!(async_operation()); // Heap pinning (needed for collections or longer lifetimes) let fut = Box::pin(async_operation());
The Stream Trait
Streams combine Iterator
and Future
concepts:
use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll<Option<Self::Item>>; }
Comparison:
Iterator::next()
→Option<Item>
(synchronous)Future::poll()
→Poll<Output>
(async ready/pending)Stream::poll_next()
→Poll<Option<Item>>
(async sequence)
StreamExt trait: Provides high-level methods like next()
, map()
, filter()
:
// Simplified StreamExt implementation trait StreamExt: Stream { async fn next(&mut self) -> Option<Self::Item> where Self: Unpin { // Implementation uses poll_next() internally } fn map<F, U>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> U { // Returns a new stream that applies f to each item } }
Practical Guidelines
Future trait: Rarely implemented directly. Use async fn
and async {}
blocks.
Pin/Unpin:
- Most types are
Unpin
(can be moved freely) - Async blocks often require pinning for collections
- Use
pin!()
for stack allocation,Box::pin()
for heap
Stream trait:
- Use
StreamExt
methods for stream operations - Similar to iterator patterns but async
- Enables backpressure and cancellation
Error patterns:
// Error: Missing StreamExt let mut stream = some_stream(); let item = stream.next().await; // ❌ No method `next` // Fix: Import StreamExt use futures::StreamExt; let item = stream.next().await; // ✅ Works // Error: Unpin requirement let futures = vec![Box::new(async_block())]; // ❌ Unpin not satisfied // Fix: Pin the futures let futures = vec![Box::pin(async_block())]; // ✅ Works
When to use each:
- Future: For single async operations
- Stream: For async sequences over time
- Pin: When collections or trait objects are involved
- StreamExt: For stream transformations and operations
The traits form a coherent system where:
Future
provides the foundation for async operationsPin
enables safe self-referential state machinesStream
extends futures to sequences- Extension traits provide ergonomic APIs
Futures, Tasks, and Threads
Understanding when to use threads vs async requires considering their different tradeoffs and capabilities.
Execution Models
Threads:
- OS-managed, preemptive multitasking
- Heavy resource overhead (~2-8MB stack per thread)
- True parallelism on multi-core systems
- Context switches managed by OS scheduler
Tasks (Async):
- Runtime-managed, cooperative multitasking
- Lightweight (~KB overhead per task)
- Concurrency via yielding at await points
- Can run millions concurrently
Interchangeable APIs
Many patterns work with both models:
extern crate trpl; // required for mdbook test use std::{pin::pin, thread, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval #{count}")) .throttle(Duration::from_millis(500)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(item) => println!("{item}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; if let Err(send_error) = tx.send(format!("Message: '{message}'")) { eprintln!("Cannot send message '{message}': {send_error}"); break; } } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); // This is *not* `trpl::spawn` but `std::thread::spawn`! thread::spawn(move || { let mut count = 0; loop { // Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`! thread::sleep(Duration::from_millis(1)); count += 1; if let Err(send_error) = tx.send(count) { eprintln!("Could not send interval {count}: {send_error}"); break; }; } }); ReceiverStream::new(rx) }
Key differences:
thread::spawn
vstrpl::spawn_task
thread::sleep
vstrpl::sleep
- Both return handles that can be awaited/joined
- Same resulting streams despite different execution models
Architectural Hierarchy
Runtime (manages tasks)
├── Task 1 (manages futures)
│ ├── Future A
│ ├── Future B
│ └── Future C
├── Task 2 (manages futures)
│ └── Future D
└── OS Threads (work-stealing execution)
├── Thread 1
├── Thread 2
└── Thread 3
Three levels of concurrency:
- Futures: Finest granularity, cooperative yield points
- Tasks: Group related futures, runtime-scheduled
- Threads: OS-scheduled, can run tasks in parallel
Performance Characteristics
Threads excel at:
- CPU-intensive work (parallel computation)
- Blocking operations without async alternatives
- Fire-and-forget background work
- Simple parallelism requirements
Async excels at:
- IO-intensive operations (network, file system)
- High-concurrency scenarios (thousands of connections)
- Structured concurrency patterns
- Resource-constrained environments
Hybrid Patterns
Combine both approaches for optimal performance:
extern crate trpl; // for mdbook test use std::{thread, time::Duration}; fn main() { let (tx, mut rx) = trpl::channel(); thread::spawn(move || { for i in 1..11 { tx.send(i).unwrap(); thread::sleep(Duration::from_secs(1)); } }); trpl::run(async { while let Some(message) = rx.recv().await { println!("{message}"); } }); }
Pattern: Use threads for blocking operations, async channels for coordination.
Work-Stealing Runtimes
Modern async runtimes use thread pools with work-stealing:
┌─────────────────────────────────────────┐
│ Async Runtime │
├─────────────────────────────────────────┤
│ Task Queue: [Task1, Task2, Task3, ...] │
├─────────────────────────────────────────┤
│ Thread Pool: │
│ ├─ Worker Thread 1 ──┐ │
│ ├─ Worker Thread 2 ──┼─ Work Stealing │
│ ├─ Worker Thread 3 ──┘ │
│ └─ Worker Thread 4 │
└─────────────────────────────────────────┘
Tasks can migrate between threads for load balancing, combining the benefits of both models.
Decision Framework
Choose Threads when:
- Heavy CPU computation (image processing, mathematical calculations)
- Blocking C libraries without async bindings
- Simple parallel algorithms (embarrassingly parallel problems)
- Need guaranteed execution progress (real-time systems)
Choose Async when:
- Network servers handling many connections
- IO-heavy applications (file processing, database queries)
- Event-driven architectures
- Memory-constrained environments
Use Both when:
- Web applications (async for request handling, threads for background jobs)
- Data pipelines (async for coordination, threads for CPU processing)
- Gaming (async for network, threads for physics/rendering)
Cancellation and Cleanup
Threads: Limited cancellation support, manual cleanup
// Thread cancellation is tricky let handle = thread::spawn(|| { // No built-in cancellation mechanism loop { /* work */ } }); // Must use external signaling
Async: Built-in cancellation when futures are dropped
// Automatic cleanup when dropped let task = spawn_task(async { some_operation().await; // Cancelled if task is dropped }); drop(task); // Automatically cancels and cleans up
Performance Guidelines
Avoid:
- Creating threads for each IO operation (thread-per-request anti-pattern)
- Async for CPU-bound work without yield points
- Mixing sync and async code unnecessarily
Optimize:
- Use
spawn_blocking
for sync code in async contexts - Pool threads for repeated CPU work
- Batch async operations to reduce overhead
- Choose appropriate buffer sizes for channels
Summary
Threads: OS-managed parallelism, higher overhead, preemptive scheduling
Tasks: Runtime-managed concurrency, lightweight, cooperative scheduling
Futures: Building blocks for async state machines
The choice isn’t exclusive - modern applications often use both approaches where each excels. Async provides better resource utilization for IO-bound work, while threads provide true parallelism for CPU-bound tasks.
Best practice: Start with async for concurrency needs, add threads for parallelism where beneficial. Use work-stealing runtimes that provide both models efficiently.
Object-Oriented Programming Features
Rust supports some object-oriented patterns but differs significantly from traditional OOP languages. This chapter examines how Rust implements OOP concepts like encapsulation and polymorphism through its type system, and demonstrates implementing the state pattern both traditionally and idiomatically in Rust.
Key topics:
- Objects, encapsulation, and inheritance in Rust
- Trait objects for runtime polymorphism
- State pattern implementation and trade-offs
- Type-based alternatives to traditional OOP patterns
Characteristics of Object-Oriented Languages
Rust implements OOP concepts differently than traditional languages. Here’s how Rust handles the core OOP characteristics:
Objects: Data + Behavior
Rust structs and enums with impl
blocks provide the same functionality as objects - they package data and methods together:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Encapsulation
Rust uses pub
keyword for visibility control. By default, everything is private:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Public methods control access to private data:
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
Private fields prevent direct manipulation, ensuring data consistency. The internal implementation can change (e.g., from Vec<i32>
to HashSet<i32>
) without breaking external code.
Inheritance: Not Supported
Rust has no traditional inheritance. Instead:
For code reuse: Use default trait implementations:
trait Summary { fn summarize(&self) -> String { "Default implementation".to_string() } }
For polymorphism: Use trait objects or generics with trait bounds:
// Trait objects (dynamic dispatch) fn process(item: &dyn Summary) { } // Generics (static dispatch) fn process<T: Summary>(item: &T) { }
Rust’s approach avoids inheritance issues like tight coupling and fragile base class problems. Trait objects enable polymorphism where different types can be treated uniformly if they implement the same trait.
Using Trait Objects for Runtime Polymorphism
Trait objects enable storing different types in the same collection when they implement a common trait. They provide dynamic dispatch at runtime cost.
Basic Usage
Define a trait for common behavior:
pub trait Draw {
fn draw(&self);
}
Store trait objects using Box<dyn Trait>
:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Call methods on trait objects:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Trait Objects vs. Generics
Generics (monomorphization - static dispatch):
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
- All components must be the same type
- Zero-cost abstraction (compile-time dispatch)
- Code duplication for each concrete type
Trait Objects (dynamic dispatch):
- Mixed types in same collection
- Runtime cost for method lookup
- Single implementation in binary
Implementation Examples
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
Compile-time Safety
Rust enforces trait implementation at compile time:
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
Performance Trade-offs
Static Dispatch (generics):
- Faster execution (no runtime lookup)
- Larger binary size (code duplication)
- All types known at compile time
Dynamic Dispatch (trait objects):
- Runtime method lookup overhead
- Smaller binary size
- Enables heterogeneous collections
- Prevents some compiler optimizations
Choose based on your use case: use generics for performance-critical code with known types, trait objects for flexible APIs with mixed types.
Implementing Design Patterns in Rust
The state pattern encodes states as separate objects, with behavior changing based on internal state. This demonstrates two approaches: traditional OOP-style implementation and idiomatic Rust using the type system.
Traditional State Pattern
The blog post workflow requires:
- Draft → Review → Published transitions
- Only published posts display content
- Invalid transitions are ignored
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Implementation
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
State Transitions
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
Trade-offs of Traditional State Pattern
Advantages:
- State logic encapsulated in state objects
- Adding new states requires minimal changes
- Transitions managed internally
Disadvantages:
- States coupled to each other
- Logic duplication in transition methods
- Runtime overhead from trait objects
- Invalid states possible at runtime
Type-Based State Pattern
Rust’s type system can encode states as types, making invalid states impossible:
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
Type-Based Advantages
Compile-time Safety:
- Invalid states impossible
- Attempting to get content from draft fails to compile
- State transitions enforced by type system
Performance:
- No runtime state checks
- No trait object overhead
- Zero-cost abstractions
API Design:
- Clear ownership semantics
- Impossible to misuse API
- Self-documenting through types
When to Use Each Pattern
Traditional State Pattern:
- Need runtime flexibility
- States determined by external data
- Working with existing OOP codebases
Type-Based Pattern:
- Compile-time state validation required
- Performance critical code
- Taking advantage of Rust’s type system
The type-based approach demonstrates Rust’s strength: encoding invariants in the type system prevents entire classes of bugs at compile time, with zero runtime cost.
Patterns and Matching
Patterns are syntactic constructs for matching against data structure shapes and extracting values. They work with match
expressions, if let
, while let
, function parameters, and destructuring assignments.
Pattern components:
- Literals:
42
,"hello"
,true
- Destructuring:
(a, b)
,Some(x)
,Point { x, y }
- Variables:
x
,mut counter
- Wildcards:
_
,..
- Guards:
x if x > 0
This chapter covers pattern usage contexts, refutability (whether patterns can fail to match), and comprehensive pattern syntax. Understanding patterns is essential for idiomatic Rust code involving data extraction and control flow.
Pattern Usage Contexts
match
Arms
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
match
expressions must be exhaustive. Use _
as a catchall pattern:
match x {
None => None,
Some(i) => Some(i + 1),
}
Conditional if let
Expressions
More flexible than match
for single-pattern matching:
fn main() { let favorite_color: Option<&str> = None; let is_tuesday = false; let age: Result<u8, _> = "34".parse(); if let Some(color) = favorite_color { println!("Using your favorite color, {color}, as the background"); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); } }
if let
doesn’t check exhaustiveness, unlike match
. Can introduce shadowed variables within the conditional scope.
while let
Conditional Loops
fn main() { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { for val in [1, 2, 3] { tx.send(val).unwrap(); } }); while let Ok(value) = rx.recv() { println!("{value}"); } }
Continues until the pattern fails to match.
for
Loops
The pattern follows for
:
fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{value} is at index {index}"); } }
Output:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
let
Statements
Every let
statement uses a pattern:
let x = 5; // x is a pattern
Formal syntax: let PATTERN = EXPRESSION;
Destructuring example:
fn main() { let (x, y, z) = (1, 2, 3); }
Pattern elements must match exactly:
fn main() {
let (x, y) = (1, 2, 3);
}
Error:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Function Parameters
Function parameters are patterns:
fn foo(x: i32) { // code goes here } fn main() {}
Destructuring in function signatures:
fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({x}, {y})"); } fn main() { let point = (3, 5); print_coordinates(&point); }
Prints: Current location: (3, 5)
Closure parameters work the same way as function parameters.
Refutability: Whether a Pattern Might Fail to Match
Patterns are either irrefutable (always match) or refutable (can fail to match).
Irrefutable Patterns
x
inlet x = 5
(a, b)
inlet (a, b) = (1, 2)
- Always succeed, can’t fail
Refutable Patterns
Some(x)
inif let Some(x) = value
- Can fail if value doesn’t match expected shape
Context Requirements
Irrefutable patterns only:
let
statements- Function parameters
for
loops
Refutable patterns accepted:
if let
expressionswhile let
expressionsmatch
arms (except final arm)
Common Errors
Using refutable pattern with let
:
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
Error:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Fix with let...else
:
fn main() { let some_option_value: Option<i32> = None; let Some(x) = some_option_value else { return; }; }
Using irrefutable pattern with if let
:
fn main() { let x = 5 else { return; }; }
Warning:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
--> src/main.rs:2:5
|
2 | let x = 5 else {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `else` clause is useless
= help: consider removing the `else` clause
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
Summary: Use the right pattern type for the context. The compiler enforces these rules to prevent logic errors.
Pattern Syntax
Matching Literals
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
Named Variables
Variables in patterns shadow outer scope variables:
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
The inner y
shadows the outer y
within the match arm scope.
Multiple Patterns
Use |
for multiple options:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
Ranges with ..=
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
Character ranges:
fn main() { let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
Destructuring
Structs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); }
Shorthand syntax:
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); }
Combining with literals:
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } }
Enums
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to destructure."); } Message::Move { x, y } => { println!("Move in the x direction {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change color to red {r}, green {g}, and blue {b}"); } } }
Nested Structures
enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32), } enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color), } fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!("Change color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}"); } _ => (), } }
Structs and Tuples Combined
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
Ignoring Values
Entire Value with _
fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {y}"); } fn main() { foo(3, 4); }
Parts of Values with Nested _
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {setting_value:?}"); }
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}"); } } }
Variables Starting with _
fn main() { let _x = 5; let y = 10; }
Important difference: _x
binds the value, _
doesn’t:
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
Remaining Parts with ..
fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {x}"), } }
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
Ambiguous usage fails:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
Error:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Match Guards
Additional if
conditions in match arms:
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {x} is even"), Some(x) => println!("The number {x} is odd"), None => (), } }
Match guards solve variable shadowing:
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(n) if n == y => println!("Matched, n = {n}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
Multiple patterns with guards:
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
The guard applies to all patterns: (4 | 5 | 6) if y
, not 4 | 5 | (6 if y)
.
@
Bindings
Bind a value while testing it:
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7, } => println!("Found an id in range: {id_variable}"), Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {id}"), } }
@
lets you capture a value that matches a range or other complex pattern for use in the associated code block.
Summary
Patterns provide powerful data extraction and control flow capabilities. Key features:
- Destructuring: Extract values from complex data structures
- Refutability: Compiler ensures pattern safety in different contexts
- Guards: Add conditional logic to patterns
- Binding: Capture values with
@
while pattern matching - Ignoring: Use
_
and..
to ignore unneeded values
Mastering patterns is essential for idiomatic Rust code involving enums, structs, and control flow.
Advanced Features
Advanced Rust features for low-level control and systems programming scenarios:
- Unsafe Rust: Raw pointers, FFI, static variables
- Advanced traits: Associated types, disambiguation, supertraits
- Advanced types: Type aliases, never type, DSTs
- Functions/closures: Function pointers, returning closures
- Macros: Compile-time code generation
Use when standard abstractions are insufficient for performance or interop requirements.
Unsafe Rust
Bypass Rust’s compile-time safety checks for low-level operations, FFI, and performance-critical code.
Unsafe Operations
Five operations require unsafe
blocks:
unsafe { // Dereference raw pointers // Call unsafe functions // Access/modify mutable statics // Implement unsafe traits // Access union fields }
Raw Pointers
let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; unsafe { println!("r1: {}", *r1); // Dereferencing requires unsafe }
Key differences from references:
- No borrow checking (multiple mutable pointers allowed)
- Can be null or point to invalid memory
- No automatic cleanup
Safe Abstractions
Wrap unsafe code in safe APIs:
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { assert!(mid <= values.len()); unsafe { let ptr = values.as_mut_ptr(); ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), values.len() - mid), ) } }
FFI (Foreign Function Interface)
extern "C" { fn abs(input: i32) -> i32; } unsafe { println!("Absolute value: {}", abs(-3)); }
Exporting to C:
#[no_mangle] pub extern "C" fn call_from_c() { println!("Called from C!"); }
Static Variables
static mut COUNTER: usize = 0; unsafe { COUNTER += 1; println!("COUNTER: {}", COUNTER); }
Unsafe Traits
unsafe trait Foo { // ... } unsafe impl Foo for i32 { // ... }
Unions
union MyUnion { f1: u32, f2: f32, } let u = MyUnion { f1: 1 }; unsafe { match u { MyUnion { f1: i } => println!("i32: {}", i), } }
Miri Validation
Detect undefined behavior in unsafe code:
cargo +nightly miri run
cargo +nightly miri test
Best Practices
- Minimize scope: Keep unsafe blocks small and focused
- Document invariants: Use
SAFETY:
comments explaining preconditions - Encapsulate: Don’t expose unsafe operations in public APIs
- Test thoroughly: Use Miri and comprehensive test coverage
- Validate inputs: Assert preconditions before unsafe operations
Unsafe Rust enables FFI, zero-cost abstractions, and performance optimizations where safety checks are insufficient.
Advanced Traits
Associated Types vs Generics
Associated types enforce single implementation per type:
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } impl Iterator for Counter { type Item = u32; // Only one Item type allowed // ... }
Generics allow multiple implementations:
trait Iterator<T> { fn next(&mut self) -> Option<T>; } // Could implement Iterator<u32>, Iterator<String>, etc.
Default Generic Type Parameters
trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } // Default: Point + Point impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { /* ... */ } } // Custom: Point + Meters impl Add<Meters> for Point { type Output = Point; fn add(self, other: Meters) -> Point { /* ... */ } }
Use cases: Extending traits without breaking existing code, mixed-type operations.
Method Disambiguation
Multiple traits, same method names:
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("Flying a plane"); } } impl Wizard for Human { fn fly(&self) { println!("Flying with magic"); } } let person = Human; Pilot::fly(&person); // Explicit trait Wizard::fly(&person); // Explicit trait
Associated functions (no self
):
println!("{}", <Dog as Animal>::baby_name()); // Fully qualified syntax
Supertraits
Require implementing one trait before another:
trait OutlinePrint: fmt::Display { fn outline_print(&self) { println!("* {} *", self); // Can use Display::fmt } } // Must implement Display before OutlinePrint impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } }
Newtype Pattern
Circumvent orphan rule (implement external traits on external types):
struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w);
Additional benefits:
- Type safety:
UserId(u32)
vsProductId(u32)
- Zero runtime cost
- Controlled API surface
Access wrapped value with Deref
:
impl Deref for Wrapper { type Target = Vec<String>; fn deref(&self) -> &Self::Target { &self.0 } }
Advanced Types
Newtype Pattern for Type Safety
Compile-time type safety and abstraction:
struct UserId(u32); struct ProductId(u32); fn get_user(id: UserId) -> User { /* ... */ } // get_user(ProductId(123)); // Compile error
Use cases: Units (Kilometers
, Miles
), IDs, domain modeling, API abstraction.
Type Aliases
Convenience syntax for complex types:
type Kilometers = i32; // No type safety type Thunk = Box<dyn Fn() + Send + 'static>; // Result alias pattern (std::io uses this) type Result<T> = std::result::Result<T, std::io::Error>; fn write_data() -> Result<()> { /* ... */ }
Never Type (!
)
Represents computations that never return:
fn never_returns() -> ! { panic!("This function never returns!"); } // Key property: ! coerces to any type let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, // continue has type !, coerces to u32 };
Other !
expressions: panic!
, loop {}
, process::exit()
.
Dynamically Sized Types (DSTs)
Types with unknown compile-time size:
// str is a DST (not &str) let s1: &str = "hello"; // OK: reference with size info let s2: Box<str> = "hello".into(); // OK: smart pointer // Won't compile - unknown size // let s3: str = "hello";
Common DSTs: str
, [T]
, dyn Trait
.
Sized
Trait
Generic functions implicitly require Sized
:
fn generic<T>(t: T) {} // Actually: fn generic<T: Sized>(t: T) fn flexible<T: ?Sized>(t: &T) {} // ?Sized relaxes the bound
?Sized
means “maybe sized” - requires using T
behind a pointer (&T
, Box<T>
).
Advanced Functions and Closures
Function Pointers vs Closures
Function pointers (fn
) are different from closure traits (Fn
, FnMut
, FnOnce
):
fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } let answer = do_twice(add_one, 5); // Function pointer
Key differences:
fn
is a type, not a trait- Function pointers implement all closure traits
- Useful for FFI (C functions don’t have closures)
Practical Examples
Map with function vs closure:
let list = vec![1, 2, 3]; let list2: Vec<String> = list.iter().map(ToString::to_string).collect();
Enum constructors as function pointers:
enum Status { Value(u32) } let list: Vec<Status> = (0u32..20).map(Status::Value).collect();
Returning Closures
Simple case with impl Trait
:
fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 }
Multiple closures require trait objects (each closure has unique type):
fn returns_closure(condition: bool) -> Box<dyn Fn(i32) -> i32> { if condition { Box::new(|x| x + 1) } else { Box::new(|x| x * 2) } }
JavaScript/TypeScript Comparison
JavaScript: Functions are first-class objects with uniform representation:
function add(x) { return x + 1; }
const multiply = (x) => x * 2;
const funcs = [add, multiply]; // Same type
Rust: Functions and closures have distinct types based on captures:
fn add(x: i32) -> i32 { x + 1 } let multiplier = 2; let multiply = |x| x * multiplier; // Different type due to capture // Need trait objects for heterogeneous storage let funcs: Vec<Box<dyn Fn(i32) -> i32>> = vec![ Box::new(add), Box::new(multiply), ];
Rust’s approach enables zero-cost abstractions - closures only pay for what they capture.
Macros
Compile-time metaprogramming through declarative and procedural macros.
Macros vs Functions
Macros: Variable parameters, operate on tokens, generate code at compile-time Functions: Fixed parameters, runtime execution, typed arguments
Declarative Macros (macro_rules!
)
Pattern match on code structure:
macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; }
Pattern syntax:
$x:expr
captures expressions$( ... ),*
captures repeated patterns with,
separator$()*
repeats code for each match
Procedural Macros
Transform TokenStream
→ TokenStream
. Three types:
1. Custom derive
Macros
#[derive(HelloMacro)] struct Pancakes; fn main() { Pancakes::hello_macro(); }
Implementation:
#[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { let ast = syn::parse(input).unwrap(); impl_hello_macro(&ast) }
2. Attribute-like Macros
#[route(GET, "/")] fn index() {}
Signature:
#[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream
3. Function-like Macros
let sql = sql!(SELECT * FROM posts WHERE id=1);
Signature:
#[proc_macro] pub fn sql(input: TokenStream) -> TokenStream
Key Crates
syn
: Parse Rust code into ASTquote
: Generate Rust code from templatesproc_macro
: Compiler API for token manipulation
Practical Applications
Declarative: Domain-specific syntax, boilerplate reduction, type-safe builders Procedural: ORM derive macros, framework attributes, compile-time validation
Performance: All expansion happens at compile-time with zero runtime cost.
Final Project: Building a Multithreaded Web Server
Build a basic HTTP server from scratch demonstrating systems programming concepts.
Project Components
- TCP connection handling and HTTP request/response parsing
- Thread pool implementation for concurrent request processing
- Low-level networking without frameworks
Production note: Use hyper
, warp
, or axum
for real applications.
Implementation note: We use threads rather than async/await to focus on concurrency fundamentals, though many async runtimes employ thread pools internally.
Building a Single-Threaded Web Server
TCP Connection Handling
use std::net::TcpListener; let listener = TcpListener::bind("127.0.0.1:7878")?; for stream in listener.incoming() { let stream = stream?; handle_connection(stream); }
Similar to Node.js server.listen()
, but blocking by default.
HTTP Request Structure
GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0...
[optional body]
- Method + URI + Version
- Headers until empty line
- Optional body
Basic Response
fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer)?; let response = "HTTP/1.1 200 OK\r\n\r\nHello, World!"; stream.write(response.as_bytes())?; stream.flush()?; }
Serving Files
fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer)?; let get = b"GET / HTTP/1.1\r\n"; let (status_line, filename) = if buffer.starts_with(get) { ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename)?; let response = format!( "{}\r\nContent-Length: {}\r\n\r\n{}", status_line, contents.len(), contents ); stream.write(response.as_bytes())?; stream.flush()?; }
Performance Limitation
// Simulate slow endpoint if buffer.starts_with(b"GET /sleep HTTP/1.1\r\n") { thread::sleep(Duration::from_secs(5)); }
Problem: Single-threaded blocking - all requests wait for slow operations.
Node.js difference: Event loop handles I/O asynchronously on single thread, while this blocks the entire server.
Next: Thread pool for concurrent request handling.
Turning Our Single-Threaded Server into a Multithreaded Server
Problem: Sequential Request Processing
The current server blocks on each request:
// /sleep endpoint demonstrates the blocking issue if buffer.starts_with(b"GET /sleep HTTP/1.1\r\n") { thread::sleep(Duration::from_secs(5)); }
Issue: All requests wait behind slow operations. Unlike Node.js’s event loop or async frameworks, this blocks the entire server.
Solution: Thread Pool Pattern
Thread pool benefits:
- Fixed number of worker threads (prevents DoS via thread exhaustion)
- Work queue distributes tasks across available workers
- Graceful handling of concurrent requests
Implementation
1. Basic Threading (Naive Approach)
use std::thread; for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream); }); }
Problem: Unlimited thread creation can exhaust system resources.
2. ThreadPool API Design
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
3. ThreadPool Implementation
Core structure:
use std::sync::{mpsc, Arc, Mutex}; pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, } struct Worker { id: usize, thread: thread::JoinHandle<()>, } type Job = Box<dyn FnOnce() + Send + 'static>;
Constructor with validation:
impl ThreadPool { pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender: Some(sender), } } }
Job execution:
impl ThreadPool { pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } }
Worker implementation:
impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let job = receiver.lock().unwrap().recv().unwrap(); println!("Worker {id} got a job; executing."); job(); }); Worker { id, thread } } }
Key Architecture Patterns
Channel-based work distribution:
ThreadPool
holds sender- Workers share receiver via
Arc<Mutex<Receiver>>
Mutex
ensures single worker processes each job
Type safety:
Job
type alias for boxed closuresSend + 'static
bounds ensure thread safetyFnOnce
trait for single execution
Resource management:
- Pre-allocated worker threads
- Bounded concurrency (DoS protection)
- Work stealing via shared queue
Performance Characteristics
vs Single-threaded: Concurrent request handling vs Unlimited threading: Bounded resource usage vs Async I/O: More memory per connection, but simpler mental model vs Node.js: Multiple OS threads vs single-threaded event loop
Note: Modern async runtimes (tokio, async-std) often use similar thread pool patterns internally for CPU-bound work.
Graceful Shutdown and Cleanup
Problem: Resource Cleanup
Current thread pool implementation doesn’t clean up resources when dropping. Worker threads continue running indefinitely, preventing graceful shutdown.
Solution: Implement Drop
Trait
1. Basic Drop Implementation
impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } }
Problem: Workers are blocked on recv()
calls - join()
waits indefinitely.
2. Signaling Shutdown
Close channel to signal workers:
impl Drop for ThreadPool { fn drop(&mut self) { // Drop sender to close channel drop(self.sender.take()); // Wait for workers to finish for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } }
Update Worker to handle channel closure:
impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let message = receiver.lock().unwrap().recv(); match message { Ok(job) => { println!("Worker {id} got a job; executing."); job(); } Err(_) => { println!("Worker {id} disconnected; shutting down."); break; } } }); Worker { id, thread: Some(thread) } } }
3. Updated Data Structures
ThreadPool with Optional sender:
pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, }
Worker with Optional thread:
struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, }
Complete Implementation
ThreadPool:
impl ThreadPool { pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender: Some(sender), } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender.take()); for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } }
Testing Graceful Shutdown
Limited server for testing:
let pool = ThreadPool::new(4); for stream in listener.incoming().take(2) { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } println!("Shutting down.");
Expected output:
Worker 0 got a job; executing.
Worker 1 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 0 disconnected; shutting down.
Worker 1 disconnected; shutting down.
Shutting down worker 1
...
Key Patterns
Resource cleanup sequence:
- Signal shutdown (close channel)
- Wait for workers to finish current tasks
- Join all worker threads
Channel closure semantics:
- Dropping sender closes channel
recv()
returnsErr
when channel closed- Workers exit loop on channel closure
RAII (Resource Acquisition Is Initialization):
Drop
trait ensures cleanup on scope exit- Automatic resource management
- Exception safety through destructors
Production considerations:
- Timeout for join operations
- Graceful vs forceful shutdown
- Signal handling for shutdown triggers
Appendix
The following sections contain reference material you may find useful in your Rust journey.
Appendix A: Keywords
The following list contains keywords that are reserved for current or future use by the Rust language. As such, they cannot be used as identifiers (except as raw identifiers as we’ll discuss in the “Raw Identifiers” section). Identifiers are names of functions, variables, parameters, struct fields, modules, crates, constants, macros, static values, attributes, types, traits, or lifetimes.
Keywords Currently in Use
The following is a list of keywords currently in use, with their functionality described.
as
- perform primitive casting, disambiguate the specific trait containing an item, or rename items inuse
statementsasync
- return aFuture
instead of blocking the current threadawait
- suspend execution until the result of aFuture
is readybreak
- exit a loop immediatelyconst
- define constant items or constant raw pointerscontinue
- continue to the next loop iterationcrate
- in a module path, refers to the crate rootdyn
- dynamic dispatch to a trait objectelse
- fallback forif
andif let
control flow constructsenum
- define an enumerationextern
- link an external function or variablefalse
- Boolean false literalfn
- define a function or the function pointer typefor
- loop over items from an iterator, implement a trait, or specify a higher-ranked lifetimeif
- branch based on the result of a conditional expressionimpl
- implement inherent or trait functionalityin
- part offor
loop syntaxlet
- bind a variableloop
- loop unconditionallymatch
- match a value to patternsmod
- define a modulemove
- make a closure take ownership of all its capturesmut
- denote mutability in references, raw pointers, or pattern bindingspub
- denote public visibility in struct fields,impl
blocks, or modulesref
- bind by referencereturn
- return from functionSelf
- a type alias for the type we are defining or implementingself
- method subject or current modulestatic
- global variable or lifetime lasting the entire program executionstruct
- define a structuresuper
- parent module of the current moduletrait
- define a traittrue
- Boolean true literaltype
- define a type alias or associated typeunion
- define a union; is only a keyword when used in a union declarationunsafe
- denote unsafe code, functions, traits, or implementationsuse
- bring symbols into scope; specify precise captures for generic and lifetime boundswhere
- denote clauses that constrain a typewhile
- loop conditionally based on the result of an expression
Keywords Reserved for Future Use
The following keywords do not yet have any functionality but are reserved by Rust for potential future use.
abstract
become
box
do
final
gen
macro
override
priv
try
typeof
unsized
virtual
yield
Raw Identifiers
Raw identifiers are the syntax that lets you use keywords where they wouldn’t
normally be allowed. You use a raw identifier by prefixing a keyword with r#
.
For example, match
is a keyword. If you try to compile the following function
that uses match
as its name:
Filename: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
you’ll get this error:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
The error shows that you can’t use the keyword match
as the function
identifier. To use match
as a function name, you need to use the raw
identifier syntax, like this:
Filename: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool { haystack.contains(needle) } fn main() { assert!(r#match("foo", "foobar")); }
This code will compile without any errors. Note the r#
prefix on the function
name in its definition as well as where the function is called in main
.
Raw identifiers allow you to use any word you choose as an identifier, even if
that word happens to be a reserved keyword. This gives us more freedom to choose
identifier names, as well as lets us integrate with programs written in a
language where these words aren’t keywords. In addition, raw identifiers allow
you to use libraries written in a different Rust edition than your crate uses.
For example, try
isn’t a keyword in the 2015 edition but is in the 2018, 2021,
and 2024 editions. If you depend on a library that is written using the 2015
edition and has a try
function, you’ll need to use the raw identifier syntax,
r#try
in this case, to call that function from your code on later editions.
See Appendix E for more information on editions.
Appendix B: Operators and Symbols
This appendix contains a glossary of Rust’s syntax, including operators and other symbols that appear by themselves or in the context of paths, generics, trait bounds, macros, attributes, comments, tuples, and brackets.
Operators
Table B-1 contains the operators in Rust, an example of how the operator would appear in context, a short explanation, and whether that operator is overloadable. If an operator is overloadable, the relevant trait to use to overload that operator is listed.
Table B-1: Operators
Operator | Example | Explanation | Overloadable? |
---|---|---|---|
! | ident!(...) , ident!{...} , ident![...] | Macro expansion | |
! | !expr | Bitwise or logical complement | Not |
!= | expr != expr | Nonequality comparison | PartialEq |
% | expr % expr | Arithmetic remainder | Rem |
%= | var %= expr | Arithmetic remainder and assignment | RemAssign |
& | &expr , &mut expr | Borrow | |
& | &type , &mut type , &'a type , &'a mut type | Borrowed pointer type | |
& | expr & expr | Bitwise AND | BitAnd |
&= | var &= expr | Bitwise AND and assignment | BitAndAssign |
&& | expr && expr | Short-circuiting logical AND | |
* | expr * expr | Arithmetic multiplication | Mul |
*= | var *= expr | Arithmetic multiplication and assignment | MulAssign |
* | *expr | Dereference | Deref |
* | *const type , *mut type | Raw pointer | |
+ | trait + trait , 'a + trait | Compound type constraint | |
+ | expr + expr | Arithmetic addition | Add |
+= | var += expr | Arithmetic addition and assignment | AddAssign |
, | expr, expr | Argument and element separator | |
- | - expr | Arithmetic negation | Neg |
- | expr - expr | Arithmetic subtraction | Sub |
-= | var -= expr | Arithmetic subtraction and assignment | SubAssign |
-> | fn(...) -> type , |…| -> type | Function and closure return type | |
. | expr.ident | Field access | |
. | expr.ident(expr, ...) | Method call | |
. | expr.0 , expr.1 , etc. | Tuple indexing | |
.. | .. , expr.. , ..expr , expr..expr | Right-exclusive range literal | PartialOrd |
..= | ..=expr , expr..=expr | Right-inclusive range literal | PartialOrd |
.. | ..expr | Struct literal update syntax | |
.. | variant(x, ..) , struct_type { x, .. } | “And the rest” pattern binding | |
... | expr...expr | (Deprecated, use ..= instead) In a pattern: inclusive range pattern | |
/ | expr / expr | Arithmetic division | Div |
/= | var /= expr | Arithmetic division and assignment | DivAssign |
: | pat: type , ident: type | Constraints | |
: | ident: expr | Struct field initializer | |
: | 'a: loop {...} | Loop label | |
; | expr; | Statement and item terminator | |
; | [...; len] | Part of fixed-size array syntax | |
<< | expr << expr | Left-shift | Shl |
<<= | var <<= expr | Left-shift and assignment | ShlAssign |
< | expr < expr | Less than comparison | PartialOrd |
<= | expr <= expr | Less than or equal to comparison | PartialOrd |
= | var = expr , ident = type | Assignment/equivalence | |
== | expr == expr | Equality comparison | PartialEq |
=> | pat => expr | Part of match arm syntax | |
> | expr > expr | Greater than comparison | PartialOrd |
>= | expr >= expr | Greater than or equal to comparison | PartialOrd |
>> | expr >> expr | Right-shift | Shr |
>>= | var >>= expr | Right-shift and assignment | ShrAssign |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Bitwise exclusive OR | BitXor |
^= | var ^= expr | Bitwise exclusive OR and assignment | BitXorAssign |
| | pat | pat | Pattern alternatives | |
| | expr | expr | Bitwise OR | BitOr |
|= | var |= expr | Bitwise OR and assignment | BitOrAssign |
|| | expr || expr | Short-circuiting logical OR | |
? | expr? | Error propagation |
Non-operator Symbols
The following list contains all symbols that don’t function as operators; that is, they don’t behave like a function or method call.
Table B-2 shows symbols that appear on their own and are valid in a variety of locations.
Table B-2: Stand-Alone Syntax
Symbol | Explanation |
---|---|
'ident | Named lifetime or loop label |
...u8 , ...i32 , ...f64 , ...usize , etc. | Numeric literal of specific type |
"..." | String literal |
r"..." , r#"..."# , r##"..."## , etc. | Raw string literal, escape characters not processed |
b"..." | Byte string literal; constructs an array of bytes instead of a string |
br"..." , br#"..."# , br##"..."## , etc. | Raw byte string literal, combination of raw and byte string literal |
'...' | Character literal |
b'...' | ASCII byte literal |
|…| expr | Closure |
! | Always empty bottom type for diverging functions |
_ | “Ignored” pattern binding; also used to make integer literals readable |
Table B-3 shows symbols that appear in the context of a path through the module hierarchy to an item.
Table B-3: Path-Related Syntax
Symbol | Explanation |
---|---|
ident::ident | Namespace path |
::path | Path relative to the extern prelude, where all other crates are rooted (i.e., an explicitly absolute path including crate name) |
self::path | Path relative to the current module (i.e., an explicitly relative path). |
super::path | Path relative to the parent of the current module |
type::ident , <type as trait>::ident | Associated constants, functions, and types |
<type>::... | Associated item for a type that cannot be directly named (e.g., <&T>::... , <[T]>::... , etc.) |
trait::method(...) | Disambiguating a method call by naming the trait that defines it |
type::method(...) | Disambiguating a method call by naming the type for which it’s defined |
<type as trait>::method(...) | Disambiguating a method call by naming the trait and type |
Table B-4 shows symbols that appear in the context of using generic type parameters.
Table B-4: Generics
Symbol | Explanation |
---|---|
path<...> | Specifies parameters to generic type in a type (e.g., Vec<u8> ) |
path::<...> , method::<...> | Specifies parameters to generic type, function, or method in an expression; often referred to as turbofish (e.g., "42".parse::<i32>() ) |
fn ident<...> ... | Define generic function |
struct ident<...> ... | Define generic structure |
enum ident<...> ... | Define generic enumeration |
impl<...> ... | Define generic implementation |
for<...> type | Higher-ranked lifetime bounds |
type<ident=type> | A generic type where one or more associated types have specific assignments (e.g., Iterator<Item=T> ) |
Table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds.
Table B-5: Trait Bound Constraints
Symbol | Explanation |
---|---|
T: U | Generic parameter T constrained to types that implement U |
T: 'a | Generic type T must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a ) |
T: 'static | Generic type T contains no borrowed references other than 'static ones |
'b: 'a | Generic lifetime 'b must outlive lifetime 'a |
T: ?Sized | Allow generic type parameter to be a dynamically sized type |
'a + trait , trait + trait | Compound type constraint |
Table B-6 shows symbols that appear in the context of calling or defining macros and specifying attributes on an item.
Table B-6: Macros and Attributes
Symbol | Explanation |
---|---|
#[meta] | Outer attribute |
#![meta] | Inner attribute |
$ident | Macro substitution |
$ident:kind | Macro capture |
$(…)… | Macro repetition |
ident!(...) , ident!{...} , ident![...] | Macro invocation |
Table B-7 shows symbols that create comments.
Table B-7: Comments
Symbol | Explanation |
---|---|
// | Line comment |
//! | Inner line doc comment |
/// | Outer line doc comment |
/*...*/ | Block comment |
/*!...*/ | Inner block doc comment |
/**...*/ | Outer block doc comment |
Table B-8 shows the contexts in which parentheses are used.
Table B-8: Parentheses
Symbol | Explanation |
---|---|
() | Empty tuple (aka unit), both literal and type |
(expr) | Parenthesized expression |
(expr,) | Single-element tuple expression |
(type,) | Single-element tuple type |
(expr, ...) | Tuple expression |
(type, ...) | Tuple type |
expr(expr, ...) | Function call expression; also used to initialize tuple struct s and tuple enum variants |
Table B-9 shows the contexts in which curly braces are used.
Table B-9: Curly Brackets
Context | Explanation |
---|---|
{...} | Block expression |
Type {...} | struct literal |
Table B-10 shows the contexts in which square brackets are used.
Table B-10: Square Brackets
Context | Explanation |
---|---|
[...] | Array literal |
[expr; len] | Array literal containing len copies of expr |
[type; len] | Array type containing len instances of type |
expr[expr] | Collection indexing. Overloadable (Index , IndexMut ) |
expr[..] , expr[a..] , expr[..b] , expr[a..b] | Collection indexing pretending to be collection slicing, using Range , RangeFrom , RangeTo , or RangeFull as the “index” |
Appendix C: Derivable Traits
In various places in the book, we’ve discussed the derive
attribute, which
you can apply to a struct or enum definition. The derive
attribute generates
code that will implement a trait with its own default implementation on the
type you’ve annotated with the derive
syntax.
In this appendix, we provide a reference of all the traits in the standard
library that you can use with derive
. Each section covers:
- What operators and methods deriving this trait will enable
- What the implementation of the trait provided by
derive
does - What implementing the trait signifies about the type
- The conditions in which you’re allowed or not allowed to implement the trait
- Examples of operations that require the trait
If you want different behavior from that provided by the derive
attribute,
consult the standard library documentation
for each trait for details on how to manually implement them.
The traits listed here are the only ones defined by the standard library that
can be implemented on your types using derive
. Other traits defined in the
standard library don’t have sensible default behavior, so it’s up to you to
implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display
, which handles
formatting for end users. You should always consider the appropriate way to
display a type to an end user. What parts of the type should an end user be
allowed to see? What parts would they find relevant? What format of the data
would be most relevant to them? The Rust compiler doesn’t have this insight, so
it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
libraries can implement derive
for their own traits, making the list of
traits you can use derive
with truly open-ended. Implementing derive
involves using a procedural macro, which is covered in the
“Macros” section of Chapter 20.
Debug
for Programmer Output
The Debug
trait enables debug formatting in format strings, which you
indicate by adding :?
within {}
placeholders.
The Debug
trait allows you to print instances of a type for debugging
purposes, so you and other programmers using your type can inspect an instance
at a particular point in a program’s execution.
The Debug
trait is required, for example, in the use of the assert_eq!
macro. This macro prints the values of instances given as arguments if the
equality assertion fails so programmers can see why the two instances weren’t
equal.
PartialEq
and Eq
for Equality Comparisons
The PartialEq
trait allows you to compare instances of a type to check for
equality and enables use of the ==
and !=
operators.
Deriving PartialEq
implements the eq
method. When PartialEq
is derived on
structs, two instances are equal only if all fields are equal, and the
instances are not equal if any fields are not equal. When derived on enums,
each variant is equal to itself and not equal to the other variants.
The PartialEq
trait is required, for example, with the use of the
assert_eq!
macro, which needs to be able to compare two instances of a type
for equality.
The Eq
trait has no methods. Its purpose is to signal that for every value of
the annotated type, the value is equal to itself. The Eq
trait can only be
applied to types that also implement PartialEq
, although not all types that
implement PartialEq
can implement Eq
. One example of this is floating point
number types: the implementation of floating point numbers states that two
instances of the not-a-number (NaN
) value are not equal to each other.
An example of when Eq
is required is for keys in a HashMap<K, V>
so the
HashMap<K, V>
can tell whether two keys are the same.
PartialOrd
and Ord
for Ordering Comparisons
The PartialOrd
trait allows you to compare instances of a type for sorting
purposes. A type that implements PartialOrd
can be used with the <
, >
,
<=
, and >=
operators. You can only apply the PartialOrd
trait to types
that also implement PartialEq
.
Deriving PartialOrd
implements the partial_cmp
method, which returns an
Option<Ordering>
that will be None
when the values given don’t produce an
ordering. An example of a value that doesn’t produce an ordering, even though
most values of that type can be compared, is the not-a-number (NaN
) floating
point value. Calling partial_cmp
with any floating-point number and the NaN
floating-point value will return None
.
When derived on structs, PartialOrd
compares two instances by comparing the
value in each field in the order in which the fields appear in the struct
definition. When derived on enums, variants of the enum declared earlier in the
enum definition are considered less than the variants listed later.
The PartialOrd
trait is required, for example, for the gen_range
method
from the rand
crate that generates a random value in the range specified by a
range expression.
The Ord
trait allows you to know that for any two values of the annotated
type, a valid ordering will exist. The Ord
trait implements the cmp
method,
which returns an Ordering
rather than an Option<Ordering>
because a valid
ordering will always be possible. You can only apply the Ord
trait to types
that also implement PartialOrd
and Eq
(and Eq
requires PartialEq
). When
derived on structs and enums, cmp
behaves the same way as the derived
implementation for partial_cmp
does with PartialOrd
.
An example of when Ord
is required is when storing values in a BTreeSet<T>
,
a data structure that stores data based on the sort order of the values.
Clone
and Copy
for Duplicating Values
The Clone
trait allows you to explicitly create a deep copy of a value, and
the duplication process might involve running arbitrary code and copying heap
data. See Variables and Data Interacting with
Clone” in Chapter 4
for more information on Clone
.
Deriving Clone
implements the clone
method, which when implemented for the
whole type, calls clone
on each of the parts of the type. This means all the
fields or values in the type must also implement Clone
to derive Clone
.
An example of when Clone
is required is when calling the to_vec
method on a
slice. The slice doesn’t own the type instances it contains, but the vector
returned from to_vec
will need to own its instances, so to_vec
calls
clone
on each item. Thus the type stored in the slice must implement Clone
.
The Copy
trait allows you to duplicate a value by only copying bits stored on
the stack; no arbitrary code is necessary. See “Stack-Only Data:
Copy” in Chapter 4 for more information on
Copy
.
The Copy
trait doesn’t define any methods to prevent programmers from
overloading those methods and violating the assumption that no arbitrary code
is being run. That way, all programmers can assume that copying a value will be
very fast.
You can derive Copy
on any type whose parts all implement Copy
. A type that
implements Copy
must also implement Clone
, because a type that implements
Copy
has a trivial implementation of Clone
that performs the same task as
Copy
.
The Copy
trait is rarely required; types that implement Copy
have
optimizations available, meaning you don’t have to call clone
, which makes
the code more concise.
Everything possible with Copy
you can also accomplish with Clone
, but the
code might be slower or have to use clone
in places.
Hash
for Mapping a Value to a Value of Fixed Size
The Hash
trait allows you to take an instance of a type of arbitrary size and
map that instance to a value of fixed size using a hash function. Deriving
Hash
implements the hash
method. The derived implementation of the hash
method combines the result of calling hash
on each of the parts of the type,
meaning all fields or values must also implement Hash
to derive Hash
.
An example of when Hash
is required is in storing keys in a HashMap<K, V>
to store data efficiently.
Default
for Default Values
The Default
trait allows you to create a default value for a type. Deriving
Default
implements the default
function. The derived implementation of the
default
function calls the default
function on each part of the type,
meaning all fields or values in the type must also implement Default
to
derive Default
.
The Default::default
function is commonly used in combination with the struct
update syntax discussed in “Creating Instances from Other Instances with Struct
Update
Syntax” in Chapter 5. You can customize a few fields of a struct and then set
and use a default value for the rest of the fields by using
..Default::default()
.
The Default
trait is required when you use the method unwrap_or_default
on
Option<T>
instances, for example. If the Option<T>
is None
, the method
unwrap_or_default
will return the result of Default::default
for the type
T
stored in the Option<T>
.
Appendix D - Useful Development Tools
In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.
Automatic Formatting with rustfmt
The rustfmt
tool reformats your code according to the community code style.
Many collaborative projects use rustfmt
to prevent arguments about which
style to use when writing Rust: everyone formats their code using the tool.
Rust installations include rustfmt by default, so you should already have the
programs rustfmt
and cargo-fmt
on your system. These two commands are
analogous to rustc
and cargo
in that rustfmt
allows finer-grained control
and cargo-fmt
understands conventions of a project that uses Cargo. To format
any Cargo project, enter the following:
$ cargo fmt
Running this command reformats all the Rust code in the current crate. This should only change the code style, not the code semantics.
This command gives you rustfmt
and cargo-fmt
, similar to how Rust gives you
both rustc
and cargo
. To format any Cargo project, enter the following:
$ cargo fmt
Running this command reformats all the Rust code in the current crate. This
should only change the code style, not the code semantics. For more information
on rustfmt
, see its documentation.
Fix Your Code with rustfix
The rustfix
tool is included with Rust installations and can automatically fix
compiler warnings that have a clear way to correct the problem that’s likely
what you want. It’s likely you’ve seen compiler warnings before. For example,
consider this code:
Filename: src/main.rs
fn main() { let mut x = 42; println!("{x}"); }
Here, we’re defining the variable x
as mutable, but we never actually mutate
it. Rust warns us about that:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut x = 0;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
The warning suggests that we remove the mut
keyword. We can automatically
apply that suggestion using the rustfix
tool by running the command cargo fix
:
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
When we look at src/main.rs again, we’ll see that cargo fix
has changed the
code:
Filename: src/main.rs
fn main() { let x = 42; println!("{x}"); }
The x
variable is now immutable, and the warning no longer appears.
You can also use the cargo fix
command to transition your code between
different Rust editions. Editions are covered in Appendix E.
More Lints with Clippy
The Clippy tool is a collection of lints to analyze your code so you can catch common mistakes and improve your Rust code. Clippy is included with standard Rust installations.
To run Clippy’s lints on any Cargo project, enter the following:
$ cargo clippy
For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:
fn main() { let x = 3.1415; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
Running cargo clippy
on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
This error lets you know that Rust already has a more precise PI
constant
defined, and that your program would be more correct if you used the constant
instead. You would then change your code to use the PI
constant. The
following code doesn’t result in any errors or warnings from Clippy:
fn main() { let x = std::f64::consts::PI; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
For more information on Clippy, see its documentation.
IDE Integration Using rust-analyzer
To help IDE integration, the Rust community recommends using
rust-analyzer
. This tool is a set of
compiler-centric utilities that speaks the Language Server Protocol, which is a specification for IDEs and programming languages to
communicate with each other. Different clients can use rust-analyzer
, such as
the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer
project’s home page
for installation instructions, then install the language server support in your
particular IDE. Your IDE will gain abilities such as autocompletion, jump to
definition, and inline errors.
Appendix E - Editions
In Chapter 1, you saw that cargo new
adds a bit of metadata to your
Cargo.toml file about an edition. This appendix talks about what that means!
The Rust language and compiler have a six-week release cycle, meaning users get a constant stream of new features. Other programming languages release larger changes less often; Rust releases smaller updates more frequently. After a while, all of these tiny changes add up. But from release to release, it can be difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has changed a lot!”
Every three years or so, the Rust team produces a new Rust edition. Each edition brings together the features that have landed into a clear package with fully updated documentation and tooling. New editions ship as part of the usual six-week release process.
Editions serve different purposes for different people:
- For active Rust users, a new edition brings together incremental changes into an easy-to-understand package.
- For non-users, a new edition signals that some major advancements have landed, which might make Rust worth another look.
- For those developing Rust, a new edition provides a rallying point for the project as a whole.
At the time of this writing, four Rust editions are available: Rust 2015, Rust 2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition idioms.
The edition
key in Cargo.toml indicates which edition the compiler should
use for your code. If the key doesn’t exist, Rust uses 2015
as the edition
value for backward compatibility reasons.
Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.
All Rust compiler versions support any edition that existed prior to that compiler’s release, and they can link crates of any supported editions together. Edition changes only affect the way the compiler initially parses code. Therefore, if you’re using Rust 2015 and one of your dependencies uses Rust 2018, your project will compile and be able to use that dependency. The opposite situation, where your project uses Rust 2018 and a dependency uses Rust 2015, works as well.
To be clear: most features will be available on all editions. Developers using any Rust edition will continue to see improvements as new stable releases are made. However, in some cases, mainly when new keywords are added, some new features might only be available in later editions. You will need to switch editions if you want to take advantage of such features.
For more details, the Edition Guide is a complete book
about editions that enumerates the differences between editions and explains
how to automatically upgrade your code to a new edition via cargo fix
.
Appendix F: Translations of the Book
For resources in languages other than English. Most are still in progress; see the Translations label to help or let us know about a new translation!
- Português (BR)
- Português (PT)
- 简体中文: KaiserY/trpl-zh-cn, gnu4cn/rust-lang-Zh_CN
- 正體中文
- Українська
- Español, alternate, Español por RustLangES
- Русский
- 한국어
- 日本語
- Français
- Polski
- Cebuano
- Tagalog
- Esperanto
- ελληνική
- Svenska
- Farsi, Persian (FA)
- Deutsch
- हिंदी
- ไทย
- Danske
Appendix G - How Rust is Made and “Nightly Rust”
This appendix is about how Rust is made and how that affects you as a Rust developer.
Stability Without Stagnation
As a language, Rust cares a lot about the stability of your code. We want Rust to be a rock-solid foundation you can build on, and if things were constantly changing, that would be impossible. At the same time, if we can’t experiment with new features, we may not find out important flaws until after their release, when we can no longer change things.
Our solution to this problem is what we call “stability without stagnation”, and our guiding principle is this: you should never have to fear upgrading to a new version of stable Rust. Each upgrade should be painless, but should also bring you new features, fewer bugs, and faster compile times.
Choo, Choo! Release Channels and Riding the Trains
Rust development operates on a train schedule. That is, all development is
done on the master
branch of the Rust repository. Releases follow a software
release train model, which has been used by Cisco IOS and other software
projects. There are three release channels for Rust:
- Nightly
- Beta
- Stable
Most Rust developers primarily use the stable channel, but those who want to try out experimental new features may use nightly or beta.
Here’s an example of how the development and release process works: let’s
assume that the Rust team is working on the release of Rust 1.5. That release
happened in December of 2015, but it will provide us with realistic version
numbers. A new feature is added to Rust: a new commit lands on the master
branch. Each night, a new nightly version of Rust is produced. Every day is a
release day, and these releases are created by our release infrastructure
automatically. So as time passes, our releases look like this, once a night:
nightly: * - - * - - *
Every six weeks, it’s time to prepare a new release! The beta
branch of the
Rust repository branches off from the master
branch used by nightly. Now,
there are two releases:
nightly: * - - * - - *
|
beta: *
Most Rust users do not use beta releases actively, but test against beta in their CI system to help Rust discover possible regressions. In the meantime, there’s still a nightly release every night:
nightly: * - - * - - * - - * - - *
|
beta: *
Let’s say a regression is found. Good thing we had some time to test the beta
release before the regression snuck into a stable release! The fix is applied
to master
, so that nightly is fixed, and then the fix is backported to the
beta
branch, and a new release of beta is produced:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
Six weeks after the first beta was created, it’s time for a stable release! The
stable
branch is produced from the beta
branch:
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six
weeks have gone by, we also need a new beta of the next version of Rust, 1.6.
So after stable
branches off of beta
, the next version of beta
branches
off of nightly
again:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
This is called the “train model” because every six weeks, a release “leaves the station”, but still has to take a journey through the beta channel before it arrives as a stable release.
Rust releases every six weeks, like clockwork. If you know the date of one Rust release, you can know the date of the next one: it’s six weeks later. A nice aspect of having releases scheduled every six weeks is that the next train is coming soon. If a feature happens to miss a particular release, there’s no need to worry: another one is happening in a short time! This helps reduce pressure to sneak possibly unpolished features in close to the release deadline.
Thanks to this process, you can always check out the next build of Rust and
verify for yourself that it’s easy to upgrade to: if a beta release doesn’t
work as expected, you can report it to the team and get it fixed before the
next stable release happens! Breakage in a beta release is relatively rare, but
rustc
is still a piece of software, and bugs do exist.
Maintenance time
The Rust project supports the most recent stable version. When a new stable version is released, the old version reaches its end of life (EOL). This means each version is supported for six weeks.
Unstable Features
There’s one more catch with this release model: unstable features. Rust uses a
technique called “feature flags” to determine what features are enabled in a
given release. If a new feature is under active development, it lands on
master
, and therefore, in nightly, but behind a feature flag. If you, as a
user, wish to try out the work-in-progress feature, you can, but you must be
using a nightly release of Rust and annotate your source code with the
appropriate flag to opt in.
If you’re using a beta or stable release of Rust, you can’t use any feature flags. This is the key that allows us to get practical use with new features before we declare them stable forever. Those who wish to opt into the bleeding edge can do so, and those who want a rock-solid experience can stick with stable and know that their code won’t break. Stability without stagnation.
This book only contains information about stable features, as in-progress features are still changing, and surely they’ll be different between when this book was written and when they get enabled in stable builds. You can find documentation for nightly-only features online.
Rustup and the Role of Rust Nightly
Rustup makes it easy to change between different release channels of Rust, on a global or per-project basis. By default, you’ll have stable Rust installed. To install nightly, for example:
$ rustup toolchain install nightly
You can see all of the toolchains (releases of Rust and associated
components) you have installed with rustup
as well. Here’s an example on one
of your authors’ Windows computer:
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
As you can see, the stable toolchain is the default. Most Rust users use stable
most of the time. You might want to use stable most of the time, but use
nightly on a specific project, because you care about a cutting-edge feature.
To do so, you can use rustup override
in that project’s directory to set the
nightly toolchain as the one rustup
should use when you’re in that directory:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
Now, every time you call rustc
or cargo
inside of
~/projects/needs-nightly, rustup
will make sure that you are using nightly
Rust, rather than your default of stable Rust. This comes in handy when you
have a lot of Rust projects!
The RFC Process and Teams
So how do you learn about these new features? Rust’s development model follows a Request For Comments (RFC) process. If you’d like an improvement in Rust, you can write up a proposal, called an RFC.
Anyone can write RFCs to improve Rust, and the proposals are reviewed and discussed by the Rust team, which is comprised of many topic subteams. There’s a full list of the teams on Rust’s website, which includes teams for each area of the project: language design, compiler implementation, infrastructure, documentation, and more. The appropriate team reads the proposal and the comments, writes some comments of their own, and eventually, there’s consensus to accept or reject the feature.
If the feature is accepted, an issue is opened on the Rust repository, and
someone can implement it. The person who implements it very well may not be the
person who proposed the feature in the first place! When the implementation is
ready, it lands on the master
branch behind a feature gate, as we discussed
in the “Unstable Features” section.
After some time, once Rust developers who use nightly releases have been able to try out the new feature, team members will discuss the feature, how it’s worked out on nightly, and decide if it should make it into stable Rust or not. If the decision is to move forward, the feature gate is removed, and the feature is now considered stable! It rides the trains into a new stable release of Rust.