Rust Hero
Rust gets easier when its features stop looking like separate rules. Ownership explains why iterator methods differ. Borrowing explains why APIs accept slices. Traits explain what generic signatures permit. Those same ideas reappear when values cross thread and task boundaries.
This book builds that connected model. It focuses first on the concepts that appear constantly in Rust code:
- ownership, moves, and borrowing;
- strings, vectors, and borrowed views;
- enums, error handling, traits, iterators, closures, and lifetimes; and
- the design choices those concepts make possible.
Concurrency and API design come next. Advanced internals are kept in a short recognition map: enough vocabulary to explain their purpose and know when they matter, without letting them crowd out the fundamentals.
own values and lend views
│
▼
model states and failures
│
▼
abstract over capabilities
│
▼
share work safely
│
▼
design APIs others can trust
The goal is not to memorize compiler messages or recite definitions. It is to look at a Rust program and answer three questions:
- Who owns each value?
- What does each type or bound guarantee?
- Which states and operations does the API allow?
Learn how to use the chapters →
Reading
The chapters are ordered by dependency. Their labels describe how deeply to study them:
- Essential: write it, debug it, and explain it without notes.
- Important: use it confidently and explain its main tradeoffs.
- Recognition: know why it exists and where it appears; defer the internals.
For every example, pause before reading the explanation. Predict whether it compiles. If it does, predict its output. If it does not, point to the precise ownership, type, or lifetime relationship that fails.
Use this four-step loop:
predict → compile → explain → change one thing
The final step matters. Change an owned parameter to a borrowed one, replace an iterator method, move a value into a closure, or alter a match arm. Small experiments turn vocabulary into a working model.
Types
Every value in Rust has one concrete type. The compiler can often infer that type, but inference does not make the program dynamically typed. It means the compiler found the single type that satisfies every use of the value.
fn double(number: i32) -> i32 {
number * 2
}
fn main() {
let score = 21;
let result = double(score);
println!("{result}");
}
The annotation on double gives the compiler enough context to infer that
both score and result are i32. The function signature is also a contract
for a reader:
double: i32 → i32
It accepts one signed 32-bit integer and produces another. Passing text or a floating-point value violates that contract before the program can run.
Start with values
Rust’s common built-in values include:
#![allow(unused)]
fn main() {
let count: u32 = 3;
let temperature: f64 = 21.5;
let enabled: bool = true;
let initial: char = 'R';
}
The annotation after each binding name states its type explicitly. Usually you can omit an annotation when the initializer or later use makes the type clear:
#![allow(unused)]
fn main() {
let enabled = true; // bool
let greeting = "hello"; // &str
}
Annotations are most useful when they communicate intent or resolve a real ambiguity. Adding them everywhere can obscure the facts that matter.
Combine types
Tuples group a fixed number of values that may have different types:
#![allow(unused)]
fn main() {
let user: (&str, u32, bool) = ("Ferris", 8, true);
let (name, level, active) = user;
}
Arrays group a fixed number of values of the same type:
#![allow(unused)]
fn main() {
let scores: [i32; 3] = [10, 20, 30];
}
These type spellings already reveal useful constraints. (&str, u32, bool)
has three positions with distinct meanings. [i32; 3] has exactly three
elements, all i32. Later chapters replace positional tuples with structs when
the fields deserve names and replace fixed arrays with vectors when the length
must change.
A mismatch is information
fn main() {
let mut answer = 42;
answer = "forty-two";
}
mut allows the value stored in answer to change. It does not allow the
binding’s type to change. Once answer is inferred as an integer, every later
assignment must still produce that integer type.
When Rust reports mismatched types, read the diagnostic as two facts:
expected: the type required by this position
found: the type the expression actually produced
The useful debugging question is not merely “How do I silence this error?” It is “Which side expresses the contract I intended?” Sometimes the expression is wrong; sometimes the function signature is too restrictive.
Types carry different kinds of promises
As the book progresses, type syntax will answer increasingly rich questions:
String owns growable text
&str borrows a view of text
Option<T> contains either a T or no value
Result<T, E> contains either a T or an error E
Vec<T> owns a growable sequence of T values
&[T] borrows a view of a sequence of T values
Rust code becomes much easier to read when these are treated as descriptions of program states rather than punctuation to memorize.
Macros
Rust macros generate code. You can recognize many of them by the ! after
their name:
#![allow(unused)]
fn main() {
println!("hello");
vec![1, 2, 3];
panic!("something went wrong");
}
A macro can accept syntax that an ordinary function cannot. For example,
println! accepts a format string followed by any number of values. The macro
checks the formatting arguments and expands into the code needed to print them.
This chapter uses macros without examining how to write them.
println! prints formatted output
println! writes a line to standard output:
fn main() {
let name = "Ferris";
let level = 3;
println!("Hello, {name}");
println!("level: {level}");
println!("next level: {}", level + 1);
}
The output is:
Hello, Ferris
level: 3
next level: 4
Named variables can appear directly inside braces. An empty {} uses the next
argument after the format string.
The value must implement the formatting trait requested by the placeholder:
#![allow(unused)]
fn main() {
let value = 42;
println!("{}", value); // Display
println!("{:?}", value); // Debug
println!("{value:#?}"); // pretty-printed Debug
}
Display is intended for user-facing output. Debug is intended for
developers and diagnostics. Many standard types implement both, but your own
types do not receive either implementation automatically.
#[derive(...)] generates trait implementations
The derive attribute asks Rust to generate implementations of selected traits
for a struct or enum:
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
level: u32,
}
fn main() {
let user = User {
name: String::from("Ferris"),
level: 3,
};
let copy = user.clone();
println!("{user:?}");
println!("same value: {}", user == copy);
}
Each name inside derive adds a capability:
Debugenables diagnostic formatting with{:?}.Cloneenables an explicit.clone().Copyallows implicit bitwise copying instead of moves.PartialEqenables==and!=.Eqstates that equality is fully reflexive.Defaultprovides a default value when every field supports it.
Deriving a trait works only when all relevant fields implement that trait. A
struct containing a field that is not Clone, for example, cannot derive
Clone.
derive is a macro even though it does not use trailing ! syntax. It is
invoked through an attribute placed above the item it affects.
dbg! shows an expression and its value
dbg! is a quick way to inspect a value while developing:
fn main() {
let width = 4;
let height = 6;
let area = dbg!(width * height);
println!("area: {area}");
}
The exact location depends on the source file, but the diagnostic resembles:
[src/main.rs:4:16] width * height = 24
area: 24
dbg! differs from println! in several useful ways:
- it prints the source expression, file, and line number;
- it uses
Debugformatting; - it writes to standard error rather than standard output;
- it returns the value it receives.
Because it returns the value, dbg! can wrap an expression without otherwise
changing the surrounding code:
#![allow(unused)]
fn main() {
let subtotal = 20;
let tax = 2;
let total = dbg!(subtotal + tax);
}
dbg! takes ownership unless you borrow
Macros still follow ordinary ownership rules:
fn main() {
let name = String::from("Ferris");
dbg!(&name);
println!("{name}");
}
Passing &name lets dbg! inspect a reference. Passing name directly would
move the String into the macro’s expression, so the binding could not be used
afterward.
This is not special behavior built into dbg!. It follows from the same move
and borrowing rules as ordinary Rust code.
When to use each tool
Use println! when output is part of the program’s behavior or when you want
controlled formatting. Use dbg! for quick, temporary inspection during
development. Use #[derive(...)] to generate standard trait behavior instead
of writing repetitive implementations by hand.
In larger applications, structured logging usually replaces temporary
println! and dbg! calls. These two macros remain ideal for small programs
and quick experiments.
Ownership
Ownership is how Rust manages memory without a garbage collector. The compiler tracks which binding is responsible for each value and rejects code that could use invalid memory.
The core rules are:
- Every value has an owner.
- A value has only one owner at a time.
- When the owner leaves scope, the value is dropped.
References let code use a value without taking ownership of it. Rust calls creating a reference borrowing.
Values are dropped with their owners
fn main() {
{
let message = String::from("hello");
println!("{message}");
} // message leaves scope; its String is dropped here
}
String owns a heap allocation. When message leaves scope, Rust runs the
value’s Drop implementation and releases that allocation. No explicit
free call and no garbage collector are required.
Scope is usually marked by braces. Function parameters, local variables, and temporary values all have scopes that determine when they are dropped.
Assignment usually moves ownership
fn main() {
let first = String::from("hello");
let second = first;
println!("{second}");
}
After let second = first, second owns the String. Rust calls this a
move. The bytes that describe the String—a pointer, length, and
capacity—are copied to second, but the heap allocation itself is not copied.
Rust invalidates first so that only one binding can release the allocation:
fn main() {
let first = String::from("hello");
let second = first;
println!("{first}");
println!("{second}");
}
The compiler reports that first was used after it was moved. This prevents a
double free: if both bindings still considered themselves owners, both would
try to release the same allocation.
Passing a value can move it
Function parameters are bindings, so passing an owned value transfers
ownership unless its type is Copy:
fn print_length(text: String) {
println!("{}", text.len());
}
fn main() {
let name = String::from("Ferris");
print_length(name);
println!("Hello, {name}");
}
name moves into the parameter text. At the end of print_length, text
leaves scope and the String is dropped. The caller no longer owns it.
A function can return ownership:
fn inspect(text: String) -> String {
println!("{} bytes", text.len());
text
}
fn main() {
let name = String::from("Ferris");
let name = inspect(name);
println!("Hello, {name}");
}
This works, but returning every value merely to preserve it is awkward. Borrowing is the usual solution.
Borrow with &T
A shared reference, written &T, provides temporary read access to a T:
fn print_length(text: &String) {
println!("{}", text.len());
}
fn main() {
let name = String::from("Ferris");
print_length(&name);
println!("Hello, {name}");
}
&name borrows the value instead of moving it. The parameter text owns only
the reference, not the String. When print_length returns, the borrow ends
and the caller still owns name.
The type shows the ownership behavior:
String owned string value
&String shared borrow of a String
A shared reference cannot mutate the borrowed value:
#![allow(unused)]
fn main() {
fn add_mark(text: &String) {
text.push('!');
}
}
That restriction makes it safe to have multiple shared references at the same time:
fn main() {
let name = String::from("Ferris");
let first = &name;
let second = &name;
println!("{first} and {second}");
}
Both references only read name, so neither can interfere with the other.
The next chapter will improve &String parameters to &str, which accepts a
wider range of string data. For now, the important distinction is between the
owned value and a reference to it.
Mutate through &mut T
The owner must first allow mutation with mut. A mutable reference, written
&mut T, then grants temporary read and write access:
fn add_mark(text: &mut String) {
text.push('!');
}
fn main() {
let mut message = String::from("hello");
add_mark(&mut message);
println!("{message}");
}
While a mutable borrow is in use, no other borrow of that value may be used:
fn main() {
let mut message = String::from("hello");
let borrowed = &message;
message.push('!');
println!("{borrowed}");
}
borrowed points into message and is used by the final println!. Mutating
message before that use conflicts with the active shared borrow.
The practical borrowing rule is:
any number of shared references (&T)
or
exactly one mutable reference (&mut T)
This prevents data from changing while other code is reading it and prevents two writers from changing it at the same time.
A borrow lasts until its final use
A borrow does not always last until the closing brace. It usually ends after the reference’s final use:
fn main() {
let mut message = String::from("hello");
let borrowed = &message;
println!("before: {borrowed}");
message.push('!');
println!("after: {message}");
}
The shared borrow ends after the first println!, so the later mutation is
allowed. This is called a non-lexical lifetime: the compiler follows actual
uses rather than blindly extending every borrow to the end of its block.
Some types are copied instead of moved
Simple, fixed-size values commonly implement the Copy trait:
fn main() {
let first = 10;
let second = first;
println!("{first} {second}");
}
Assignment copies the i32, so both bindings remain usable. Types such as
integers, floating-point numbers, booleans, characters, and shared references
are Copy.
Types that manage resources, such as String, Vec<T>, and files, are not
Copy. Silently duplicating them would make ownership of the underlying
resource ambiguous or require hidden expensive work.
Copy is implicit. Clone is explicit:
fn main() {
let first = String::from("hello");
let second = first.clone();
println!("{first} {second}");
}
clone creates a second independently owned String and copies the heap data.
Use it when two owners are genuinely needed, not simply to avoid reasoning
about a move.
Moving one field can partially move a value
Ownership is tracked precisely enough to move a field out of a struct:
struct User {
name: String,
level: u32,
}
fn main() {
let user = User {
name: String::from("Ferris"),
level: 3,
};
let name = user.name;
println!("{name}");
println!("{}", user.level);
}
The String moves out of user.name, while the Copy field user.level
remains usable. The complete user value can no longer be used because it is
missing a field.
Borrow the field instead when the complete struct must remain intact:
struct User {
name: String,
level: u32,
}
fn main() {
let user = User {
name: String::from("Ferris"),
level: 3,
};
let name = &user.name;
println!("{name}");
println!("{}", user.level);
}
Read ownership from function signatures
These signatures describe three different contracts:
fn store(value: String) // takes ownership
fn inspect(value: &String) // borrows for reading
fn modify(value: &mut String) // borrows for mutation
Use an owned parameter when the function needs to store, consume, or transfer the value. Use a shared reference when it only needs to read. Use a mutable reference when it must modify the caller’s existing value.
When a move or borrow error appears, trace the value through the program:
- Where was the value created?
- Which binding currently owns it?
- Did an assignment, function call, or closure move it?
- Which references are still used later?
- Does the operation need ownership, shared access, or mutable access?
Those questions usually reveal whether the correct fix is to move, borrow, shorten a borrow, or create a deliberate clone.
Memory
Ownership describes who is responsible for a value. Stack and heap describe where a program commonly stores the value’s data. Pointers and references describe how code reaches data stored somewhere else.
These ideas are related, but they are not interchangeable:
- a value can contain both stack data and heap data;
- moving a value does not necessarily move its heap allocation;
- a reference points to a value but does not own it;
- using the heap does not automatically make a type shared or long-lived.
Function calls create stack frames
Each function call gets a stack frame containing information needed for that call. Local variables whose size is known at compile time can be stored directly in the frame.
fn add(left: i32, right: i32) -> i32 {
let sum = left + right;
sum
}
fn main() {
let answer = add(20, 22);
println!("{answer}");
}
A simplified picture while add is running looks like this:
stack
┌──────────────────────┐
│ add │
│ left: 20 │
│ right: 22 │
│ sum: 42 │
├──────────────────────┤
│ main │
│ answer: not set yet │
└──────────────────────┘
When add returns, its frame is removed as a unit. This is fast because the
program only needs to adjust the current end of the stack.
The diagram is a useful model, not a promise about exact machine layout. The optimizer may keep values in registers, remove variables, or reorganize code.
The heap stores dynamically sized allocations
The heap supports allocations whose size or lifetime does not fit neatly into one function’s fixed stack frame. The program asks an allocator for a region and later returns that region.
String is a common example:
fn main() {
let message = String::from("hello");
println!("{message}");
}
The String value is a small, fixed-size handle. Conceptually, it contains a
pointer, a length, and a capacity. The UTF-8 bytes are stored in a heap
allocation:
stack heap
┌──────────────────────┐ ┌─────────────┐
│ message │ │ h e l l o │
│ pointer ──────────────┼─────────▶│ │
│ length: 5 │ └─────────────┘
│ capacity: 5 │
└──────────────────────┘
When message is dropped, its destructor uses the pointer and capacity to
release the heap allocation.
Vec<T> has the same broad shape: a fixed-size handle points to a heap region
containing its elements.
A move usually moves the handle
Moving a String copies its pointer, length, and capacity into a new binding.
It does not copy each byte in the heap allocation:
fn main() {
let first = String::from("hello");
let second = first;
println!("{second}");
}
After the move:
stack heap
┌──────────────────────┐ ┌─────────────┐
│ first: unusable │ │ h e l l o │
├──────────────────────┤ │ │
│ second │ └─────────────┘
│ pointer ──────────────┼─────────▶
│ length: 5 │
│ capacity: 5 │
└──────────────────────┘
Only second remains responsible for the allocation. This is why moves are
often cheap even for values that manage large amounts of heap data.
By contrast, cloning the String creates a second heap allocation and copies
the bytes:
#![allow(unused)]
fn main() {
let first = String::from("hello");
let second = first.clone();
}
Now each String owns and eventually frees its own allocation.
A reference is a non-owning pointer
&T is a shared reference to a T. It contains the location of a value but
does not own that value:
fn main() {
let number = 42;
let reference = &number;
println!("number: {number}");
println!("through reference: {}", *reference);
}
&number creates the reference. *reference follows, or dereferences, it
to access the i32.
stack
┌────────────────────────┐
│ number: 42 │◀────┐
├────────────────────────┤ │
│ reference: address ────┼─────┘
└────────────────────────┘
Rust often inserts dereferencing automatically for method calls and some
expressions. Explicit * is still useful for understanding what the reference
means.
A reference is valid only while its target remains valid. Lifetimes are the compiler’s way of checking that relationship:
#![allow(unused)]
fn main() {
fn invalid_reference() -> &String {
let message = String::from("hello");
&message
}
}
message would be dropped when the function returns, so returning a reference
to it would create a dangling pointer. Rust rejects the function.
Shared and mutable references carry guarantees
References are more than memory addresses. A valid Rust reference carries rules the compiler and optimizer can rely on:
&Tpoints to a valid, properly alignedTthat remains alive for the reference’s lifetime;&mut Tprovides exclusive access to thatTwhile the mutable reference is in use;- neither kind of reference may be null or dangling.
These guarantees are why references participate in the borrowing rules. They
also allow ordinary reference use to remain safe: dereferencing a valid Rust
reference does not require an unsafe block.
Some references contain extra metadata
A reference to a sized value, such as &i32, is typically one machine word: an
address.
A slice reference must also record how many elements are visible:
&[T] = data pointer + length
&str = data pointer + byte length
Such a reference is often called a fat pointer because it contains an address plus metadata.
fn main() {
let numbers = [10, 20, 30, 40];
let middle = &numbers[1..3];
println!("length: {}", middle.len());
println!("values: {middle:?}");
}
middle does not own or copy the values 20 and 30. It stores where that
region begins and that it contains two elements.
Trait-object references such as &dyn Display also contain metadata, but use a
table of methods instead of a length.
Print an address with {:p}
Pointer formatting can make a reference visible during an experiment:
fn main() {
let value = 42;
let reference = &value;
println!("address: {reference:p}");
}
The exact address changes between runs and is rarely meaningful by itself. It is useful for confirming that two references point to the same value or that a clone owns a different allocation.
Do not build program logic around printed addresses.
Raw pointers provide fewer guarantees
Rust also has raw pointer types:
*const T raw pointer for reading
*mut T raw pointer that may permit writing
Creating a raw pointer is safe:
fn main() {
let value = 42;
let pointer: *const i32 = &value;
println!("{pointer:p}");
}
Dereferencing one requires unsafe because the compiler does not prove that a
raw pointer is non-null, aligned, alive, or properly synchronized. Raw pointers
are mainly used at foreign-function boundaries and inside low-level data
structures. Ordinary Rust code should prefer references.
Owning pointers are values with ownership behavior
Box<T> is an owning pointer. It places a T on the heap and owns that
allocation:
fn main() {
let boxed = Box::new(42);
println!("{}", *boxed);
}
When boxed is dropped, the i32 and its heap allocation are dropped. Moving
the Box transfers that ownership. Borrowing the Box or its contents lends
temporary access without transferring it.
Other pointer-like types add different ownership rules. Rc<T> and Arc<T>
provide shared ownership; the later concurrency chapter covers Arc<T> in
detail.
Stack versus heap does not decide ownership
Avoid rules such as “owned values live on the heap” or “copied values live on the stack.” They do not hold:
- an owned
i32normally needs no heap allocation; - an owned
Stringhas a stack-sized handle and heap-allocated contents; - a
Box<i32>owns a heap allocation; - a reference can point to data on the stack, the heap, or static memory;
- whether a type moves or copies is determined by
Copy, not by its location.
Start with ownership: who must eventually clean up the value? Then consider representation: is the data stored inline, behind a pointer, or across both?
Strings
This chapter will compare String, &String, &str, and string literals;
show deref coercion at ordinary call sites; explain UTF-8 boundaries; and use
function signatures to make allocation choices visible.
Slices
This chapter will connect arrays, vectors, and slices; compare &Vec<T> with
&[T]; and make capacity growth and invalidated references observable.
Models
This chapter will cover structs, enums, methods, destructuring, newtypes, and the first examples of making invalid states difficult to represent.
Outcomes
This chapter will move from exhaustive match expressions to focused control
flow and combinators, then show how ? performs an early return while
preserving the function’s error contract.
Generics
Generics let one definition work with many types. A generic definition uses a
type parameter such as T instead of naming one concrete type.
fn identity<T>(value: T) -> T {
value
}
fn main() {
let number = identity(42);
let text = identity("hello");
println!("{number} {text}");
}
The <T> after the function name declares a type parameter. Within the
function, T stands for one concrete type selected for each call.
identity(42) uses T = i32
identity("hello") uses T = &str
The two calls use the same function definition, but T is consistent within
each call. If the input is an i32, the output is also an i32.
Type parameters express relationships
The useful part of a generic signature is often the relationship between its positions:
fn choose<T>(first: T, second: T, use_first: bool) -> T {
if use_first {
first
} else {
second
}
}
fn main() {
let selected = choose("left", "right", true);
println!("{selected}");
}
Both arguments and the return value use T. The function requires the two
arguments to have the same type and promises to return that type.
This call fails because the arguments have different types:
fn choose<T>(first: T, second: T, use_first: bool) -> T {
if use_first { first } else { second }
}
fn main() {
let selected = choose(10, "ten", true);
}
Rust cannot choose one T that is both an integer and &str.
Use more than one parameter when the types may differ:
fn pair<T, U>(first: T, second: U) -> (T, U) {
(first, second)
}
fn main() {
let value = pair("level", 3);
println!("{}: {}", value.0, value.1);
}
Here T is &str and U is i32.
Generic functions cannot do everything
The body of an unconstrained generic function knows almost nothing about T:
#![allow(unused)]
fn main() {
fn print_value<T>(value: T) {
println!("{value}");
}
}
Not every possible T implements display formatting, so Rust rejects the
definition. The function must state that requirement with a trait bound:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("{value}");
}
fn main() {
print_value(42);
print_value("hello");
}
Read T: Display as “any type T that implements Display.” The next chapter
covers traits and bounds in detail.
Generic structs
A struct can be generic over the types stored in its fields:
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 2, y: 5 };
let decimal = Point { x: 1.5, y: 3.0 };
println!("{integer:?}");
println!("{decimal:?}");
}
Point<i32> and Point<f64> are different concrete types produced from the
same definition. Because both fields use T, x and y must have the same
type in each value.
Use separate parameters when the fields may differ:
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let pair = Pair {
first: "height",
second: 180,
};
println!("{}: {}", pair.first, pair.second);
}
Generic methods
Place the generic declaration after impl when defining methods for every
Point<T>:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let point = Point { x: 2, y: 5 };
println!("{}", point.x());
}
The first <T> declares the parameter. The T in Point<T> uses it:
impl<T> Point<T>
▲ ▲
declare use
A method can introduce additional type parameters of its own:
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn replace_second<V>(self, value: V) -> Pair<T, V> {
Pair {
first: self.first,
second: value,
}
}
}
fn main() {
let pair = Pair {
first: "level",
second: 3,
};
let pair = pair.replace_second("three");
println!("{}: {}", pair.first, pair.second);
}
The method consumes Pair<T, U> and returns Pair<T, V>, preserving the first
field’s type while changing the second.
Methods can target one concrete version
Not every method must exist for every T:
struct Point<T> {
x: T,
y: T,
}
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let point = Point { x: 3.0, y: 4.0 };
println!("{}", point.distance_from_origin());
}
distance_from_origin is available on Point<f64>, where the required
floating-point operations are known. It is not available on Point<String>.
Trait bounds provide a more flexible way to make methods available whenever a type supports the required operations.
Generic enums
Standard-library enums use generics extensively:
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Option<T> represents either one value of type T or no value. Result<T, E>
represents either a success of type T or an error of type E.
The generic parameters let these enums describe many concrete types:
fn main() {
let port: Option<u16> = Some(8080);
let name: Option<&str> = None;
let parsed: Result<i32, _> = "42".parse();
println!("{port:?} {name:?} {parsed:?}");
}
Type inference usually fills in the arguments
Rust often infers generic arguments from function inputs or expected output:
fn main() {
let numbers: Vec<i32> = (1..=3).collect();
println!("{numbers:?}");
}
collect can build many collection types. The annotation Vec<i32> tells it
which one to produce.
You can also supply a generic argument explicitly with ::<>, informally
called the turbofish:
fn main() {
let numbers = (1..=3).collect::<Vec<i32>>();
println!("{numbers:?}");
}
Prefer inference when the surrounding code already makes the type clear. Use an annotation or turbofish when Rust needs help or when the type is useful to a reader.
Const generics parameterize values
Const generics use a compile-time value instead of a type. This function accepts a reference to an array of any length:
fn length<T, const N: usize>(_: &[T; N]) -> usize {
N
}
fn main() {
println!("{}", length(&[10, 20, 30]));
println!("{}", length(&['a', 'b']));
}
T is a type parameter. N is a usize value known at compile time. The array
length is part of the array’s type, so [i32; 3] and [i32; 4] are distinct
types.
Const generics are useful for fixed-size arrays and data structures whose size is part of their contract.
Generics have no inherent runtime type lookup
Rust normally compiles generic code through monomorphization. The compiler
generates concrete code for the versions the program uses, such as
identity::<i32> and identity::<&str>.
This provides static type checking and usually avoids runtime dispatch. The tradeoff is that many concrete versions can increase compile time and binary size.
Generics are most useful when the same operation is genuinely valid across multiple types. If only one type makes sense, a generic parameter adds complexity without adding flexibility.
Traits
A trait describes behavior that a type can provide. It is a collection of method signatures and, optionally, default method implementations.
#![allow(unused)]
fn main() {
trait Describe {
fn describe(&self) -> String;
}
}
This trait says that any type implementing Describe must provide a
describe method. The method borrows self and returns an owned String.
Traits let code depend on capabilities instead of one specific type.
Implement a trait for a type
Use an impl Trait for Type block:
trait Describe {
fn describe(&self) -> String;
}
struct User {
name: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("user: {}", self.name)
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
println!("{}", user.describe());
}
The trait declares the required interface. The implementation supplies the
behavior for User.
Another type can implement the same trait differently:
trait Describe {
fn describe(&self) -> String;
}
struct User {
name: String,
}
struct Server {
address: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("user: {}", self.name)
}
}
impl Describe for Server {
fn describe(&self) -> String {
format!("server: {}", self.address)
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
let server = Server {
address: String::from("127.0.0.1"),
};
println!("{}", user.describe());
println!("{}", server.describe());
}
The caller uses one method name while each type keeps its own implementation.
Default methods
A trait can provide a method body:
trait Describe {
fn name(&self) -> &str;
fn describe(&self) -> String {
format!("item: {}", self.name())
}
}
struct User {
name: String,
}
impl Describe for User {
fn name(&self) -> &str {
&self.name
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
println!("{}", user.describe());
}
User must implement name, but it receives describe automatically. An
implementation can override a default method when it needs different behavior.
Default methods are useful when most types should share an implementation and the required behavior can be expressed through other methods in the trait.
Trait bounds constrain generics
An unconstrained generic function knows almost nothing about its type parameter. A trait bound states which behavior the function needs:
trait Describe {
fn describe(&self) -> String;
}
fn print_description<T: Describe>(value: &T) {
println!("{}", value.describe());
}
struct User { name: String }
impl Describe for User {
fn describe(&self) -> String { self.name.clone() }
}
fn main() {
let user = User { name: String::from("Ferris") };
print_description(&user);
}
T: Describe means that the function accepts any T implementing Describe.
Inside the function, Rust allows methods promised by that trait.
Without the bound, the method call fails:
#![allow(unused)]
fn main() {
fn print_description<T>(value: &T) {
println!("{}", value.describe());
}
}
The compiler cannot assume that every possible T has a describe method.
where clauses improve longer bounds
Bounds can follow the type parameter:
use std::fmt::{Debug, Display};
fn show<T: Display + Debug>(value: T) {
println!("display: {value}");
println!("debug: {value:?}");
}
fn main() {
show(42);
}
The + means that T must implement both traits.
A where clause expresses the same requirement more clearly when a signature
has several parameters:
use std::fmt::{Debug, Display};
fn show_pair<T, U>(first: T, second: U)
where
T: Display + Debug,
U: Display,
{
println!("first: {first} ({first:?})");
println!("second: {second}");
}
fn main() {
show_pair(42, "hello");
}
Use whichever form is easier to read. They have the same meaning.
impl Trait in parameters
impl Trait is a shorter way to accept a value implementing a trait:
use std::fmt::Display;
fn show(value: impl Display) {
println!("{value}");
}
fn main() {
show(42);
show("hello");
}
For a single parameter, this is similar to:
use std::fmt::Display;
fn show<T: Display>(value: T) {
println!("{value}");
}
fn main() {}
Named type parameters are necessary when the signature must express a relationship. These two parameters must have the same concrete type:
use std::fmt::Display;
fn show_same<T: Display>(first: T, second: T) {
println!("{first} {second}");
}
fn main() {
show_same(10, 20);
}
By contrast, two separate impl Display parameters may have different
concrete types:
use std::fmt::Display;
fn show_different(first: impl Display, second: impl Display) {
println!("{first} {second}");
}
fn main() {
show_different(10, "twenty");
}
impl Trait in return position
A function can hide its concrete return type while promising a trait:
fn numbers() -> impl Iterator<Item = i32> {
1..=3
}
fn main() {
for number in numbers() {
println!("{number}");
}
}
The caller knows that the result is an iterator yielding i32 values. It does
not need to name the range’s concrete type.
The function must still return one concrete type on every path:
#![allow(unused)]
fn main() {
fn numbers(reverse: bool) -> impl Iterator<Item = i32> {
if reverse {
(1..=3).rev()
} else {
1..=3
}
}
}
The two branches return different iterator types. Both implement Iterator,
but return-position impl Trait does not mean “any implementing type at
runtime.” It hides one concrete type chosen by the function.
Trait objects choose implementations at runtime
A trait object, written dyn Trait, can refer to values of different concrete
types through one interface:
trait Describe {
fn describe(&self) -> String;
}
struct User;
struct Server;
impl Describe for User {
fn describe(&self) -> String {
String::from("user")
}
}
impl Describe for Server {
fn describe(&self) -> String {
String::from("server")
}
}
fn print_all(items: &[&dyn Describe]) {
for item in items {
println!("{}", item.describe());
}
}
fn main() {
let user = User;
let server = Server;
let items: [&dyn Describe; 2] = [&user, &server];
print_all(&items);
}
&dyn Describe contains a pointer to a value and metadata used to find that
value’s trait methods. The concrete implementation is selected at runtime.
This is called dynamic dispatch.
Compare the main forms:
T: Trait named generic type; static dispatch
impl Trait unnamed concrete type; usually static dispatch
dyn Trait concrete type chosen at runtime; dynamic dispatch
Generics are usually the simplest choice when a collection contains one concrete type. Trait objects are useful when one collection must contain different concrete types that share behavior.
Because dyn Trait does not have a compile-time size by itself, it appears
behind a pointer such as &dyn Trait or Box<dyn Trait>.
Associated types name outputs of a trait
A trait can declare a type that each implementation chooses:
trait Source {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
current: u32,
end: u32,
}
impl Source for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
fn main() {
let mut counter = Counter { current: 0, end: 2 };
println!("{:?}", counter.next());
println!("{:?}", counter.next());
println!("{:?}", counter.next());
}
Item is an associated type. The Counter implementation chooses u32, so
Counter::next returns Option<u32>.
The standard Iterator trait uses this pattern. Each iterator implementation
has one associated Item type.
Supertraits require another trait
A trait can require implementations to provide another trait first:
use std::fmt::Display;
trait Labeled: Display {
fn label(&self) -> String {
format!("value: {self}")
}
}
impl Labeled for i32 {}
fn main() {
println!("{}", 42.label());
}
Labeled: Display means every Labeled type must also implement Display.
That allows the default label method to format self.
Traits follow coherence rules
Rust prevents conflicting trait implementations. In general, you may implement a trait when your crate defines the trait or defines the target type.
This is allowed because the type is local:
use std::fmt;
struct UserId(u64);
impl fmt::Display for UserId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "user-{}", self.0)
}
}
fn main() {
println!("{}", UserId(42));
}
Implementing an external trait for an external type is not allowed:
#![allow(unused)]
fn main() {
use std::fmt;
impl fmt::Display for Vec<i32> {
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}
}
Both Display and Vec belong to the standard library. If every crate could
add that implementation, two dependencies could make incompatible choices.
The restriction keeps trait resolution consistent.
A small wrapper type, often called a newtype, gives your crate a local type that can implement the trait.
Common standard-library traits
These traits appear throughout ordinary Rust code:
Debugformats developer-facing output with{:?}.Displayformats user-facing output with{}.Clonecreates an explicit duplicate.Copypermits implicit copying instead of moving.PartialEqandEqdefine equality.Defaultconstructs a default value.Iteratorproduces a sequence of values.FromandIntoconvert between types.AsRefandBorrowprovide borrowed views.ReadandWritedescribe byte-oriented I/O.
Derive standard traits when their generated behavior matches the meaning of your type. Write an implementation when the behavior needs a deliberate choice.
When reading a bound, translate it into a capability. T: Clone + Debug means
the function may duplicate T explicitly and format it for diagnostics. The
bound is not decoration; it explains what the implementation is allowed to do.
Iterators
This chapter will center iter, iter_mut, and into_iter; distinguish lazy
adapters from consumers; and derive Fn, FnMut, and FnOnce from how a
closure captures values.
Lifetimes
This chapter will begin with elision, add annotations only when a relationship
is ambiguous, place references in structs, and distinguish a 'static value
from a 'static bound.
Smart Pointers
This chapter will build from Box<T> to Arc<T> to Arc<Mutex<T>>, treating
each layer as the answer to a distinct requirement rather than a memorized
concurrency recipe.
Send and Sync
This chapter will explain Send and Sync compositionally and use compiler
errors to show how one field changes the contract of an entire type.
Threads
This chapter will build a small worker with std::thread and channels, explain
move closures, and contrast message passing with shared mutable state.
Async
This chapter will distinguish futures, tasks, threads, and runtimes; make lazy
execution observable; and introduce spawning, cancellation, and Send without
diving into manual polling or pinning internals.
APIs
This chapter will compare borrowed and owned parameters, flexible slice and trait inputs, returned references, and cases where cloning is the clearest contract.
Errors
This chapter will distinguish library and application errors, preserve source errors, add useful context, and use parse-then-validate construction to protect invariants.
Testing
This chapter will cover unit and integration test placement, success and error paths, edge cases, and the role of runnable examples in keeping the book honest.
Integration
This chapter will combine the essential concepts in a compact program and turn the walkthrough into a reusable checklist for reading and writing Rust.
Advanced
This appendix will give a concise orientation to Pin and Unpin, atomic
memory ordering, manual Future implementations, unsafe Rust, lifetime
variance, and complex macros.
Each topic will answer four questions:
- What problem does it solve?
- Where will you encounter it?
- What safe, public-facing mental model is enough for now?
- What signal means it is time to study the internals?