
Rust
- 64 installs
- 2 repo stars
- Updated August 3, 2026
- fandhe-ai/agent-reference-skills
Helps with ai & agent building tasks.
About
rust is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- rust
- AI & Agent Building
- AI-coding skill
Rust by the numbers
- 64 all-time installs (skills.sh)
- Ranked #6,160 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fandhe-ai/agent-reference-skills --skill rustAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | fandhe-ai/agent-reference-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Chapter 1: Getting Started
Introduction to Rust: installation, writing your first program, and using Cargo.
Key Concepts
Installation via rustup
# Linux/macOS
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
# Verify
rustc --version
# Update / uninstall
rustup update
rustup self uninstall
# Open local offline docs
rustup docWindows: download the installer from https://www.rust-lang.org/tools/install (requires Visual Studio C++ tools).
Hello, World!
fn main() {
println!("Hello, world!");
}rustc main.rs # compile
./main # run (Linux/macOS)fn main()is the entry point of every executable.println!is a macro (note the!).- Lines end with
;. - Rust is ahead-of-time compiled — distribute the binary without requiring Rust on the target machine.
- Use
rustfmtfor automatic code formatting.
Hello, Cargo
cargo new hello_cargo # create a project
cargo build # debug build → target/debug/
cargo run # build + run
cargo check # check compilation without producing binary (faster)
cargo build --release # optimized build → target/release/Cargo.toml structure:
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]- Source files live in
src/. Cargo.locklocks exact dependency versions for reproducible builds.cargo checkis fastest for iteration; use it frequently.
Notes
- Rust file naming convention:
snake_case.rs. - The
[dependencies]section lists external crates. - Running
cargo newinside an existing git repo skips.gitinitialization.
Related
- README
- Chapter 2: Programming a Guessing Game
Chapter 2: Programming a Guessing Game
A hands-on introduction to Rust fundamentals through building a complete number-guessing game. Covers variables, I/O, external crates, pattern matching, and control flow.
Key Concepts
Full example
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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;
}
}
}
}Add the rand dependency in Cargo.toml:
[dependencies]
rand = "0.8.5"Concepts demonstrated
| Concept | Example |
|---|---|
| Immutable variable | let secret_number = ... |
| Mutable variable | let mut guess = String::new() |
| Reading stdin | io::stdin().read_line(&mut guess) |
| Shadowing (type change) | let guess: u32 = guess.trim().parse()... |
| Pattern matching | match guess.cmp(&secret_number) { ... } |
| Result handling | .expect(...) / match Ok/Err |
| Infinite loop with break | loop { ... break; } |
Notes
- Shadowing lets you re-bind a variable with a different type using
letagain. This differs frommut, which cannot change the type. read_linereturnsResult<usize, io::Error>; calling.expect()panics with a message onErr.parse()returns aResult; usingmatchhandles invalid input gracefully withcontinue.rand::thread_rng().gen_range(1..=100)requiresuse rand::Rngfor the trait's methods.Cargo.lockpins dependency versions;cargo updatefetches compatible newer versions.- SemVer
"0.8.5"means>=0.8.5, <0.9.0.
Related
- Chapter 1: Getting Started
- Chapter 3: Common Programming Concepts
Chapter 3: Common Programming Concepts
Fundamental Rust building blocks: variables, data types, functions, comments, and control flow.
Variables and Mutability
let x = 5; // immutable by default
let mut y = 5; // mutable
y = 6; // OK
const MAX: u32 = 100_000; // constant: always immutable, type required
// Shadowing: re-bind with let (can change type)
let spaces = " ";
let spaces = spaces.len(); // now usize, not &str- Constants use
ALL_CAPS_SNAKE_CASE, must be type-annotated, and can be set only to compile-time expressions. - Shadowing allows type changes;
mutdoes not.
Data Types
Scalar types
| Type | Examples |
|---|---|
| Integers | i8/u8 … i128/u128, isize/usize; default i32 |
| Floats | f32, f64; default f64 |
| Boolean | bool: true / false |
| Character | char: single quotes, 4-byte Unicode scalar |
let decimal = 98_222; // underscores for readability
let hex = 0xff;
let binary = 0b1111_0000;
let byte = b'A'; // u8 onlyCompound types
// Tuple (fixed length, mixed types)
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // destructure
let five_hundred = tup.0;
// Array (fixed length, same type)
let a: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
let first = a[0]; // index access; out-of-bounds panics at runtimeFunctions
fn add(x: i32, y: i32) -> i32 {
x + y // expression (no semicolon) = return value
}- Parameter types are always required.
- The last expression (without
;) is the return value. - Adding
;turns an expression into a statement that returns(). - Function definition order does not matter.
Control Flow
// if as expression
let number = if condition { 5 } else { 6 }; // arms must be same type
// loop with return value
let result = loop {
counter += 1;
if counter == 10 { break counter * 2; }
};
// Loop labels for nested loops
'outer: loop {
loop { break 'outer; }
}
// while
while number != 0 { number -= 1; }
// for over collection or range
for element in array { println!("{element}"); }
for n in (1..4).rev() { println!("{n}"); }Notes
- Integer overflow: debug builds panic; release builds wrap (two's complement).
- Array out-of-bounds access panics at runtime (memory-safe).
ifconditions must bebool— Rust does not auto-convert numbers to booleans.foris preferred overwhilewith indices: safer and often faster.
Related
- Chapter 2: Programming a Guessing Game
- Chapter 4: Understanding Ownership
Chapter 4: Understanding Ownership
Rust's core memory-management feature: ownership, borrowing, and slices — enabling memory safety without a garbage collector.
Ownership Rules
1. Each value has exactly one owner. 2. There can be only one owner at a time. 3. When the owner goes out of scope, the value is dropped (memory freed).
Move vs. Copy
// Move: heap-allocated types transfer ownership
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{s1}"); // ❌ compile error: s1 invalid
// Clone: explicit deep copy
let s3 = s2.clone();
println!("{s2}, {s3}"); // both valid
// Copy: stack types are trivially copied
let x = 5;
let y = x; // x is copied, both still valid
println!("{x}, {y}");Copy types: integers, floats, bool, char, tuples of Copy types.
References and Borrowing
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope but the String is NOT dropped (no ownership)
let s1 = String::from("hello");
let len = calculate_length(&s1); // pass reference with &Mutable references
let mut s = String::from("hello");
change(&mut s);
fn change(s: &mut String) {
s.push_str(", world");
}Borrowing rules
- At any given time: either one mutable reference or any number of immutable references — never both simultaneously.
- References must always be valid (no dangling references).
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // last use of r1, r2
let r3 = &mut s; // OK: r1/r2 scopes ended
println!("{r3}");The Slice Type
Slices are references to a contiguous sequence — they don't own data.
let s = String::from("hello world");
let hello = &s[0..5]; // &str
let world = &s[6..11];
// String slices in function signatures — preferred over &String
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[..]
}
// Array slices
let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3]; // [2, 3]Notes
- Passing a
Stringto a function moves it unless you pass a reference (&Stringor&str). - Returning a value from a function moves ownership to the caller.
- The borrow checker enforces borrowing rules at compile time — no runtime cost.
- Prefer
&strover&Stringin function parameters for maximum flexibility (works with bothStringand string literals). - String literal type is
&str(a slice into the program binary).
Related
- Chapter 3: Common Programming Concepts
- Chapter 5: Using Structs
- Chapter 10: Generic Types, Traits, and Lifetimes
Chapter 5: Using Structs
Custom data types that group related named fields, with methods to attach behavior.
Defining and Instantiating
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
// Instantiate
let user1 = User {
active: true,
username: String::from("alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
};
// Mutable instance — the whole instance must be mut
let mut user1 = User { /* ... */ };
user1.email = String::from("new@example.com");Field init shorthand
fn build_user(email: String, username: String) -> User {
User {
active: true,
username, // shorthand when param name == field name
email,
sign_in_count: 1,
}
}Struct update syntax
let user2 = User {
email: String::from("other@example.com"),
..user1 // remaining fields from user1 (moves String fields)
};Tuple structs and unit-like structs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let x = black.0; // index access
struct AlwaysEqual; // no fields; useful for implementing traits
let subject = AlwaysEqual;Methods
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// Method: first param is &self (borrows immutably)
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// Associated function (no self) — often used as constructor
fn square(size: u32) -> Self {
Self { width: size, height: size }
}
}
let rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
let sq = Rectangle::square(10); // call with :: syntaxself parameter forms
| Form | Meaning |
|---|---|
&self | Immutable borrow (read-only) |
&mut self | Mutable borrow (can modify) |
self | Takes ownership (rare; transforms the instance) |
Notes
- Rust applies automatic referencing/dereferencing when calling methods, so
rect.area()and(&rect).area()are equivalent. - Use owned types (
String) rather than references (&str) in struct fields to avoid lifetime annotations (until Chapter 10). - A struct can have multiple
implblocks (useful with generics and traits). #[derive(Debug)]auto-generates{:?}/{:#?}printing.
Related
- Chapter 4: Understanding Ownership
- Chapter 6: Enums and Pattern Matching
- Chapter 10: Generic Types, Traits, and Lifetimes
Chapter 6: Enums and Pattern Matching
Enumerations express a value that can be one of several variants. Combined with match, they enable exhaustive, type-safe control flow.
Defining Enums
// Basic enum
enum Direction { North, South, East, West }
let go = Direction::North;
// Variants with associated data
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // single value
ChangeColor(i32, i32, i32), // multiple values
}
impl Message {
fn call(&self) { /* ... */ }
}
let m = Message::Write(String::from("hello"));
m.call();Option\<T\>
Rust's type-safe replacement for null:
enum Option<T> {
Some(T),
None,
}
let some_number: Option<i32> = Some(5);
let absent: Option<i32> = None;Option<T> and T are different types — you cannot use an Option<i32> where an i32 is expected without explicitly handling both cases.
match Expression
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
// Binding values from variants
match coin {
Coin::Quarter(state) => println!("State: {state:?}"),
other => println!("Other coin"),
}
// Matching Option<T>
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}Catch-all patterns
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other), // binds value
// or: _ => reroll(), // ignores value
// or: _ => (), // do nothing
}matchis exhaustive: all cases must be covered or the code won't compile.- Catch-all arms must be last.
if let and let...else
// Concise single-pattern matching
if let Some(max) = config_max {
println!("Max is {max}");
}
// With else
if let Coin::Quarter(state) = coin {
println!("Quarter from {state:?}");
} else {
count += 1;
}
// let...else: bind or return early (stays on happy path)
let Coin::Quarter(state) = coin else {
return None;
};Notes
if letandlet...elsesacrifice exhaustive checking for conciseness.- Use
matchwhen you need to verify all variants are handled. - Enum variants with data act like constructor functions.
Related
- Chapter 5: Using Structs
- Chapter 7: Packages, Crates, and Modules
- Chapter 19: Patterns and Matching
Chapter 7: Managing Growing Projects with Packages, Crates, and Modules
Rust's module system for organizing code into packages, crates, and modules with controlled visibility.
Packages and Crates
| Concept | Description |
|---|---|
| Crate | Smallest compilation unit; either a binary or library |
| Package | Bundle of crates managed by Cargo.toml |
| Crate root | Source file where the compiler starts |
Cargo conventions:
src/main.rs→ binary crate root (same name as package)src/lib.rs→ library crate root (same name as package)src/bin/*.rs→ additional binary crates
A package may have at most one library crate but any number of binary crates.
Modules
// src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
mod serving { // private by default
fn take_order() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}- Items (functions, structs, modules) are private by default.
pubmakes them accessible from parent modules and external code.pub structmakes the struct public but fields remain private by default; addpubper field.pub enummakes all variants public.
Paths and use
// Bring into scope
use crate::front_of_house::hosting;
hosting::add_to_waitlist(); // idiomatic for functions
use std::collections::HashMap;
let mut map = HashMap::new(); // idiomatic for types
// Rename with as
use std::io::Result as IoResult;
// Re-export with pub use
pub use crate::front_of_house::hosting;
// Nested paths
use std::{cmp::Ordering, io};
use std::io::{self, Write};
// Glob (use sparingly)
use std::collections::*;Splitting Modules Across Files
src/
├── lib.rs → mod front_of_house;
├── front_of_house.rs OR
└── front_of_house/
├── mod.rs
└── hosting.rs// src/lib.rs
mod front_of_house; // loads src/front_of_house.rs or src/front_of_house/mod.rs
pub use crate::front_of_house::hosting;Notes
super::navigates to the parent module (like..in filesystem paths).self::refers to the current module.- The module tree mirrors a filesystem; modules can be inline or in separate files.
- Glob imports (
use x::*) reduce clarity and may cause conflicts — prefer explicit imports. - For very large projects, use Cargo workspaces (Chapter 14).
Related
- Chapter 6: Enums and Pattern Matching
- Chapter 8: Common Collections
- Chapter 14: More about Cargo and Crates.io
Chapter 8: Common Collections
Heap-allocated collections that can grow and shrink at runtime: vectors, strings, and hash maps.
Vec\<T\> — Vectors
// Creation
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3]; // type inferred
// Updating
let mut v = Vec::new();
v.push(5);
// Reading elements
let third: &i32 = &v[2]; // panics if out of bounds
let third: Option<&i32> = v.get(2); // safe: returns None
// Iterating
for i in &v { println!("{i}"); }
for i in &mut v { *i += 50; } // dereference to modify
// Storing multiple types via enum
enum Cell { Int(i32), Text(String) }
let row = vec![Cell::Int(3), Cell::Text(String::from("blue"))];- Freed when the vector goes out of scope, dropping all elements.
- Cannot hold an immutable reference while also mutating the vector (borrow rules apply).
String
// Creation
let mut s = String::new();
let s = String::from("hello");
let s = "hello".to_string();
// Updating
s.push_str(" world"); // appends &str; doesn't take ownership
s.push('!'); // single char
// Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved; s2 borrowed
let s = format!("{s2}-{s3}"); // doesn't take ownership of any arg
// Iterating — strings cannot be indexed with [0]
for c in "Зд".chars() { println!("{c}"); } // Unicode scalar values
for b in "Зд".bytes() { println!("{b}"); } // raw bytesWhy indexing is not allowed:
- UTF-8 characters vary in byte length (1–4 bytes).
"hello"[0]would return the first byte (104), not a character.- Rust guarantees O(1) indexing; scanning for character boundaries would violate that.
Use &s[0..4] (byte ranges) with caution — panics if the range falls inside a multi-byte character.
HashMap\<K, V\>
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
// Access
let score = scores.get("Blue").copied().unwrap_or(0);
// Iterate
for (key, value) in &scores { println!("{key}: {value}"); }
// Update patterns
scores.insert(String::from("Blue"), 25); // overwrite
// Insert only if key absent
scores.entry(String::from("Yellow")).or_insert(50);
// Update based on old value
let count = map.entry(word).or_insert(0);
*count += 1;- HashMap is not in the prelude; must
use std::collections::HashMap. - Owned values (like
String) are moved into the map;Copytypes are copied. - All keys must be the same type; all values must be the same type.
- Default hash function is SipHash (DoS-resistant but not the fastest).
Notes
- Prefer
v.get(i)over&v[i]when out-of-bounds is possible. Stringis aVec<u8>wrapper — allVecperformance characteristics apply.- For grapheme clusters (user-perceived characters), use a crate from crates.io.
Related
- Chapter 7: Packages, Crates, and Modules
- Chapter 9: Error Handling
- Chapter 13: Iterators and Closures
Chapter 9: Error Handling
Rust distinguishes recoverable errors (Result<T, E>) from unrecoverable ones (panic!), requiring explicit acknowledgment at compile time.
Unrecoverable Errors — panic!
panic!("crash and burn");
// Out-of-bounds access also panics
let v = vec![1, 2, 3];
v[99]; // thread 'main' panicked: index out of bounds
// Get a backtrace
RUST_BACKTRACE=1 cargo runBy default, panic unwinds the stack. For smaller binaries, configure to abort:
[profile.release]
panic = 'abort'Recoverable Errors — Result\<T, E\>
enum Result<T, E> {
Ok(T),
Err(E),
}Handling Result
use std::fs::File;
use std::io::ErrorKind;
let f = match File::open("hello.txt") {
Ok(file) => file,
Err(e) => match e.kind() {
ErrorKind::NotFound => File::create("hello.txt").unwrap(),
_ => panic!("Problem opening file: {e:?}"),
},
};Shortcuts
// unwrap: returns Ok value or panics with default message
let f = File::open("hello.txt").unwrap();
// expect: panics with a custom message (preferred in production)
let f = File::open("hello.txt").expect("Failed to open hello.txt");Propagating errors with ?
use std::fs::File;
use std::io::{self, Read};
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)
}?unwrapsOkand continues, or returnsErrearly to the caller.- Converts error types using the
Fromtrait automatically. - Can only be used in functions returning
Result,Option, or types implementingFromResidual.
// Using ? in main
fn main() -> Result<(), Box<dyn std::error::Error>> {
let f = File::open("hello.txt")?;
Ok(())
}When to panic vs. return Result
| Situation | Recommendation |
|---|---|
| Examples, prototypes, tests | unwrap / expect as placeholders |
| You've verified the logic succeeds | expect with explanation |
| Caller should decide how to handle | Return Result |
| Invalid/unexpected state (bug, not user error) | panic! |
| Expected occasional failures (wrong input, network) | Return Result |
Custom validation type pattern:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Self {
if value < 1 || value > 100 {
panic!("Guess value must be less than or equal to 100, got {value}.");
}
Self { value }
}
pub fn value(&self) -> i32 { self.value }
}Notes
- There are no exceptions in Rust — only
Resultandpanic!. - Use
expectoverunwrapin production: the message aids debugging. ?enables clean, linear error-propagation code without deeply nestedmatch.Box<dyn Error>inmainis a convenient trait object for any error type.
Related
- Chapter 6: Enums and Pattern Matching
- Chapter 10: Generic Types, Traits, and Lifetimes
Chapter 10: Generic Types, Traits, and Lifetimes
Generics eliminate code duplication; traits define shared behavior; lifetimes ensure references remain valid.
Generic Types
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
// Generic struct
struct Point<T> { x: T, y: T }
struct Pair<T, U> { x: T, y: U }
// Generic methods
impl<T> Point<T> {
fn x(&self) -> &T { &self.x }
}
// Implement only for specific type
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}Monomorphization: Rust generates specialized code for each concrete type used — zero runtime cost.
Traits
pub trait Summary {
fn summarize(&self) -> String; // required method
fn preview(&self) -> String { // default implementation
format!("{}...", &self.summarize()[..20])
}
}
pub struct Article { pub headline: String, pub author: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}Trait bounds
// impl Trait syntax (sugar)
pub fn notify(item: &impl Summary) { println!("{}", item.summarize()); }
// Generic trait bound (explicit)
pub fn notify<T: Summary>(item: &T) { println!("{}", item.summarize()); }
// Multiple bounds
pub fn notify<T: Summary + Display>(item: &T) { }
// where clause (cleaner for many bounds)
fn some_fn<T, U>(t: &T, u: &U)
where
T: Display + Clone,
U: Clone + Debug,
{ }
// Returning impl Trait (single concrete type only)
fn returns_summarizable() -> impl Summary { Article { /* ... */ } }The orphan rule: you can implement a trait on a type only if either the trait or the type is defined in your crate.
Lifetimes
Lifetimes describe how long references must be valid. The borrow checker uses them to prevent dangling references.
// Lifetime annotation: 'a means "at least as long as 'a"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Lifetime in structs (struct cannot outlive the reference it holds)
struct ImportantExcerpt<'a> {
part: &'a str,
}Lifetime elision rules (compiler infers automatically)
1. Each reference parameter gets its own lifetime. 2. If there is exactly one input lifetime, it is assigned to all output lifetimes. 3. If one of the inputs is &self or &mut self, its lifetime is assigned to all outputs.
Static lifetime
let s: &'static str = "I live for the entire program.";String literals are always 'static. Don't use 'static as a quick fix for lifetime errors — fix the root cause instead.
Combined example
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement: {ann}");
if x.len() > y.len() { x } else { y }
}Notes
- Generic type parameters and lifetime parameters share the same
<>bracket. - Lifetime annotations do not change how long references live; they describe relationships.
- Most lifetimes are inferred; annotations are needed only when relationships are ambiguous.
- Traits must be in scope to call their methods (e.g.,
use rand::Rng).
Related
- Chapter 4: Understanding Ownership
- Chapter 9: Error Handling
- Chapter 18: OOP Features
- Chapter 20: Advanced Features
Chapter 11: Writing Automated Tests
Rust's built-in testing framework: test functions, assertion macros, and test organization.
Writing Test Functions
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[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));
}
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting was `{result}`" // custom failure message
);
}
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200); // must panic with message containing expected string
}
#[test]
fn it_works_result() -> Result<(), String> {
if add(2, 2) == 4 { Ok(()) } else { Err(String::from("wrong")) }
}
}Assertion macros
| Macro | Use |
|---|---|
assert!(expr) | Passes if expr is true |
assert_eq!(left, right) | Passes if equal; prints both on failure |
assert_ne!(left, right) | Passes if not equal |
#[should_panic] | Passes if code panics |
assert_eq!/assert_ne!requirePartialEqandDebugon the values.- Custom messages are formatted strings after the assertion arguments.
Running Tests
cargo test # run all tests
cargo test one_hundred # run tests matching "one_hundred"
cargo test add # run tests whose name contains "add"
cargo test -- --test-threads=1 # sequential (no parallelism)
cargo test -- --show-output # show stdout from passing tests
cargo test -- --ignored # run only #[ignore] tests
cargo test -- --include-ignored # run all including ignored#[test]
#[ignore]
fn expensive_test() { /* skipped by default */ }Test Organization
Unit tests — same file as code
// src/lib.rs
pub fn add_two(x: i32) -> i32 { x + 2 }
#[cfg(test)] // compiled only with `cargo test`
mod tests {
use super::*; // can test private functions
#[test]
fn test_add_two() { assert_eq!(4, add_two(2)); }
}Integration tests — separate tests/ directory
// tests/integration_test.rs
use adder::add_two; // public API only
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}Shared test helpers — use a subdirectory to avoid them appearing as test suites:
tests/
├── common/
│ └── mod.rs // shared setup; not treated as a test file
└── integration_test.rsNotes
#[cfg(test)]ensures test code is excluded from production builds.Result-returning tests can use?but cannot use#[should_panic].- Binary-only crates (
src/main.rswithoutsrc/lib.rs) cannot have integration tests — move logic tolib.rs. - Tests in the same file can access private functions via
use super::*.
Related
- Chapter 9: Error Handling
- Chapter 12: I/O Project
Chapter 12: An I/O Project — Building a Command Line Program
A practical project (minigrep) consolidating Chapters 1–11: reading CLI args, files, environment variables, stderr, and writing tests.
Project Goal
Build minigrep — a simplified grep that searches a file for lines containing a query string.
cargo run -- searchterm file.txtKey Implementation Concepts
Reading command-line arguments
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
// args[0] = program name, args[1] = query, args[2] = file path
let query = &args[1];
let file_path = &args[2];
}- Use
env::args_os()if arguments may contain invalid Unicode.
Reading a file
use std::fs;
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");Separation of concerns — Config struct
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
Ok(Config {
query: args[1].clone(),
file_path: args[2].clone(),
ignore_case: env::var("IGNORE_CASE").is_ok(),
})
}
}Library vs. binary split
- Business logic goes in
src/lib.rs(testable). src/main.rsonly parses args, callslib::run(), and handles top-level errors.
// src/main.rs
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) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}Writing to stderr
eprintln!("Error: {err}"); // goes to stderr, not stdoutTest-driven search function
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents.lines()
.filter(|line| line.contains(query))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "Duct tape.\nSafe, fast, productive.";
assert_eq!(vec!["Safe, fast, productive."], search(query, contents));
}
}Case-insensitive variant
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
contents.lines()
.filter(|line| line.to_lowercase().contains(&query))
.collect()
}Notes
- This chapter practices refactoring toward clean code before introducing advanced features.
process::exit(1)immediately terminates with a non-zero exit code (signals error to shell).eprintln!directs output to stderr; users can then pipe stdout without noise.- The
IGNORE_CASE=1 cargo runenvironment variable pattern is idiomatic for Unix CLI tools. - Lifetimes on
searchare needed because the returned slices referencecontents.
Related
- Chapter 9: Error Handling
- Chapter 11: Testing
- Chapter 13: Iterators and Closures
Chapter 13: Functional Language Features — Iterators and Closures
Closures are anonymous functions that capture their environment; iterators provide lazy, composable sequence processing.
Closures
// Syntax variants (all equivalent for a simple add-one)
let add_one = |x: u32| -> u32 { x + 1 }; // fully annotated
let add_one = |x| x + 1; // types inferred
// Closures capture their environment
let offset = 5;
let add_offset = |x| x + offset; // captures `offset` by immutable borrowCapture modes
let list = vec![1, 2, 3];
// Immutable borrow (default when only reading)
let borrows = || println!("{list:?}");
borrows();
println!("{list:?}"); // list still accessible
// Mutable borrow
let mut list = vec![1, 2, 3];
let mut appends = || list.push(7);
appends();
// Ownership transfer with move (required for threads)
use std::thread;
let list = vec![1, 2, 3];
thread::spawn(move || println!("{list:?}")).join().unwrap();Fn traits
| Trait | How closure uses captured values |
|---|---|
FnOnce | May move captured values out; callable once |
FnMut | Mutates captured values; callable multiple times |
Fn | Only reads captured values; callable multiple times |
FnOnce is the most permissive bound (accepts all closures); use it when a closure is called once. sort_by_key requires FnMut.
Iterators
All iterators implement the Iterator trait:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}Iterators are lazy — no computation until consumed.
Creating iterators
let v = vec![1, 2, 3];
v.iter() // &T (immutable references)
v.iter_mut() // &mut T (mutable references)
v.into_iter() // T (takes ownership)Iterator adapters (lazy, return new iterators)
v.iter().map(|x| x + 1) // transform each element
v.iter().filter(|&&x| x > 1) // keep matching elements
v.iter().zip(other.iter()) // pair two iterators
v.iter().enumerate() // (index, &value) pairs
v.iter().take(3) // first N elements
v.iter().skip(2) // skip N elements
v.iter().flat_map(|x| vec![x, x]) // flatten one levelConsuming adaptors (call next, produce a value)
let sum: i32 = v.iter().sum();
let product: i32 = v.iter().product();
let collected: Vec<i32> = v.iter().map(|x| x + 1).collect();
let count = v.iter().count();
let any_positive = v.iter().any(|&x| x > 0);
let all_positive = v.iter().all(|&x| x > 0);Chaining
let result: Vec<String> =
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.map(|s| s.style.clone())
.collect();Custom iterator
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 { self.count += 1; Some(self.count) }
else { None }
}
}Notes
- Iterator adaptors produce no output until consumed with a consuming adaptor — forgetting to consume is a common mistake.
- Iterators are generally as fast as or faster than manual loops (zero-cost abstraction via inlining).
collect()requires a type annotation (e.g.,Vec<_>) because it can produce many collection types.- Returning a closure: use
impl Fn(i32) -> i32for a single type, orBox<dyn Fn(i32) -> i32>when multiple different closures may be returned.
Related
- Chapter 12: I/O Project
- Chapter 15: Smart Pointers
- Chapter 17: Async and Await
Chapter 14: More about Cargo and Crates.io
Release profiles, documentation, publishing, workspaces, and installing binaries.
Release Profiles
# Cargo.toml
[profile.dev] # cargo build
opt-level = 0 # fast compile, no optimization
[profile.release] # cargo build --release
opt-level = 3 # slow compile, maximum optimizationOverride any default setting by adding a [profile.*] section.
Documentation Comments
/// Adds one to the given number.
///
/// # Examples
///
/// ```
/// let result = my_crate::add_one(5);
/// assert_eq!(6, result);
/// ```
///
/// # Panics
/// Never panics.
///
/// # Errors
/// Returns `Err` if … (for Result-returning functions)
pub fn add_one(x: i32) -> i32 { x + 1 }
//! # My Crate
//! Crate-level documentation (place at top of src/lib.rs)cargo doc # generate HTML docs in target/doc/
cargo doc --open # build and open in browser
cargo test # also runs code examples in doc comments as testsRe-exporting for Convenient Public API
// src/lib.rs
pub use self::kinds::PrimaryColor;
pub use self::utils::mix;Users can then write use art::PrimaryColor instead of use art::kinds::PrimaryColor.
Publishing to Crates.io
[package]
name = "my_crate"
version = "0.1.0"
edition = "2024"
description = "A brief description"
license = "MIT OR Apache-2.0"cargo login # store API token from crates.io/me
cargo publish # publish current version (permanent)
cargo yank --vers 1.0.1 # prevent new projects from using this version
cargo yank --vers 1.0.1 --undo # reverse a yank- Publishes are permanent — versions cannot be deleted.
- Yank prevents new dependencies but does not break existing ones.
- Follow Semantic Versioning for version bumps.
Cargo Workspaces
# workspace root Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]add/
├── Cargo.lock # shared across all crates
├── Cargo.toml # workspace config
├── target/ # shared output directory
├── adder/ # binary crate
│ ├── Cargo.toml
│ └── src/main.rs
└── add_one/ # library crate
├── Cargo.toml
└── src/lib.rsInter-crate dependency in adder/Cargo.toml:
[dependencies]
add_one = { path = "../add_one" }cargo build # build all crates
cargo build -p adder # build specific crate
cargo test -p add_one # test specific crate- Shared
Cargo.lockensures all crates use the same dependency versions. - Each external dependency must be declared per-crate even if the version is shared.
Installing Binaries
cargo install ripgrep # install binary crate from crates.ioBinaries are installed in ~/.cargo/bin/. Only crates with binary targets can be installed.
Notes
cargo check(from Chapter 1) is the fastest feedback loop; use it during development.opt-levelranges from0(no optimization) to3(full);"s"/"z"optimize for size.- Common license for open-source Rust crates:
"MIT OR Apache-2.0".
Related
- Chapter 7: Packages, Crates, and Modules
- Chapter 11: Testing
Chapter 15: Smart Pointers
Data structures that act like pointers but provide additional metadata and capabilities. Implemented via the Deref and Drop traits.
Box\<T\> — Heap Allocation
// Store data on the heap
let b = Box::new(5);
println!("b = {b}"); // Derefs transparently
// Primary use case: recursive types (Box gives known size)
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));Use Box<T> when:
- Type size is unknown at compile time (recursive types).
- You want to transfer ownership of large data without copying.
- You need a trait object (
Box<dyn Trait>).
Rc\<T\> — Reference Counted Multiple Ownership
use std::rc::Rc;
let a = Rc::new(Cons(5, Rc::new(Nil)));
println!("count = {}", Rc::strong_count(&a)); // 1
let b = Cons(3, Rc::clone(&a)); // increments ref count (cheap)
println!("count = {}", Rc::strong_count(&a)); // 2
{
let c = Cons(4, Rc::clone(&a));
println!("count = {}", Rc::strong_count(&a)); // 3
}
println!("count = {}", Rc::strong_count(&a)); // 2 (c dropped)- Single-threaded only — not thread-safe.
Rc::cloneincrements the reference count; it does NOT deep-copy data.- Data is freed when
strong_countreaches 0. - Provides immutable shared access only.
RefCell\<T\> — Interior Mutability
Enforces borrowing rules at runtime instead of compile time.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4); // mutable borrow
println!("{:?}", data.borrow()); // immutable borrow
// Panics at runtime if rules are violated:
// let _r1 = data.borrow_mut();
// let _r2 = data.borrow_mut(); // ❌ panic: already mutably borrowed- Single-threaded only — use
Mutex<T>for multi-threading. borrow()returnsRef<T>;borrow_mut()returnsRefMut<T>.- Useful for mock objects in tests and when you need mutability through a shared reference.
Rc\<RefCell\<T\>\> — Multiple Owners with Mutation
use std::rc::Rc;
use std::cell::RefCell;
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*value.borrow_mut() += 10;
println!("a = {:?}, b = {:?}", a.borrow(), b.borrow()); // both see 15Weak\<T\> — Preventing Reference Cycles
Rc::clone creates strong references; Rc::downgrade creates weak references that don't affect the ref count and don't prevent deallocation.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // weak: child doesn't own parent
children: RefCell<Vec<Rc<Node>>>, // strong: parent owns children
}
// Access a weak reference
if let Some(parent) = leaf.parent.borrow().upgrade() {
println!("parent value: {}", parent.value);
}Rc::downgrade(&rc)→Weak<T>weak.upgrade()→Option<Rc<T>>(returnsNoneif data was dropped)
Deref and Drop Traits
// Deref: lets Box<T> behave like &T
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.0 }
}
// Drop: cleanup when value goes out of scope
impl Drop for CustomSmartPointer {
fn drop(&mut self) { println!("Dropping!"); }
}
// Early drop: std::mem::drop(value); — cannot call value.drop() directlyNotes
| Type | Ownership | Borrow checking | Thread-safe |
|---|---|---|---|
Box<T> | Single | Compile-time | Yes (if T: Send) |
Rc<T> | Multiple | Compile-time | No |
RefCell<T> | Single | Runtime | No |
Arc<T> | Multiple | Compile-time | Yes |
Mutex<T> | Single/shared | Runtime (lock) | Yes |
Related
- Chapter 4: Understanding Ownership
- Chapter 16: Fearless Concurrency
Chapter 16: Fearless Concurrency
Rust's ownership and type system prevent data races and many concurrency bugs at compile time.
Threads
use std::thread;
use std::time::Duration;
// Spawn a thread
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi {i} from spawned thread");
thread::sleep(Duration::from_millis(1));
}
});
// Join: block until the thread finishes
handle.join().unwrap();- When the main thread ends, all spawned threads are killed regardless of completion.
- Use
moveclosures to transfer ownership of captured values to the thread:
let v = vec![1, 2, 3];
let handle = thread::spawn(move || println!("{v:?}"));
handle.join().unwrap();Message Passing — Channels
use std::sync::mpsc; // multiple producer, single consumer
use std::thread;
let (tx, rx) = mpsc::channel();
// Multiple producers via clone
let tx2 = tx.clone();
thread::spawn(move || {
tx.send(String::from("hello")).unwrap();
});
thread::spawn(move || {
tx2.send(String::from("world")).unwrap();
});
// Receive: blocks until a message arrives
for msg in rx { // rx as iterator: exits when all senders are dropped
println!("Got: {msg}");
}sendtakes ownership of the value — prevents use-after-send bugs.rx.recv()blocks;rx.try_recv()returns immediately withOkorErr.- The channel closes when all
txhandles are dropped, endingfor msg in rx.
Shared State — Mutex\<T\> + Arc\<T\>
use std::sync::{Arc, Mutex};
use std::thread;
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(); // blocks until lock acquired
*num += 1;
}); // MutexGuard dropped here, lock released
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10Mutex::lock()returns aMutexGuard<T>— automatically released when it goes out of scope.Arc<T>(Atomic Reference Counting): thread-safe version ofRc<T>.- A poisoned mutex (thread panicked while holding the lock) causes
lock()to returnErr.
Send and Sync Marker Traits
| Trait | Meaning |
|---|---|
Send | Safe to transfer ownership between threads |
Sync | Safe for multiple threads to hold a reference simultaneously (&T: Send) |
- Almost all primitive types are both
SendandSync. Rc<T>: neitherSendnorSync— useArc<T>for threads.RefCell<T>:Sendbut notSync— useMutex<T>for threads.- Implementing
SendorSyncmanually requiresunsafeand careful reasoning.
Notes
- Deadlocks are possible with
Mutex(e.g., two threads each waiting for the other's lock). Rust cannot prevent them at compile time. - Prefer channels (message passing) over shared state when possible — it's easier to reason about.
thread::spawnrequires'staticlifetimes for closures; useArcto share data instead of references.
Related
- Chapter 15: Smart Pointers
- Chapter 17: Async and Await
Chapter 17: Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams
Rust's async/await model for writing concurrent I/O-bound programs without blocking threads.
Core Concepts
| Term | Meaning |
|---|---|
| Future | A value representing work that may not be done yet; produced by async functions |
| async | Keyword marking a function or block as asynchronous |
| await | Postfix keyword; pauses execution until the future is ready |
| Runtime | External library (e.g., Tokio) that polls and drives futures to completion |
Futures are lazy — they do nothing until awaited.
Async Functions and Await
async fn page_title(url: &str) -> Option<String> {
// .await is postfix — enables chaining
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|t| t.inner_html())
}
// An async fn is syntactic sugar for:
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move { /* body */ }
}Running Async Code
main cannot be async. Use a runtime's block-on entry point:
fn main() {
trpl::block_on(async {
let title = page_title("https://example.com").await;
println!("{title:?}");
})
}Concurrency with Multiple Futures
// Join: run two futures concurrently, wait for both
let (result1, result2) = trpl::join(future1, future2).await;
// join! macro for arbitrary number of futures (known at compile time)
trpl::join!(fut_a, fut_b, fut_c);
// select: race futures, return whichever finishes first
match trpl::select(fut1, fut2).await {
Either::Left(val) => println!("First: {val:?}"),
Either::Right(val) => println!("Second: {val:?}"),
}Spawning Tasks
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("task: {i}");
trpl::sleep(Duration::from_millis(500)).await;
}
});
handle.await.unwrap();Yielding Control
Within a single async block, code runs synchronously between await points. Long-running work between awaits starves other futures:
// Yield control to the runtime explicitly
trpl::yield_now().await; // preferred over sleep for yieldingAsync Channels
let (tx, mut rx) = trpl::channel();
let sender = async move {
for msg in ["hello", "world"] {
tx.send(msg).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let receiver = async {
while let Some(msg) = rx.recv().await {
println!("Got: {msg}");
}
};
trpl::join(sender, receiver).await;Building Abstractions
async fn timeout<F: Future>(fut: F, max: Duration) -> Result<F::Output, Duration> {
match trpl::select(fut, trpl::sleep(max)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max),
}
}Async vs. Threads
| Threads | Async | |
|---|---|---|
| Concurrency model | OS-scheduled, preemptive | Cooperative (yields at await) |
| Overhead | High (stack per thread) | Low (state machine per future) |
| Best for | CPU-bound, parallel work | I/O-bound, many concurrent tasks |
| Data sharing | Arc<Mutex<T>> | Often avoidable via message passing |
They are complementary: many async runtimes (e.g., Tokio) use thread pools internally.
Notes
- Rust does not include an async runtime in the standard library — choose one (Tokio, async-std, smol).
- Each await point is where the compiler generates a state machine transition.
moveasync blocks transfer ownership of captured variables.- Streams (async iterators) are covered later in the chapter and allow processing sequences of asynchronously produced values.
Related
- Chapter 16: Fearless Concurrency
- Chapter 13: Iterators and Closures
Chapter 18: Object-Oriented Programming Features
How Rust relates to OOP concepts, and how to use trait objects for dynamic polymorphism.
OOP Characteristics in Rust
| OOP Concept | Rust Equivalent |
|---|---|
| Objects with data + behavior | Structs/enums + impl blocks |
| Encapsulation | pub / private by default |
| Inheritance | Not supported — use trait default methods or composition |
| Polymorphism | Generics (static) + trait objects (dynamic) |
Encapsulation example
pub struct AveragedCollection {
list: Vec<i32>, // private
average: f64, // private
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn average(&self) -> f64 { self.average }
fn update_average(&mut self) {
self.average = self.list.iter().sum::<i32>() as f64 / self.list.len() as f64;
}
}Trait Objects — Dynamic Dispatch
Use Box<dyn Trait> (or &dyn Trait) for heterogeneous collections where the concrete type is unknown at compile time.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // can hold any type implementing Draw
}
impl Screen {
pub fn run(&self) {
for component in &self.components {
component.draw(); // dynamic dispatch via vtable
}
}
}
// Any type can be added as long as it implements Draw
struct Button { width: u32, height: u32, label: String }
impl Draw for Button { fn draw(&self) { /* ... */ } }
let screen = Screen {
components: vec![
Box::new(Button { width: 50, height: 10, label: String::from("OK") }),
Box::new(SelectBox { /* ... */ }),
],
};
screen.run();Static dispatch (generics) vs dynamic dispatch (trait objects)
// Generics: monomorphization, all components must be the same concrete type
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
// Trait objects: runtime vtable lookup, components can be different types
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}- Trait objects incur a small runtime cost (vtable lookup, no inlining).
- Use trait objects for open extensibility; generics for performance with a single type.
State Pattern
OOP style (trait objects)
pub struct Post { state: Option<Box<dyn State>>, content: String }
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, _: &'a Post) -> &'a str { "" }
}Rust-native style (type-state pattern)
Encode states as different types — invalid state transitions become compile errors:
pub struct DraftPost { content: String }
pub struct PendingReview { content: String }
pub struct Post { content: String }
impl DraftPost {
pub fn new() -> DraftPost { DraftPost { content: String::new() } }
pub fn add_text(&mut self, text: &str) { self.content.push_str(text); }
pub fn request_review(self) -> PendingReview { PendingReview { content: self.content } }
}
impl PendingReview {
pub fn approve(self) -> Post { Post { content: self.content } }
}
impl Post {
pub fn content(&self) -> &str { &self.content }
}- The compiler prevents calling
content()onDraftPostorPendingReview(method doesn't exist). - No runtime overhead — transitions are just function calls.
Notes
- Rust does not support classical inheritance (no
extend/ base classes). - Default trait method implementations provide code reuse similar to inheritance.
- The type-state pattern is idiomatic Rust — prefer it over the OOP state pattern when possible.
- Object safety rules: a trait can be used as a trait object only if its methods don't have generic type parameters and don't return
Self.
Related
- Chapter 10: Generic Types, Traits, and Lifetimes
- Chapter 15: Smart Pointers
- Chapter 20: Advanced Features
Chapter 19: Patterns and Matching
Patterns are a special syntax for matching against the structure of types. They appear throughout Rust and enable expressive, type-safe deconstruction.
Where Patterns Appear
// match arms
match value {
Pattern1 => expression1,
Pattern2 => expression2,
}
// if let
if let Some(x) = optional { use(x); }
// while let
while let Ok(val) = rx.recv() { process(val); }
// for loops (variable after `for` is a pattern)
for (index, value) in v.iter().enumerate() { }
// let statements
let (x, y, z) = (1, 2, 3);
// function parameters
fn print_point(&(x, y): &(i32, i32)) { println!("({x}, {y})"); }Pattern Syntax
Literals and named variables
match x {
1 => println!("one"),
2 | 3 => println!("two or three"), // multiple patterns
1..=5 => println!("one through five"), // inclusive range
_ => println!("other"), // wildcard
}Note: named variables in match/if let create new bindings that shadow outer variables. Use match guards to compare against outer values.
Destructuring structs
struct Point { x: i32, y: i32 }
let Point { x, y } = p; // shorthand
match p {
Point { x, y: 0 } => println!("on x-axis at {x}"),
Point { x: 0, y } => println!("on y-axis at {y}"),
Point { x, y } => println!("({x}, {y})"),
}Destructuring enums
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
match msg {
Message::Quit => quit(),
Message::Move { x, y } => move_to(x, y),
Message::Write(text) => println!("{text}"),
Message::ChangeColor(r,g,b) => change(r, g, b),
}Ignoring values
fn foo(_: i32, y: i32) {} // ignore entire parameter
match (a, b) {
(Some(_), Some(_)) => println!("both some"), // ignore inner values
_ => (),
}
let Point { x, .. } = p; // ignore remaining fields with ..
let (first, .., last) = (1,2,3,4,5); // ignore middle_does not bind — no ownership transfer._xbinds but suppresses the unused warning.
Match guards
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("even: {x}"),
Some(x) => println!("odd: {x}"),
None => (),
}
// Guards solve the shadowing problem (compare against outer variable)
let y = 10;
match x {
Some(n) if n == y => println!("matched outer y"),
_ => println!("no match"),
}Guards apply to all patterns when using |:
match x {
4 | 5 | 6 if condition => println!("yes"), // condition applies to all three
_ => (),
}@ bindings — bind while testing
match msg {
Message::Hello { id: id @ 3..=7 } => println!("id in range: {id}"),
Message::Hello { id: 10..=12 } => println!("another range, id not bound"),
Message::Hello { id } => println!("id: {id}"),
}Refutable vs. Irrefutable Patterns
| Type | Description | Use in |
|---|---|---|
| Irrefutable | Always match (e.g., let x = 5) | let, function params, for |
| Refutable | May not match (e.g., Some(x)) | if let, while let, match arms |
Using a refutable pattern in let is a compile error; using an irrefutable pattern in if let generates a warning.
Notes
matchis exhaustive — all possible values must be covered.- Ranges in patterns (
1..=5) work only with numeric andchartypes. - The compiler warns when a pattern is unreachable (e.g., after a wildcard arm).
- Patterns can be nested arbitrarily deep for destructuring complex data.
Related
- Chapter 6: Enums and Pattern Matching
- Chapter 18: OOP Features
Chapter 20: Advanced Features
Unsafe Rust, advanced traits, advanced types, function pointers, and macros.
Unsafe Rust
unsafe blocks grant five additional capabilities not checked by the borrow checker:
unsafe {
// 1. Dereference raw pointers
let r1 = &raw const num; // *const i32
let r2 = &raw mut num; // *mut i32
println!("{}", *r1);
// 2. Call unsafe functions
dangerous();
// 3. Access/modify mutable static variables
static mut COUNTER: u32 = 0;
COUNTER += 1;
}
// 4. Implement unsafe traits
unsafe trait Foo {}
unsafe impl Foo for i32 {}
// 5. Access fields of unions
union MyUnion { f1: u32, f2: f32 }
unsafe { let u = MyUnion { f1: 1 }; println!("{}", u.f1); }Creating safe abstractions over unsafe code
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}FFI (Foreign Function Interface)
unsafe extern "C" {
fn abs(input: i32) -> i32; // call C function
}
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() { } // export to CBest practices: keep unsafe blocks small; document with // SAFETY: comments; use cargo +nightly miri run to detect undefined behavior.
Advanced Traits
Associated types
pub trait Iterator {
type Item; // associated type — one per implementor, unlike generics
fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> { /* ... */ }
}Default generic parameters
trait Add<Rhs = Self> { // default: add same type
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
// Override default: add Meters to Millimeters
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters { /* ... */ }
}Disambiguation and fully qualified syntax
Pilot::fly(&person); // call specific trait's method
<Dog as Animal>::baby_name(); // fully qualified: no self parameterSupertraits
trait OutlinePrint: fmt::Display { // requires Display to also be implemented
fn outline_print(&self) { let s = self.to_string(); /* ... */ }
}Newtype pattern (bypass orphan rule)
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper { // can now impl Display for Vec
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}Advanced Types
// Type alias: synonym, no extra type safety
type Kilometers = i32;
type Thunk = Box<dyn Fn() + Send + 'static>; // useful for long types
// Never type (!): functions that never return
fn bar() -> ! { panic!(""); }
// Dynamically sized types: always behind a pointer
let s: &str = "hello"; // str is DST, &str is fat pointer
let b: Box<dyn Trait> = ...; // dyn Trait is DST
// ?Sized: allow DSTs as generic parameter
fn generic<T: ?Sized>(t: &T) { }Advanced Functions and Closures
// Function pointers: fn is a type, not a trait
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); // 12
// fn implements Fn, FnMut, FnOnce — can pass named functions where closures expected
let strings: Vec<String> = nums.iter().map(ToString::to_string).collect();
// Returning closures
fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 }
fn returns_dynamic() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) }Macros
Declarative macros (macro_rules!)
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp = Vec::new();
$( temp.push($x); )*
temp
}
};
}Procedural macros
// Custom derive
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
quote! {
impl HelloMacro for #name {
fn hello_macro() { println!("Hello from {}!", stringify!(#name)); }
}
}.into()
}
// Attribute-like macro
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// Function-like macro
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }| Macro type | Syntax | Use case |
|---|---|---|
macro_rules! | vec![...] | Variable args, general metaprogramming |
| Derive | #[derive(Trait)] | Auto-implement traits |
| Attribute | #[route(GET, "/")] | Custom attributes on any item |
| Function-like | sql!(SELECT ...) | Complex code generation |
Macros vs. functions: macros can accept variable argument counts, generate code, implement traits, and expand before type-checking; functions cannot.
Notes
unsafedoesn't disable the borrow checker; it only unlocks the five superpowers listed above.- Associated types vs. generics: associated types allow only one implementation per type; generics allow multiple.
- Procedural macros live in separate crates with
proc-macro = trueinCargo.toml. - The
synandquotecrates are the standard tools for procedural macro development.
Related
- Chapter 10: Generic Types, Traits, and Lifetimes
- Chapter 18: OOP Features
- Chapter 21: Final Project
Chapter 21: Final Project — Building a Multithreaded Web Server
A capstone project combining concepts from the entire book: TCP networking, HTTP parsing, thread pools, and graceful shutdown.
Project Overview
Build hello — a minimal HTTP server that: 1. Listens for TCP connections on 127.0.0.1:7878 2. Parses HTTP GET requests 3. Returns hello.html for /, 404.html otherwise 4. Handles connections concurrently via a thread pool of 4 workers
Single-Threaded Server
use std::fs;
use std::io::{BufReader, prelude::*};
use std::net::{TcpListener, TcpStream};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
handle_connection(stream.unwrap());
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!(
"{status_line}\r\nContent-Length: {}\r\n\r\n{contents}",
contents.len()
);
stream.write_all(response.as_bytes()).unwrap();
}Thread Pool
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
type Job = Box<dyn FnOnce() + Send + 'static>;
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
impl ThreadPool {
pub fn new(size: usize) -> Self {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let workers = (0..size)
.map(|id| Worker::new(id, Arc::clone(&receiver)))
.collect();
ThreadPool { workers, sender: Some(sender) }
}
pub fn execute<F>(&self, f: F)
where F: FnOnce() + Send + 'static {
self.sender.as_ref().unwrap().send(Box::new(f)).unwrap();
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
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} executing."); job(); }
Err(_) => { println!("Worker {id} shutting down."); break; }
}
});
Worker { id, thread: Some(thread) }
}
}Graceful shutdown via Drop
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take()); // close channel → workers receive Err and exit loop
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap(); // wait for each worker to finish
}
}
}
}Multithreaded Server
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) { // limit for demo
let stream = stream.unwrap();
pool.execute(|| handle_connection(stream));
}
println!("Shutting down.");
} // pool.drop() called here — joins all worker threadsKey Design Decisions
| Decision | Rationale |
|---|---|
| Fixed thread pool size | Prevents DoS via unlimited thread spawning |
mpsc::channel for jobs | Multiple producers (main) → one receiver per worker |
Arc<Mutex<Receiver>> | Shared ownership + mutual exclusion for lock safety |
FnOnce + Send + 'static | Jobs execute once; cross-thread safe; no borrowed data |
Lock released before job() | Release lock after recv() so other workers aren't blocked during execution |
Option<Sender> + take() | Allows cleanly closing channel in Drop |
Notes
- Dropping the sender (channel close) causes
recv()in workers to returnErr, signaling them to exit their loop — this is the graceful shutdown mechanism. - The
while let Ok(job) = ...pattern would hold the mutex lock during job execution, blocking other workers. Thelet job = ...; job()pattern releases it first. - Port 7878 is "rust" on a telephone keypad.
- For production servers, use an existing crate (e.g., Hyper, Axum) instead of this manual implementation.
Related
- Chapter 16: Fearless Concurrency
- Chapter 15: Smart Pointers
- Chapter 13: Iterators and Closures
book
| Name | Description | Path |
|---|---|---|
| Chapter 1: Getting Started | Introduction to Rust: installation, writing your first program, and using Cargo. | 01-getting-started.md |
| Chapter 2: Programming a Guessing Game | A hands-on introduction to Rust fundamentals through building a complete… | 02-guessing-game.md |
| Chapter 3: Common Programming Concepts | Fundamental Rust building blocks: variables, data types, functions, comments,… | 03-common-programming-concepts.md |
| Chapter 4: Understanding Ownership | Rust's core memory-management feature: ownership, borrowing, and slices… | 04-ownership.md |
| Chapter 5: Using Structs | Custom data types that group related named fields, with methods to attach… | 05-structs.md |
| Chapter 6: Enums and Pattern Matching | Enumerations express a value that can be one of several variants. Combined… | 06-enums-and-pattern-matching.md |
| Chapter 7: Managing Growing Projects with Packages, Crates, and Modules | Rust's module system for organizing code into packages, crates, and modules… | 07-packages-crates-modules.md |
| Chapter 8: Common Collections | Heap-allocated collections that can grow and shrink at runtime: vectors,… | 08-common-collections.md |
| Chapter 9: Error Handling | Rust distinguishes recoverable errors (Result<T, E>) from unrecoverable ones… | 09-error-handling.md |
| Chapter 10: Generic Types, Traits, and Lifetimes | Generics eliminate code duplication; traits define shared behavior; lifetimes… | 10-generic-types-traits-lifetimes.md |
| Chapter 11: Writing Automated Tests | Rust's built-in testing framework: test functions, assertion macros, and… | 11-testing.md |
| Chapter 12: An I/O Project — Building a Command Line Program | A practical project (minigrep) consolidating Chapters 1–11: reading CLI… | 12-io-project.md |
| Chapter 13: Functional Language Features — Iterators and Closures | Closures are anonymous functions that capture their environment; iterators… | 13-iterators-closures.md |
| Chapter 14: More about Cargo and Crates.io | Release profiles, documentation, publishing, workspaces, and installing… | 14-cargo-crates-io.md |
| Chapter 15: Smart Pointers | Data structures that act like pointers but provide additional metadata and… | 15-smart-pointers.md |
| Chapter 16: Fearless Concurrency | Rust's ownership and type system prevent data races and many concurrency… | 16-fearless-concurrency.md |
| Chapter 17: Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams | Rust's async/await model for writing concurrent I/O-bound programs without… | 17-async-await.md |
| Chapter 18: Object-Oriented Programming Features | How Rust relates to OOP concepts, and how to use trait objects for dynamic… | 18-oop.md |
| Chapter 19: Patterns and Matching | Patterns are a special syntax for matching against the structure of types.… | 19-patterns-and-matching.md |
| Chapter 20: Advanced Features | Unsafe Rust, advanced traits, advanced types, function pointers, and macros. | 20-advanced-features.md |
| Chapter 21: Final Project — Building a Multithreaded Web Server | A capstone project combining concepts from the entire book: TCP networking,… | 21-final-project.md |
Hello World
The entry point of a Rust program is fn main(). The println! macro (note the !) prints text to stdout. Comments begin with //.
Signature / Usage
fn main() {
// This is a comment, ignored by the compiler.
println!("Hello World!");
}Compilation
$ rustc hello.rs
$ ./hello
Hello World!Notes
println!is a macro, not a function — indicated by the trailing!.- The
mainfunction is required as the entry point of every executable. - Subsections cover: Comments (
//,/* */,///,//!), Formatted print (print!,println!,eprint!,format!), and Debug/Display formatting.
Related
- 02-primitives.md
Primitives
Rust's primitive types include scalar types (integers, floats, bool, char) and compound types (arrays, tuples). The compiler infers types from context; integers default to i32, floats to f64.
Scalar Types
| Category | Types |
|---|---|
| Signed integers | i8, i16, i32, i64, i128, isize |
| Unsigned integers | u8, u16, u32, u64, u128, usize |
| Floating point | f32, f64 |
| Character | char (Unicode scalar value, 4 bytes) |
| Boolean | bool (true / false) |
| Unit | () |
Compound Types
| Type | Example | Description |
|---|---|---|
| Array | [1, 2, 3] | Fixed-size, same type |
| Tuple | (1, true, 3.0) | Fixed-size, mixed types |
Signature / Usage
fn main() {
// Explicit type annotation
let logical: bool = true;
let an_integer = 5i32; // suffix annotation
// Default types
let default_float = 3.0; // f64
let default_integer = 7; // i32
// Type inferred from later use
let mut inferred_type = 12;
inferred_type = 4294967296i64;
// Mutable variable
let mut mutable = 12;
mutable = 21;
// Shadowing (rebind with new let)
let mutable = true;
// Array: [Type; length]
let my_array: [i32; 5] = [1, 2, 3, 4, 5];
// Tuple
let my_tuple = (5u32, 1u8, true, -5.04f32);
}Notes
- Integer overflow panics in debug mode; wraps in release mode.
usize/isizesize matches the pointer width of the platform (32 or 64 bit).- Subsections: Literals and operators, Tuples, Arrays and slices.
Related
- 05-types.md
- 06-conversion.md
Custom Types
Rust defines custom data types with struct (named fields, tuple structs, unit structs) and enum (C-like enums, enums with data). Constants are defined with const and static.
Signature / Usage
// Named-field struct
struct Point {
x: f64,
y: f64,
}
// Tuple struct
struct Pair(i32, i32);
// Unit struct (no fields)
struct Unit;
// Enum with variants
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
}
// Enum carrying data
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
// Constants
const MAX_POINTS: u32 = 100_000;
static LANGUAGE: &str = "Rust";
fn main() {
let p = Point { x: 1.0, y: 2.0 };
let pair = Pair(1, 2);
// Destructure struct
let Point { x, y } = p;
let dir = Direction::North;
println!("{:?}", dir);
}Notes
structupdate syntax (..other_struct) copies remaining fields from another instance.enumvariants can be unit-like, tuple-like, or struct-like.const: compile-time constant, no fixed memory address.static: single memory location for program lifetime;static mutis unsafe.- Use
#[derive(Debug)]to enable{:?}printing.
Related
- 08-flow-of-control.md
- 16-traits.md
Variable Bindings
Values are bound to names with let. Rust is statically typed and infers types from context. Variables are immutable by default; add mut to allow mutation.
Signature / Usage
fn main() {
// Immutable binding with type inference
let an_integer = 1u32;
let a_boolean = true;
let unit = ();
// Suppress unused-variable warning with underscore prefix
let _unused = 42;
// Mutable binding
let mut mutable = 12;
mutable = 21;
// Scope: inner binding shadows outer
let shadowed = 1;
{
let shadowed = 2; // shadows outer `shadowed`
println!("inner: {}", shadowed); // 2
}
println!("outer: {}", shadowed); // 1
// Shadowing in same scope (rebind with new type)
let shadowed = "now a string";
// Declare first, initialize later
let declared;
declared = 5i32;
println!("{}", declared);
// Freezing: shadowing mut with immutable binding freezes it
let mut frozen = 7i32;
{
let frozen = frozen; // immutable copy — `frozen` is now frozen
// frozen = 50; // error: cannot assign to immutable variable
}
frozen = 3; // OK back in outer scope
}Notes
- A variable declared but never initialized cannot be used (compile error).
- Shadowing allows changing the type of a binding without
mut. - "Freezing" occurs when a mutable variable is re-bound immutably in an inner scope.
Related
- 02-primitives.md
- 15-scoping-rules.md
Types
Rust supports explicit casting with as, type inference from context, and type aliases with type. There is no implicit conversion between primitive types.
Casting with as
fn main() {
let decimal = 65.4321_f32;
// Explicit cast — no implicit conversion in Rust
let integer = decimal as u8; // 65
let character = integer as char; // 'A'
// Overflow wraps for integer targets (modular arithmetic)
println!("{}", 1000u32 as u8); // 232 (1000 % 256)
// Float-to-int saturates since Rust 1.45
println!("{}", 300.0_f32 as u8); // 255
println!("{}", -1.0_f32 as u8); // 0
}Type Inference
fn main() {
// Compiler infers Vec<i64> from later push
let mut vec = Vec::new();
vec.push(4i64);
}Type Aliasing with type
type NanoSecond = u64;
type Inch = u64;
fn main() {
let ns: NanoSecond = 5;
let inch: Inch = 2;
// Aliases are the same underlying type, so addition compiles:
println!("{}", ns + inch);
}Notes
- Type aliases do not create new types — they are alternative names and provide no extra type safety.
- Alias names should be
UpperCamelCase(primitives likeusizeare excepted). - The primary use of aliases is to reduce boilerplate (e.g.,
io::Result<T>=Result<T, io::Error>). - Literals can specify their type via suffix:
42u8,3.14f32.
Related
- 06-conversion.md
- 02-primitives.md
Conversion
Rust uses traits to handle type conversion between custom types. The primary traits are From/Into (infallible) and TryFrom/TryInto (fallible). String conversion uses ToString/FromStr.
From and Into
Implement From<T> and Into is automatically derived (but not vice versa).
use std::convert::From;
#[derive(Debug)]
struct Number {
value: i32,
}
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}
fn main() {
// Using From
let num = Number::from(30);
println!("{:?}", num);
// Using Into (requires type annotation)
let num: Number = 5i32.into();
println!("{:?}", num);
// Standard library From: &str -> String
let s = String::from("hello");
}TryFrom and TryInto
For conversions that may fail, returning Result.
use std::convert::TryFrom;
#[derive(Debug, PartialEq)]
struct EvenNumber(i32);
impl TryFrom<i32> for EvenNumber {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNumber(value))
} else {
Err(())
}
}
}
fn main() {
assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
assert_eq!(EvenNumber::try_from(5), Err(()));
}ToString and FromStr
use std::str::FromStr;
fn main() {
// ToString via Display trait
let s = 42.to_string();
// Parse string to type
let n: i32 = "42".parse().unwrap();
let n = i32::from_str("42").unwrap();
}Notes
- Implementing
From<T> for Uautomatically providesInto<U> for T. TryFrom/TryIntoreturnResult<T, Error>— use when conversion can logically fail.- Implement
fmt::Displayto getto_string()for free via the blanketToStringimpl.
Related
- 05-types.md
- 16-traits.md
Expressions
A Rust program is made up of statements and expressions. Expressions evaluate to a value; statements perform an action. Blocks {} are expressions — their value is the last expression inside (without a trailing ;).
Signature / Usage
fn main() {
let x = 5;
// Statement (binds a value, returns nothing)
let y = {
let x_squared = x * x;
let x_cube = x_squared * x;
// Last expression — no semicolon — becomes the value of the block
x_cube + x_squared + x
};
println!("y = {}", y); // y = 155
// Adding `;` suppresses the return value — block returns `()`
let z = {
2 * x; // semicolon → statement, block returns ()
};
// z is ()
}Notes
- Omitting the semicolon on the final line of a block makes it an expression that returns a value.
- Adding a semicolon converts the final expression to a statement, making the block return
(). if/else,match, andloopare also expressions and can be used on the right-hand side oflet.
Related
- 08-flow-of-control.md
- 09-functions.md
Flow of Control
Rust provides if/else, loop, while, for, match, if let, let else, and while let. Most are expressions that return a value.
if / else
fn main() {
let n = 5;
if n < 0 {
println!("negative");
} else if n > 0 {
println!("positive");
} else {
println!("zero");
}
// if as expression
let big_n = if n < 10 { 10 * n } else { n / 2 };
}loop
fn main() {
let mut count = 0u32;
loop {
count += 1;
if count == 3 { continue; }
if count == 5 { break; }
}
// loop returns a value
let result = loop {
count += 1;
if count == 10 { break count * 2; }
};
}while / while let
fn main() {
let mut n = 1;
while n < 101 { n *= 2; }
// while let: loop while pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
}for and ranges
fn main() {
for n in 1..=5 { // inclusive range
println!("{}", n);
}
let names = vec!["Alice", "Bob"];
for name in names.iter() {
println!("{}", name);
}
}match
fn main() {
let number = 13;
match number {
1 => println!("One!"),
2 | 3 | 5 => println!("Prime"),
13..=19 => println!("Teen"),
_ => println!("Other"),
}
// match as expression
let boolean = true;
let binary = match boolean { false => 0, true => 1 };
}if let / let else
fn main() {
let opt: Option<i32> = Some(7);
// if let — concise single-pattern match
if let Some(i) = opt {
println!("Got {}", i);
}
// let else — bind or diverge (break/return/panic)
let Some(value) = opt else {
panic!("No value");
};
println!("{}", value);
}Notes
matchmust be exhaustive — all cases must be covered.- Labeled loops (
'outer: loop) allowbreak 'outerto exit nested loops. forloop:iter()borrows,into_iter()consumes,iter_mut()mutably borrows.
Related
- 07-expressions.md
- 03-custom-types.md
Functions
Functions are declared with fn. Arguments must be type-annotated; the return type follows ->. The final expression is returned implicitly (no return needed). Closures are anonymous functions that can capture their environment.
Basic Functions
fn is_divisible(lhs: u32, rhs: u32) -> bool {
if rhs == 0 { return false; } // early return
lhs % rhs == 0 // implicit return
}
fn fizzbuzz(n: u32) { // returns () implicitly
if is_divisible(n, 15) { println!("fizzbuzz"); }
else if is_divisible(n, 3) { println!("fizz"); }
else if is_divisible(n, 5) { println!("buzz"); }
else { println!("{}", n); }
}
fn main() {
for n in 1..=20 { fizzbuzz(n); }
}Closures
fn main() {
let outer = 42;
// Type annotations are optional — inferred from usage
let add = |i: i32| -> i32 { i + outer };
let add_inferred = |i| i + outer;
println!("{}", add(1)); // 43
println!("{}", add_inferred(1)); // 43
// Closures as arguments: Fn, FnMut, FnOnce
fn apply<F: Fn()>(f: F) { f(); }
apply(|| println!("called!"));
}Methods
struct Rectangle { width: f64, height: f64 }
impl Rectangle {
// Associated function (no self)
fn new(w: f64, h: f64) -> Self {
Rectangle { width: w, height: h }
}
// Method (takes &self)
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let r = Rectangle::new(3.0, 4.0);
println!("area = {}", r.area());
}Higher-Order Functions
fn main() {
// Iterator combinators
let sum: u32 = (1..=10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
println!("{}", sum); // 220
}Diverging Functions
// Never returns; return type is `!`
fn diverge() -> ! {
panic!("This function never returns");
}Notes
- Function definition order does not matter in Rust (unlike C).
- Closures capture by reference by default; use
moveto capture by value. Fn: borrows immutably;FnMut: borrows mutably;FnOnce: takes ownership (can only be called once).- Returning closures from functions requires
impl Fn(...)orBox<dyn Fn(...)>.
Related
- 07-expressions.md
- 14-generics.md
- 16-traits.md
Modules
Modules (mod) organize code into logical units and control visibility. Items are private by default; pub makes them public.
Signature / Usage
mod my_mod {
// Private by default
fn private_fn() {}
pub fn public_fn() {
println!("public");
}
pub mod nested {
pub fn nested_fn() {
// Access parent-scope private item via super
super::private_fn();
}
}
}
fn main() {
my_mod::public_fn();
my_mod::nested::nested_fn();
// my_mod::private_fn(); // error: private
}Visibility Modifiers
| Modifier | Scope |
|---|---|
| (none) | Private to the current module |
pub | Accessible everywhere |
pub(crate) | Accessible within the current crate |
pub(super) | Accessible to the parent module |
pub(in path) | Accessible within a specific module path |
use Declaration
use my_mod::nested::nested_fn;
fn main() {
nested_fn(); // no full path needed
}
// Alias with `as`
use std::fmt::Result as FmtResult;File Hierarchy
Split modules across files:
src/
main.rs // mod my_module; ← declares module
my_module.rs // contents of my_moduleOr using a directory:
src/
main.rs
my_module/
mod.rs // contents of my_module
sub.rs // mod sub; declared in mod.rsNotes
selfrefers to the current module;superrefers to the parent module.usecan glob-import withuse my_mod::*;(use sparingly).- Struct fields follow their own visibility rules independently of the struct itself.
Related
- 11-crates.md
- 12-cargo.md
Crates
A crate is the fundamental compilation unit in Rust. Running rustc file.rs treats that file as the crate root. Crates produce either a binary (executable) or a library.
Creating a Library
// rary.rs — a library crate
pub fn public_function() {
println!("called rary's public_function()");
}
fn private_function() {
println!("called rary's private_function()");
}
pub fn indirect_access() {
private_function();
}Compile to a library:
$ rustc --crate-type=lib rary.rs
# produces library.rlibUsing a Library
// executable.rs
fn main() {
rary::public_function();
rary::indirect_access();
}Link against the library:
$ rustc executable.rs --extern rary=library.rlibNotes
- Only crates are compiled as complete units;
modfiles within a crate are inlined before compilation. - Library crates export a public API; private items are inaccessible from outside the crate.
- In practice, use Cargo to manage crates and their dependencies rather than calling
rustcdirectly. - Crate type options for
--crate-type:bin,lib,rlib,dylib,cdylib,staticlib,proc-macro.
Related
- 10-modules.md
- 12-cargo.md
Cargo
Cargo is Rust's official package manager and build tool. It manages dependencies from crates.io, runs tests, benchmarks, and build scripts.
Common Commands
cargo new my_project # create a new binary project
cargo new --lib my_lib # create a library project
cargo build # compile (debug)
cargo build --release # compile with optimizations
cargo run # build and run
cargo test # run tests
cargo doc --open # build and open documentation
cargo clean # remove build artifactsCargo.toml — Project Manifest
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.8"
[dev-dependencies]
# dependencies only for tests / examples
pretty_assertions = "1"
[build-dependencies]
# dependencies for build scripts
cc = "1"Conventions
my_project/
Cargo.toml
Cargo.lock # exact dependency versions (commit for binaries)
src/
main.rs # binary entry point
lib.rs # library entry point (optional)
tests/ # integration tests
examples/ # runnable examples (cargo run --example name)
benches/ # benchmarks
build.rs # build script (optional)Build Scripts
build.rs runs before compilation — useful for code generation or linking native libraries:
// build.rs
fn main() {
println!("cargo:rustc-link-lib=ssl");
}Notes
Cargo.lockshould be committed for binaries; libraries typically exclude it from VCS.- Feature flags:
cargo build --features "feat1 feat2". - Workspace: group multiple packages under one
[workspace]in a rootCargo.toml.
Related
- 11-crates.md
- 21-testing.md
- 24-meta.md
Attributes
An attribute is metadata applied to a crate, module, or item. Outer attributes (#[...]) apply to the next item; inner attributes (#![...]) apply to the enclosing item (typically the whole crate/module).
Syntax
#[attribute]
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value1, value2)]Common Attributes
dead_code — suppress unused warnings
#[allow(dead_code)]
fn unused_fn() {}derive — auto-implement standard traits
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }
fn main() {
let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p); // Debug
let q = p.clone(); // Clone
assert_eq!(p, q); // PartialEq
}cfg — conditional compilation
#[cfg(target_os = "linux")]
fn linux_only() { println!("running on Linux"); }
#[cfg(feature = "my_feature")]
fn feature_gated() {}
fn main() {
#[cfg(debug_assertions)]
println!("debug build");
}crate-level attributes
// lib.rs
#![crate_name = "my_lib"]
#![crate_type = "lib"]
#![allow(unused_variables)]Notes
#[test]marks a function as a unit test.#[inline]/#[inline(always)]hint the compiler to inline a function.#[must_use]causes a compiler warning if the return value is ignored.- Custom
cfgflags: pass-C --cfg 'flag'torustcor use[features]inCargo.toml. - Procedural macros can define custom attributes (e.g.,
#[derive(Serialize)]fromserde).
Related
- 17-macros.md
- 21-testing.md
Generics
Generics allow functions, structs, enums, and traits to operate over multiple types while maintaining type safety. Type parameters are written in angle brackets (<T>).
Generic Functions and Structs
// Concrete struct
struct A;
// Generic struct — accepts any type T
struct Wrapper<T>(T);
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter() {
if item > largest { largest = item; }
}
largest
}
fn main() {
let w = Wrapper(42i32);
let w2 = Wrapper("hello");
let numbers = vec![34, 50, 25, 100, 65];
println!("{}", largest(&numbers)); // 100
}Generic Implementations
struct Pair<T> { first: T, second: T }
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("first is larger: {}", self.first);
} else {
println!("second is larger: {}", self.second);
}
}
}Trait Bounds and where Clauses
use std::fmt::Debug;
fn print_if_debug<T: Debug>(val: T) {
println!("{:?}", val);
}
// Equivalent with where clause (cleaner for multiple bounds)
fn print_both<T, U>(t: T, u: U)
where
T: Debug,
U: Debug + Clone,
{
println!("{:?} {:?}", t, u);
}Associated Types
trait Container {
type Item;
fn first(&self) -> Option<&Self::Item>;
}
struct Stack<T>(Vec<T>);
impl<T> Container for Stack<T> {
type Item = T;
fn first(&self) -> Option<&T> { self.0.first() }
}Phantom Types
use std::marker::PhantomData;
// PhantomData<T> marks a type parameter that isn't stored
struct Tagged<T> {
value: f64,
_tag: PhantomData<T>,
}Notes
- Generic type parameters are resolved at compile time — zero runtime cost (monomorphization).
- Use
whereclauses when bounds become complex or when the signature would be hard to read. impl Traitin function position is syntactic sugar for a generic with a trait bound.- Multiple bounds:
T: Display + Clone.
Related
- 16-traits.md
- 15-scoping-rules.md
Scoping Rules
Scopes determine when resources are freed (RAII), when borrows are valid, and when lifetimes start and end. These rules underpin Rust's memory safety guarantees without a garbage collector.
RAII — Automatic Resource Management
fn create_box() {
let _b = Box::new(3i32);
// `_b` dropped here — heap memory freed automatically
}
fn main() {
let _box2 = Box::new(5i32);
{
let _box3 = Box::new(4i32);
} // _box3 dropped here
// _box2 dropped at end of main
}Implement the Drop trait for custom cleanup:
struct MyResource;
impl Drop for MyResource {
fn drop(&mut self) { println!("dropped!"); }
}Ownership and Moves
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved — no longer valid
// println!("{}", s1); // error: value used after move
// Copy types (integers, bool, etc.) are copied, not moved
let x = 5;
let y = x; // x is still valid
}Borrowing
fn borrow(s: &String) { // immutable borrow
println!("{}", s);
}
fn borrow_mut(s: &mut String) { // mutable borrow
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
borrow(&s); // s still owned here
borrow_mut(&mut s);
println!("{}", s);
}Borrow rules (enforced at compile time):
- Any number of immutable borrows at once, OR
- Exactly one mutable borrow — never both simultaneously.
Lifetimes
Lifetime annotations describe how long references are valid. The compiler infers most lifetimes automatically (elision rules).
// Explicit lifetime: 'a means both inputs and output live at least as long as 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // OK — both live here
}
}Notes
- Stack values that implement
Copyare copied on assignment; heap-owning values are moved. - A dangling reference is a compile error — the borrow checker rejects code where a reference outlives its referent.
'staticlifetime means a reference is valid for the entire program duration (e.g., string literals).- Lifetime elision rules: most
&self/&mut selfmethods have lifetimes inferred automatically.
Related
- 04-variable-bindings.md
- 14-generics.md
- 19-std-library-types.md
Traits
A trait defines a set of methods for an unknown type Self. Any type can implement a trait; traits enable polymorphism and shared behavior across types.
Defining and Implementing Traits
trait Animal {
fn new(name: &'static str) -> Self;
fn name(&self) -> &'static str;
fn noise(&self) -> &'static str;
// Default implementation
fn talk(&self) {
println!("{} says {}", self.name(), self.noise());
}
}
struct Dog { name: &'static str }
impl Animal for Dog {
fn new(name: &'static str) -> Dog { Dog { name } }
fn name(&self) -> &'static str { self.name }
fn noise(&self) -> &'static str { "woof!" }
// talk() inherited from default
}
fn main() {
let d: Dog = Animal::new("Rex");
d.talk(); // Rex says woof!
}derive — Auto-implement Standard Traits
#[derive(Debug, Clone, PartialEq, PartialOrd)]
struct Point { x: f64, y: f64 }Derivable traits: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default.
Operator Overloading
use std::ops::Add;
#[derive(Debug)]
struct Vec2 { x: f64, y: f64 }
impl Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2 { x: self.x + other.x, y: self.y + other.y }
}
}impl Trait — Return Trait Objects
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
}Supertraits
trait Printable: std::fmt::Display {
fn print(&self) { println!("{}", self); }
}Disambiguating Overlapping Traits
trait UsernameWidget { fn get(&self) -> String; }
trait AgeWidget { fn get(&self) -> u8; }
struct Form;
impl UsernameWidget for Form { fn get(&self) -> String { "Alice".into() } }
impl AgeWidget for Form { fn get(&self) -> u8 { 30 } }
fn main() {
let form = Form;
// Fully-qualified syntax to disambiguate
println!("{}", <Form as UsernameWidget>::get(&form));
println!("{}", <Form as AgeWidget>::get(&form));
}Notes
dyn Traitis a trait object for dynamic dispatch;impl Traitis static dispatch.- The
Iteratortrait (next() -> Option<Self::Item>) powers Rust's iterator combinators. Droptrait: implementdrop(&mut self)for custom cleanup when a value goes out of scope.Clonerequires explicit.clone()call;Copyenables implicit bitwise copy for small types.
Related
- 14-generics.md
- 06-conversion.md
- 09-functions.md
macro_rules!
Macros are metaprogramming constructs that expand into Rust code at compile time. macro_rules! defines declarative macros using pattern matching on syntax trees (not string replacement).
Basic Definition
macro_rules! say_hello {
() => {
println!("Hello!")
};
}
fn main() {
say_hello!(); // expands to println!("Hello!")
}Designators (Pattern Matchers)
| Designator | Matches |
|---|---|
expr | expressions |
stmt | statements |
ty | types |
ident | identifiers |
path | module paths |
tt | token tree |
literal | literal values |
item | items (fn, struct, etc.) |
block | blocks {} |
meta | attribute metadata |
macro_rules! create_fn {
($func_name:ident) => {
fn $func_name() {
println!("function: {:?}", stringify!($func_name));
}
};
}
create_fn!(foo);
create_fn!(bar);
fn main() {
foo(); // function: "foo"
bar(); // function: "bar"
}Overloading (Multiple Arms)
macro_rules! test {
($left:expr; and $right:expr) => {
println!("{} AND {}", $left, $right);
};
($left:expr; or $right:expr) => {
println!("{} OR {}", $left, $right);
};
}
fn main() {
test!(1 + 1 == 2; and 2 + 2 == 4);
test!(true; or false);
}Repetition
Use $(...)* (zero or more) or $(...)+ (one or more):
macro_rules! vec_of_strings {
($($x:expr),*) => {
vec![$($x.to_string()),*]
};
}
fn main() {
let v = vec_of_strings!["hello", "world"];
}Use Cases
- DRY: avoid duplicating logic for different types.
- DSL: build mini-languages (e.g.,
html! {}in Yew). - Variadic: accept variable number of arguments (like
println!,vec!).
Notes
- Macro names end with
!at call sites. - Macros expand before type checking; errors can be hard to diagnose.
- Procedural macros (
#[derive(...)], attribute macros, function-like macros) offer more power but require a separate crate withproc-macro = true. - Use
macro_exportto make a macro available outside its defining crate.
Related
- 13-attributes.md
- 09-functions.md
Error Handling
Rust distinguishes recoverable errors (Result<T, E>) from unrecoverable ones (panic!). The Option<T> type handles values that may be absent. The ? operator propagates errors concisely.
panic!
fn main() {
// Unrecoverable — terminates the thread
panic!("something went terribly wrong");
}Use for truly unrecoverable situations, prototype code, or tests.
Option
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
match divide(4.0, 2.0) {
Some(v) => println!("{}", v),
None => println!("cannot divide by zero"),
}
// Combinators
let doubled = divide(4.0, 2.0).map(|v| v * 2.0);
let val = divide(4.0, 0.0).unwrap_or(0.0);
let val = divide(4.0, 2.0).expect("division failed"); // panics with message on None
}Result
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n = s.parse::<i32>()?; // `?` returns Err early if parse fails
Ok(n * 2)
}
fn main() {
match parse_and_double("5") {
Ok(v) => println!("{}", v), // 10
Err(e) => println!("Error: {}", e),
}
}The ? Operator
? unwraps Ok or returns the Err early. Works in functions returning Result or Option.
use std::fs;
use std::io;
fn read_file(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // propagate io::Error
Ok(content)
}Box\<dyn Error\> — Multiple Error Types
use std::error::Error;
use std::num::ParseIntError;
fn double_first(vec: &[&str]) -> Result<i32, Box<dyn Error>> {
let first = vec.first().ok_or("vector is empty")?;
let n = first.parse::<i32>()?;
Ok(2 * n)
}Defining Custom Error Types
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(s) => write!(f, "not found: {}", s),
AppError::ParseError(s) => write!(f, "parse error: {}", s),
}
}
}
impl std::error::Error for AppError {}Iterating over Results
fn main() {
let strings = vec!["1", "two", "3"];
// Collect successes and ignore failures
let numbers: Vec<i32> = strings.iter()
.filter_map(|s| s.parse().ok())
.collect();
println!("{:?}", numbers); // [1, 3]
// Fail on first error
let result: Result<Vec<i32>, _> = strings.iter()
.map(|s| s.parse::<i32>())
.collect();
}Notes
- Prefer
Resultoverpanic!in library code; let callers decide how to handle errors. unwrap()andexpect()panic onErr/None— acceptable in tests and prototypes.- Use the
thiserrorcrate to deriveErrorimplementations ergonomically. - Use the
anyhowcrate for easyBox<dyn Error>equivalents in application code.
Related
- 08-flow-of-control.md
- 19-std-library-types.md
Std Library Types
The standard library provides essential types beyond primitives: Box, Vec, String, Option, Result, HashMap, HashSet, Rc, and Arc.
Box\<T\> — Heap Allocation
fn main() {
// Allocate on heap
let boxed: Box<i32> = Box::new(5);
let val: i32 = *boxed; // dereference
// Box enables recursive types
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}Vec\<T\> — Growable Array
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(1); v.push(2); v.push(3);
// Macro shorthand
let v = vec![1, 2, 3];
println!("{}", v[0]); // 1
println!("{:?}", &v[1..]); // [2, 3]
println!("{}", v.len()); // 3
}String — Growable UTF-8 String
fn main() {
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s += "world";
let literal: &str = "slice";
let owned: String = literal.to_string();
println!("{}", s.len()); // byte length
}HashMap\<K, V\>
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<&str, i32> = HashMap::new();
scores.insert("Alice", 10);
scores.insert("Bob", 20);
// Entry API — insert if absent
scores.entry("Alice").or_insert(50); // no change
scores.entry("Carol").or_insert(50); // inserts 50
if let Some(score) = scores.get("Alice") {
println!("{}", score); // 10
}
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}HashSet\<T\>
use std::collections::HashSet;
fn main() {
let mut a: HashSet<i32> = [1, 2, 3].iter().cloned().collect();
let b: HashSet<i32> = [2, 3, 4].iter().cloned().collect();
let union: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();
}Rc\<T\> — Reference Counting (single-threaded)
use std::rc::Rc;
fn main() {
let a = Rc::new(5);
let b = Rc::clone(&a); // increments count
println!("count = {}", Rc::strong_count(&a)); // 2
}Arc\<T\> — Atomic Reference Counting (multi-threaded)
use std::sync::Arc;
use std::thread;
fn main() {
let val = Arc::new(42);
let val2 = Arc::clone(&val);
let handle = thread::spawn(move || println!("{}", val2));
handle.join().unwrap();
}Notes
Boxis for single ownership + heap allocation; use when size is unknown at compile time.Rcfor multiple owners in single-threaded code;Arcfor multi-threaded.HashMapis unordered; useBTreeMapfor sorted keys.Stringowns its data;&stris a borrowed string slice.Vecis backed by a contiguous heap allocation;&[T]is a borrowed slice.
Related
- 15-scoping-rules.md
- 18-error-handling.md
- 20-std-misc.md
Std Misc
Additional standard library features for concurrency, file I/O, processes, and inter-process communication.
Threads
use std::thread;
const NTHREADS: u32 = 10;
fn main() {
let mut handles = vec![];
for i in 0..NTHREADS {
handles.push(thread::spawn(move || {
println!("thread {}", i);
}));
}
for h in handles {
h.join().unwrap(); // wait for each thread
}
}Channels — Message Passing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("hello from thread").unwrap();
});
let msg = rx.recv().unwrap();
println!("{}", msg);
}mpsc = multiple producer, single consumer. Clone tx for multiple senders.
File I/O
use std::fs::{self, File};
use std::io::{self, BufRead, Write};
fn main() -> io::Result<()> {
// Write a file
let mut file = File::create("hello.txt")?;
writeln!(file, "Hello, World!")?;
// Read entire file
let content = fs::read_to_string("hello.txt")?;
println!("{}", content);
// Read line by line
let file = File::open("hello.txt")?;
for line in io::BufReader::new(file).lines() {
println!("{}", line?);
}
// Remove file
fs::remove_file("hello.txt")?;
Ok(())
}Child Processes
use std::process::{Command, Stdio};
fn main() {
// Run a command and collect output
let output = Command::new("echo")
.arg("hello")
.output()
.expect("failed to execute");
println!("{}", String::from_utf8_lossy(&output.stdout));
// Pipe output to next process
let mut child = Command::new("ls")
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.wait().unwrap();
}Filesystem Operations
use std::fs;
use std::path::Path;
fn main() -> std::io::Result<()> {
fs::create_dir_all("a/b/c")?;
if Path::new("a").exists() {
println!("exists");
}
for entry in fs::read_dir(".")? {
let entry = entry?;
println!("{:?}", entry.file_name());
}
fs::remove_dir_all("a")?;
Ok(())
}Notes
- Share mutable state across threads with
Arc<Mutex<T>>(not shown here — covered in the standard library docs). mpsc::sync_channel(n)creates a bounded channel with capacityn; send blocks when full.- Use
std::process::exit(code)to terminate the process immediately. Command::new("prog").stdin(Stdio::piped())enables piping data into child stdin.
Related
- 19-std-library-types.md
- 22-unsafe-operations.md
Testing
Rust has first-class support for three kinds of tests: unit tests (alongside code), integration tests (in tests/), and doc tests (in /// comments). Run all tests with cargo test.
Unit Tests
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_add_negative() {
assert_ne!(add(-1, 1), 99);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_panic() {
let _ = 1 / 0;
}
#[test]
#[ignore]
fn expensive_test() {
// run with: cargo test -- --ignored
}
// Tests can return Result
#[test]
fn test_with_result() -> Result<(), String> {
if add(2, 2) == 4 { Ok(()) } else { Err("math is broken".into()) }
}
}Assert Macros
| Macro | Description |
|---|---|
assert!(expr) | Panics if expr is false |
assert_eq!(a, b) | Panics if a != b |
assert_ne!(a, b) | Panics if a == b |
All accept an optional format message: assert_eq!(a, b, "got {}", a).
Integration Tests
Place in tests/ directory — each file is a separate crate that imports your library:
// tests/integration_test.rs
use my_lib::add;
#[test]
fn test_add_from_outside() {
assert_eq!(add(2, 3), 5);
}Run a specific integration test file: cargo test --test integration_test.
Doc Tests
Code blocks in /// doc comments are compiled and run as tests:
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// assert_eq!(my_lib::add(1, 2), 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }Run only doc tests: cargo test --doc.
Dev Dependencies
Test-only dependencies go in [dev-dependencies] in Cargo.toml:
[dev-dependencies]
pretty_assertions = "1"Notes
#[cfg(test)]ensures thetestsmodule is only compiled forcargo test, not in the final binary.- Run a specific test by name:
cargo test test_add. - Run tests in parallel by default; use
-- --test-threads=1for sequential execution. cargo test -- --nocaptureshowsprintln!output from passing tests.
Related
- 12-cargo.md
- 24-meta.md
- 13-attributes.md
Unsafe Operations
The unsafe keyword lets you bypass Rust's safety guarantees for operations the compiler cannot verify. Minimize unsafe code and carefully document invariants.
What Requires unsafe
1. Dereferencing raw pointers 2. Calling unsafe functions or methods 3. Accessing or modifying mutable static variables 4. Implementing unsafe traits
Raw Pointers
fn main() {
let raw: *const u32 = &10;
unsafe {
assert!(*raw == 10);
println!("{}", *raw);
}
}Raw pointer types: *const T (immutable) and *mut T (mutable). Creating raw pointers is safe; dereferencing them requires unsafe.
Calling Unsafe Functions
use std::slice;
fn main() {
let v = vec![1u32, 2, 3, 4];
let ptr = v.as_ptr();
let len = v.len();
unsafe {
// Caller must guarantee: ptr is valid, len is correct, alignment is right
let s: &[u32] = slice::from_raw_parts(ptr, len);
assert_eq!(v.as_slice(), s);
}
}Mutable Static Variables
static mut COUNTER: u32 = 0;
fn increment() {
unsafe { COUNTER += 1; }
}
fn main() {
increment();
unsafe { println!("{}", COUNTER); }
}FFI — Calling C Functions
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("{}", abs(-3)); // 3
}
}Expose Rust functions to C:
#[no_mangle]
pub extern "C" fn my_rust_fn(x: i32) -> i32 { x * 2 }Unsafe Traits
unsafe trait MyUnsafeTrait {
fn dangerous(&self);
}
struct Foo;
unsafe impl MyUnsafeTrait for Foo {
fn dangerous(&self) { println!("danger!"); }
}Notes
- Prefer safe abstractions: wrap
unsafeblocks in safe public APIs. - Undefined behavior in
unsafeblocks is still undefined — the compiler can miscompile it. - Use tools like
Miri(UB detector) andsanitizersto verify unsafe code. - Common safe wrappers:
std::slice::from_raw_parts→ use&v[..]; raw pointer arithmetic → use iterators.
Related
- 15-scoping-rules.md
- 20-std-misc.md
Compatibility
Rust evolves rapidly while striving for backward compatibility. Editions and raw identifiers are the main tools for managing compatibility across Rust versions.
Editions
Rust releases a new edition every three years (2015, 2018, 2021). An edition may introduce syntax-breaking changes, but Rust guarantees inter-edition compatibility within a project.
Set the edition in Cargo.toml:
[package]
name = "my_crate"
edition = "2021"Raw Identifiers
Raw identifiers (r#keyword) allow using Rust keywords as identifiers. This is useful when:
- Calling code from an older edition that used a now-reserved keyword as a name.
- Interfacing with languages (e.g., C) that have identifiers conflicting with Rust keywords.
// `match` is a keyword, but r#match is allowed as an identifier
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
// r# prefix at call site too
assert!(r#match("foo", "foobar"));
// Variables using keyword names
let r#type = "i32";
let r#fn = 42;
println!("{} {}", r#type, r#fn);
}Edition Migration
# Automatically migrate code to a newer edition
cargo fix --editionThe tool applies mechanical fixes; manual review is still recommended afterward.
Notes
- All crates in a workspace can use different editions independently.
- Keywords introduced in newer editions (e.g.,
async,awaitin 2018) become reserved; use raw identifiers to reference old code that used them as names. - Rust maintains a stability guarantee: code that compiled on stable Rust will continue to compile on future stable versions.
Related
- 12-cargo.md
Meta
Tooling for documentation and benchmarking: rustdoc generates HTML docs from source comments; Rust's built-in benchmark harness (nightly) measures performance.
Documentation with rustdoc
Doc Comments
Use /// for item-level docs and //! for module/crate-level docs. Markdown is fully supported.
//! Crate-level documentation (inner attribute style).
/// A human being.
pub struct Person {
/// The person's name.
pub name: String,
}
impl Person {
/// Creates a new [`Person`] with the given name.
///
/// # Examples
///
/// ```
/// use my_crate::Person;
/// let p = Person::new("Alice");
/// assert_eq!(p.name, "Alice");
/// ```
pub fn new(name: &str) -> Self {
Person { name: name.to_string() }
}
}Generating Docs
cargo doc # build docs into target/doc/
cargo doc --open # build and open in browser
cargo doc --no-deps # skip dependency docsDoc Attributes
#[doc(hidden)] // exclude from docs
pub fn internal() {}
#[doc(inline)] // inline re-exported item docs
pub use crate::detail::Foo;Hiding Doc Test Code
Use # prefix to hide setup lines while keeping them compiled:
/// ```
/// # fn setup() -> Vec<i32> { vec![1,2,3] }
/// # let v = setup();
/// assert_eq!(v.len(), 3);
/// ```
pub fn example() {}Benchmarking
The built-in #[bench] attribute requires nightly Rust and the test crate:
#![feature(test)]
extern crate test;
pub fn add(a: u64, b: u64) -> u64 { a + b }
#[cfg(test)]
mod benches {
use super::*;
use test::Bencher;
#[bench]
fn bench_add(b: &mut Bencher) {
b.iter(|| add(2, 3));
}
}Run benchmarks:
cargo +nightly benchFor stable Rust benchmarking, use the `criterion` crate.
Playground Integration
Embed interactive examples in docs using the Rust Playground URL:
https://play.rust-lang.org/?code=...rustdoc automatically links code blocks to the Playground when generating docs on docs.rs.
Notes
cargo testalso runs all doc tests — no separate step required.- Doc tests verify that examples in documentation stay correct as code evolves.
- Use
criterionfor statistically rigorous benchmarking on stable Rust. #[doc = "..."]is the attribute form of doc comments (equivalent to///).
Related
- 21-testing.md
- 12-cargo.md
- 13-attributes.md
by-example
| Name | Description | Path |
|---|---|---|
| Hello World | The entry point of a Rust program is fn main(). The… | 01-hello-world.md |
| Primitives | Rust's primitive types include scalar types (integers,… | 02-primitives.md |
| Custom Types | Rust defines custom data types with struct (named… | 03-custom-types.md |
| Variable Bindings | Values are bound to names with let. Rust is… | 04-variable-bindings.md |
| Types | Rust supports explicit casting with as, type… | 05-types.md |
| Conversion | Rust uses traits to handle type conversion between… | 06-conversion.md |
| Expressions | A Rust program is made up of statements and expressions.… | 07-expressions.md |
| Flow of Control | Rust provides if/else, loop, while, for,… | 08-flow-of-control.md |
| Functions | Functions are declared with fn. Arguments must be… | 09-functions.md |
| Modules | Modules (mod) organize code into logical units and… | 10-modules.md |
| Crates | A crate is the fundamental compilation unit in Rust.… | 11-crates.md |
| Cargo | Cargo is Rust's official package manager and build… | 12-cargo.md |
| Attributes | An attribute is metadata applied to a crate, module, or… | 13-attributes.md |
| Generics | Generics allow functions, structs, enums, and traits to… | 14-generics.md |
| Scoping Rules | Scopes determine when resources are freed (RAII), when… | 15-scoping-rules.md |
| Traits | A trait defines a set of methods for an unknown type… | 16-traits.md |
| macro_rules! | Macros are metaprogramming constructs that expand into… | 17-macros.md |
| Error Handling | Rust distinguishes recoverable errors (Result<T, E>)… | 18-error-handling.md |
| Std Library Types | The standard library provides essential types beyond… | 19-std-library-types.md |
| Std Misc | Additional standard library features for concurrency,… | 20-std-misc.md |
| Testing | Rust has first-class support for three kinds of tests:… | 21-testing.md |
| Unsafe Operations | The unsafe keyword lets you bypass Rust's safety… | 22-unsafe-operations.md |
| Compatibility | Rust evolves rapidly while striving for backward… | 23-compatibility.md |
| Meta | Tooling for documentation and benchmarking: rustdoc… | 24-meta.md |
Cargo Guide
A practical guide to using Cargo for everyday Rust development.
Why Cargo Exists
Without Cargo, building non-trivial Rust programs means invoking rustc manually with explicit flags, and managing transitive dependencies by hand. Cargo solves this by:
1. Introducing metadata files (Cargo.toml) to declare package info and dependencies 2. Fetching and building dependencies automatically from a registry 3. Invoking the compiler with correct parameters 4. Establishing conventions so any Cargo project can be built the same way
Creating a New Package
cargo new hello_world --bin # binary (executable)
cargo new my_lib --lib # libraryOptions:
--vcs none— skip git initialization (git is created by default)
Package Layout
Cargo follows these directory conventions:
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs # default library target
│ ├── main.rs # default binary target
│ └── bin/
│ └── extra.rs # additional binaries
├── examples/
│ └── demo.rs
├── tests/
│ └── integration.rs # integration tests
└── benches/
└── bench.rs| Location | Purpose |
|---|---|
src/main.rs | Default binary |
src/lib.rs | Default library |
src/bin/*.rs | Additional binaries |
examples/ | Example programs |
tests/ | Integration tests |
benches/ | Benchmarks |
Adding Dependencies
Edit Cargo.toml:
[dependencies]
regex = "0.1.41"
time = "0.1.12"Or use cargo add:
cargo add serde --features deriveAfter adding, cargo build fetches and compiles dependencies automatically.
Cargo.toml vs Cargo.lock
| Aspect | Cargo.toml | Cargo.lock |
|---|---|---|
| Written by | Developer | Cargo (auto-generated) |
| Purpose | Declare dependencies (ranges) | Lock exact versions used |
| Commit to VCS | Always | Yes for binaries; optional for libraries |
| Edit manually | Yes | No |
The lock file ensures reproducible builds. Update it with:
cargo update # update all dependencies
cargo update regex # update just "regex"Running Tests
cargo test # run all tests
cargo test foo # run tests matching "foo"Tests are discovered from:
src/files (unit tests and doc tests)tests/directory (integration tests)
Continuous Integration
Minimal CI configuration (GitHub Actions):
- uses: dtolnay/rust-toolchain@stable
- run: cargo testFor verifying newest dependencies:
cargo update && cargo testWorking on an Existing Package
git clone <url>
cd <project>
cargo build # Cargo.lock ensures reproducible buildCargo Home
Cargo stores downloaded data in $CARGO_HOME (default: ~/.cargo/):
| Path | Content |
|---|---|
~/.cargo/bin/ | Installed binaries |
~/.cargo/registry/ | Registry index and crate sources |
~/.cargo/git/ | Git-based dependency sources |
Optimizing Build Performance
- Use
cargo checkinstead ofcargo buildwhen only checking for errors - Enable the
sccachecompiler cache viaRUSTC_WRAPPER=sccache - Use
[profile.dev.package."*"]to optimize specific slow dependencies - Reduce
codegen-unitsin[profile.release]for smaller binaries at cost of build time
Related
- getting-started.md
- reference-manifest.md
- reference-specifying-dependencies.md
- reference-cargo-toml.md
- reference-profiles.md
Using Config Files
Persist user settings between invocations using the confy crate, which handles XDG/platform-appropriate config paths automatically.
Signature / Usage
# Cargo.toml
[dependencies]
confy = "0.5"
serde = { version = "1", features = ["derive"] }use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct MyConfig {
name: String,
comfy: bool,
foo: i64,
}
impl Default for MyConfig {
fn default() -> Self {
MyConfig { name: "default".into(), comfy: true, foo: 42 }
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cfg: MyConfig = confy::load("my_app", None)?;
println!("{:#?}", cfg);
Ok(())
}Notes
confy::load("app_name", None)reads from a platform-appropriate path (e.g.~/.config/my_app/default.tomlon Linux via XDG).- The config struct must implement
Default(used when no file exists),Serialize, andDeserialize. confystores configs as TOML by default; it creates the file with default values on first run.- For more complex needs (env vars, layered config, CLI override), consider the `config` crate.
Related
- parsing-arguments.md
- in-depth-human-communication.md