Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust Hero

Practical RustCompiler reasoningFast education

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:

  1. Who owns each value?
  2. What does each type or bound guarantee?
  3. Which states and operations does the API allow?

Learn how to use the chapters →

Reading

Practice loopRunnable examplesThree depth tiers

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

FoundationValues · inference · signaturesDraft

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:

  • Debug enables diagnostic formatting with {:?}.
  • Clone enables an explicit .clone().
  • Copy allows implicit bitwise copying instead of moves.
  • PartialEq enables == and !=.
  • Eq states that equality is fully reflexive.
  • Default provides 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 Debug formatting;
  • 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:

  1. Every value has an owner.
  2. A value has only one owner at a time.
  3. 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:

  1. Where was the value created?
  2. Which binding currently owns it?
  3. Did an assignment, function call, or closure move it?
  4. Which references are still used later?
  5. 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:

  • &T points to a valid, properly aligned T that remains alive for the reference’s lifetime;
  • &mut T provides exclusive access to that T while 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.

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 i32 normally needs no heap allocation;
  • an owned String has 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

EssentialOwned · borrowed · UTF-8Planned

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

EssentialVec · array · slicePlanned

This chapter will connect arrays, vectors, and slices; compare &Vec<T> with &[T]; and make capacity growth and invalidated references observable.

Models

EssentialProduct types · sum typesPlanned

This chapter will cover structs, enums, methods, destructuring, newtypes, and the first examples of making invalid states difficult to represent.

Outcomes

EssentialOption · Result · match · ?Planned

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:

  • Debug formats developer-facing output with {:?}.
  • Display formats user-facing output with {}.
  • Clone creates an explicit duplicate.
  • Copy permits implicit copying instead of moving.
  • PartialEq and Eq define equality.
  • Default constructs a default value.
  • Iterator produces a sequence of values.
  • From and Into convert between types.
  • AsRef and Borrow provide borrowed views.
  • Read and Write describe 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

EssentialIterator · closure · capturePlanned

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

EssentialElision · relationships · 'staticPlanned

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

ImportantHeap · shared ownership · lockingPlanned

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

ImportantMarker traits · thread safetyPlanned

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

Importantspawn · join · channelPlanned

This chapter will build a small worker with std::thread and channels, explain move closures, and contrast message passing with shared mutable state.

Async

ImportantFuture · task · suspensionPlanned

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

ImportantParameters · returns · flexibilityPlanned

This chapter will compare borrowed and owned parameters, flexible slice and trait inputs, returned references, and cases where cloning is the clearest contract.

Errors

ImportantError types · validation · contextPlanned

This chapter will distinguish library and application errors, preserve source errors, add useful context, and use parse-then-validate construction to protect invariants.

Testing

ImportantAssertions · edge cases · contractsPlanned

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

Essential reviewTrace · explain · designPlanned

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

RecognitionVocabulary · boundariesPlanned

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:

  1. What problem does it solve?
  2. Where will you encounter it?
  3. What safe, public-facing mental model is enough for now?
  4. What signal means it is time to study the internals?