Introduction
Matt Weisfeld opens The Object-Oriented Thought Process with an observation that has aged better than most of the technology it was written about: a programmer can learn the syntax of an object-oriented language over a weekend and then go on writing procedural code in it for years. The compiler never objects. The classes are there, the methods are there, and underneath them sits the same program the same person would have written in C, with the data in one place and the logic in another. What was missing was never syntax. It was a way of thinking about where responsibility lives.
Rust sets the same trap and then springs it immediately. The syntax is learnable
in a weekend, and a programmer who arrives with the habits of Java, Python, C# or
Go will write something that looks reasonable and will be told no. Not once, but
persistently, in long messages about moved values and borrowed content and
lifetimes that do not live long enough. The usual reaction at that point is to
treat the compiler as an obstacle and go looking for the incantation that will
make it stop talking. There is always one. clone() will silence most of these
errors, Rc<RefCell<T>> will silence the rest, and a program built out of those
two moves will compile, run, and teach its author nothing.
This book is about the thought process that makes those incantations unnecessary. The premise is that nearly every complaint the Rust compiler makes is a design question wearing an error message. Who owns this value. Who is allowed to change it, and who is only allowed to look. How long does this thing need to be valid, and what guarantees that. What states can this type actually be in, and which of those states are ones you never want to handle. Those questions have answers in every program ever written, in every language. Most languages let you leave the answers implicit, in a comment, in a convention, or in the head of whoever wrote the module three years ago. Rust asks you to write them down in the program, and then it holds you to them.
The payoff for answering them is not only that the program compiles. It is that the design gets better in ways that have nothing to do with Rust. A programmer who has spent a year making ownership explicit writes clearer Go, because the question of who is responsible for a thing does not stop being a real question in a language that declines to ask it.
The intended audience
You can already program. You have written and shipped something in some language, you can read a function and see what it does, and terms like compilation, heap, stack, reference and type are not new to you. Where you are coming from matters less than you might expect, but it does matter, and the book assumes you are coming from somewhere: a class-based object-oriented language, most likely, or a garbage-collected language with structs and interfaces, or C and C++ if you took the low road. Each chapter names the habit it is asking you to put down and where that habit came from.
If Rust is your first language, this book will still work, but it is aimed over your shoulder at the reader who has something to unlearn. The Rust Programming Language, the free official book, is the better starting point, and this one will be waiting afterward.
What you do not need is prior exposure to Rust. Syntax is introduced as it becomes necessary, and never for its own sake.
This book’s scope
This is a book about thinking, so it is short on syntax and long on argument. The reference material exists already and is excellent: the standard library documentation, the Rust Programming Language, the Rust API Guidelines. None of them is trying to do what this book is trying to do, which is to describe the model a Rust programmer holds in their head and to show how it produces particular designs.
The consequence is that the book is incomplete on purpose. Macros appear only
where they change how you design something. Async appears in one chapter, as an
extension of the ownership model rather than as a subject in itself. unsafe
appears as the place where you take responsibility back from the compiler, not as
a tutorial. Cargo, formatting, tooling and the ecosystem are barely present. You
will need other sources for all of that, and you will find them easily.
The chapters run in the order the ideas depend on each other. Ownership before borrowing, borrowing before lifetimes, all three before anything about types and traits, because the reason Rust’s type system looks the way it does is that it was built on top of that model rather than beside it. Reading out of order is possible but the early chapters are the ones the rest lean on.
This book’s conventions
Code appears in fenced blocks, and nearly all of it compiles. Examples are
written against the standard library alone unless a crate is named, and they are
checked by mdbook test as part of building the book, so an example that claims
to work does work.
Some examples are meant to fail. Teaching the borrow checker without showing it rejecting something would be teaching the rules without the enforcement, so rejected programs appear throughout, marked as rejected, with the compiler’s actual complaint underneath. Those examples are checked too: the build verifies that they still fail, which means the book cannot quietly drift into claiming an error that a later compiler accepts.
Two kinds of aside interrupt the text. A note adds something useful that does not belong in the argument.
Note
Notes look like this. Skipping them costs you nothing structural.
A comparison names the habit under discussion and where you learned it.
Coming from Java, C# or Python
Asides like this one translate. They are not there to tell you that your old language was wrong; they are there because a habit is much easier to put down once you can see it.
Each chapter ends with a conclusion that states what changed in your model, and with a short set of questions to think through. The questions are not exercises in the homework sense and mostly do not have code answers. They are there because a thought process is practiced rather than read.
Introduction to Rust’s Core Concepts
Every language makes a bargain with you about who is responsible for the things your program creates. C’s bargain is that you are responsible for all of it, and the language will not check your work. Java’s bargain is that a garbage collector is responsible for memory and you are responsible for everything else, which turns out to be files, sockets, locks, transactions and the order in which anything happens. Rust’s bargain is different in a way that takes a while to feel natural: responsibility is written into the program, in the types, and the compiler verifies it before the program runs.
This chapter is a tour of what that means. It introduces the ideas the rest of the book develops one at a time, in the order they depend on each other, and it deliberately moves fast. Nothing here is the final word on anything. The point is to get the shape of the model in front of you, so that when a later chapter spends thirty pages on borrowing you already know why borrowing is a thing that needed inventing.
Values and places
Start with a distinction that most languages let you ignore. A place is a
location that can hold something: a local variable, a field of a struct, a slot
in an array. A value is the thing held. In a language where every object lives
on the heap and every variable is a reference to it, the two collapse into each
other and you stop noticing the difference. A Java String s is a place holding
a reference; the value is somewhere else, and how it got there and when it leaves
are not your business.
Rust keeps them apart. When you write
#![allow(unused)]
fn main() {
let greeting = String::from("hello");
}
the variable greeting is a place, and the value in it is a String. That
String is a small struct of three machine words — a pointer to a buffer, a
length, and a capacity — and those three words sit directly in greeting’s
stack slot. The text bytes live in a heap buffer that the String owns. There is
no second layer of indirection, no header, no object identity. A String is a
value that happens to manage some heap memory, and if you put it in a struct, the
three words go inside that struct.
This sounds like an implementation detail and it is not. It is the reason the rest of the model works. Because a value can live directly inside a place, the question of what happens when the place goes away has a real answer, and that answer is the first core concept.
Ownership
Every value in Rust has exactly one owner, and when the owner goes out of scope, the value is destroyed.
One sentence, and most of the rest of the language follows from taking it seriously. Destruction happens at a point in the program you can read off the page, because scopes are lexical and the compiler knows where they end. No collector runs later. No free is written by hand.
You can watch it happen. A type can implement the Drop trait to run code when
it is destroyed:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let _outer = Noisy("outer");
{
let _inner = Noisy("inner");
println!("inside the block");
}
println!("after the block");
}
The program prints inside the block, then dropping inner, then after the block, then dropping outer. The inner value dies at the closing brace, because
that is where its owner’s scope ends. The outer one dies at the end of main,
after everything else, because owners are dropped in reverse order of
declaration.
Memory is the least interesting thing this schedules. A file handle closes when
the File that owns it is dropped. A lock releases when the guard that owns it
is dropped. A database transaction rolls back when the transaction object is
dropped without having been committed. All of these are the same mechanism, and
none of them needs a finally block, a defer, a with, or a using, because
the cleanup is attached to the value rather than to the block of code that
happened to use it.
Coming from a garbage-collected language
The habit to notice is that you have been thinking of memory as free and everything else as needing ceremony. You close files in
finallyblocks and you have been burned at least once by a connection pool that leaked because some path out of a method skipped the close. Rust inverts this: the ceremony disappears and the schedule becomes something you design. The cost is that you have to decide who owns things, and you have to decide it up front.
Moves
If a value has one owner, then giving it to someone else means giving up being the owner. This is a move, and it is what assignment does:
fn main() {
let a = String::from("hello");
let b = a;
println!("{a}");
}
That program does not compile. The compiler says borrow of moved value: a, and
points out that the String was moved into b on the previous line.
The rule that produces this is not arbitrary strictness. Follow the alternative
through. If both a and b owned the same String, then at the end of the
block both would be dropped, both would free the same heap buffer, and the
program would corrupt its allocator. Languages without ownership avoid that
outcome by never freeing on scope exit — a collector decides later, or nobody
does. Rust avoids it by making the assignment transfer responsibility rather than
duplicate it.
Passing a value to a function moves it for the same reason, and so does returning one. A function signature is therefore a statement about responsibility. When you write
#![allow(unused)]
fn main() {
fn consume(text: String) -> usize {
text.len()
}
}
you are saying that consume takes over the caller’s String and the caller
will not see it again. That is sometimes what you want, and usually it is not,
which brings us to the next idea.
Borrowing
Most of the time a function needs to look at a value, or change it, without
taking responsibility for destroying it. That is a borrow, written with &:
fn length(text: &String) -> usize {
text.len()
}
fn shout(text: &mut String) {
text.make_ascii_uppercase();
}
fn main() {
let mut message = String::from("hello");
println!("{}", length(&message));
shout(&mut message);
println!("{message}");
}
&message is a shared reference: it grants the right to read, and the owner
keeps ownership. &mut message is an exclusive reference: it grants the right to
modify, and while it exists nobody else may touch the value at all, not even to
read it.
The rule governing references is short enough to memorize and deep enough to spend a career on. At any given moment, a value may have any number of shared references or exactly one exclusive reference, and never both. Chapter 3 is about why that single sentence eliminates an entire family of bugs — iterator invalidation, use-after-free, and data races turn out to be the same bug wearing different clothes — and about how to design so that you are not fighting it.
Note
Read
&mutas “exclusive” rather than “mutable”. The important half of the guarantee is not that you may write through it; it is that nobody else can do anything through anything else while you hold it. The name is historical and it misleads nearly every newcomer at least once.
Mutability is a property of the binding, not the data
In an object-oriented language, mutability is a property of an object. Anyone
holding a reference to a mutable object may change it, and the number of people
holding such a reference is not knowable. That is why defensive copying exists,
why immutable classes are recommended, and why a Collections.unmodifiableList
wrapper has to exist as a runtime trick.
In Rust the question is asked one level up. A value does not have a mutable setting; a path to a value does. The owner declares whether it may change the thing it owns:
#![allow(unused)]
fn main() {
let fixed = String::from("hello");
let mut changeable = String::from("hello");
}
and a reference declares whether it carries the right to change:
#![allow(unused)]
fn main() {
let mut changeable = String::from("hello");
let look: &String = &changeable;
println!("{look}");
let change: &mut String = &mut changeable;
change.push('!');
}
The consequence is that shared data is read-only by construction. You never have to wonder who else might write to something you are reading, because if anyone could, you would not have been allowed to read it. Defensive copying stops being a practice.
The type carries what must be true
Rust’s second big idea is that a type is the right place to put facts you do not want to check at runtime, and its enums are strong enough to make that practical. An enum is not a list of named integers. It is a choice between alternatives, each of which may carry its own data:
#![allow(unused)]
fn main() {
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
}
A Shape is one of those three and there is no fourth possibility, no null, and
no subclass someone adds in another crate. The match must cover every variant
or the program does not compile, so adding a Square variant later turns into a
list of compile errors naming every place that needs to think about squares. That
is the opposite of the usual experience, where adding a case means hunting for
the switches that need updating and finding the one you missed in production.
Two enums in the standard library do most of the work in practice. Option<T> is
either Some(value) or None, and it is how Rust says “there might not be one”
without having a null. Result<T, E> is either Ok(value) or Err(error), and
it is how a function that can fail says so in its return type rather than in its
documentation. Neither can be ignored by accident: you cannot use the T without
first dealing with the case where there isn’t one.
Coming from Java, C# or Python
The habit is checking for null, or not checking and finding out later. The replacement is not “check more carefully”. It is to notice that a nullable reference is a type saying two different things at once, and that separating them lets the compiler track which one you are holding. A
Stringin Rust is always a string. If it might not be there, its type says so.
Traits describe capability, not classification
The third idea is that behavior is attached to types separately from the types themselves. A trait is a named set of things a type can do:
use std::fmt;
struct Money {
cents: i64,
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
fn main() {
println!("{}", Money { cents: 1250 });
}
Money is not a subclass of anything. It gained the ability to be formatted
because an impl block said how, and it could have gained that ability in a
different crate from the one that defined it. A function can then require the
capability rather than a position in a hierarchy: fn log<T: fmt::Display>(item: T) accepts anything that can display itself, including types that did not exist
when log was written.
There is no inheritance in Rust. No struct extends another struct, there are no
base classes, no super, no overriding. This is the part that most unsettles
programmers arriving from an object-oriented background, and chapters 9 and 11
are about why the loss is smaller than it sounds and what replaces it. The short
version is that inheritance bundles four separate things — subtyping, code reuse,
interface conformance and a shared initialization path — and Rust unbundles them,
handing interface conformance to traits, code reuse to composition and generics,
and initialization to ordinary functions.
The compiler is part of the thought process
One practical note before the real chapters begin. Rust’s error messages are unusually good, and treating them as noise to be silenced is the single most expensive mistake a newcomer makes. When the compiler says that something does not live long enough, it is not asking you to add an annotation until it stops complaining. It is telling you that your program contains a reference that outlives the thing it refers to, which is a fact about your design, and which would have been a crash in C and a mystery in a language with a collector.
The productive loop is to read the error as a question about the design. Who should own this. Does this really need a reference, or does it need its own copy. Is this function doing two jobs that want different access. Programmers who run that loop for a few weeks report the same thing: the errors stop appearing, not because they learned the tricks, but because they stopped writing the designs that produce them.
What the model buys
The reward for all of this arrives in chapter 14 and is worth naming now. Because the compiler tracks who owns each value and enforces that a value has either many readers or one writer, it already has everything it needs to prove that two threads cannot race on the same data. Data race freedom is not a separate feature bolted onto Rust’s concurrency story; it is the same rule that prevents use-after-free, applied across threads instead of across time. You get to share data between threads without a mutex when the sharing is read-only, and the compiler will not let you forget the mutex when it is not.
That is the bargain. You answer questions about responsibility that other languages let you leave unanswered, and in exchange a class of bugs that dominates the industry’s crash reports becomes unrepresentable.
Conclusion
Six ideas carry the rest of the book. Values live in places and are owned by exactly one place at a time. Ownership schedules destruction, and destruction handles far more than memory. Giving a value away is a move, because responsibility cannot be duplicated. Borrowing grants access without responsibility, under a rule that permits many readers or one writer. Types, especially enums, are where you put the facts you do not want to check at runtime. Traits name capabilities, and capability is what code should depend on rather than ancestry.
The next chapter slows down and takes the first two seriously, because ownership is the concept everything else is built on and the one that most needs to become reflexive.
Questions to think through
Think about the last program you wrote in another language. Pick one object that lived a long time and was referred to from several places. Who was responsible for deciding it was finished, and how would you have written that down?
The Drop example in this chapter prints its messages in an order determined
entirely by the shape of the source code. What would have to be true about a
language for that to be impossible?
A function signature in Rust distinguishes taking a value, borrowing it, and borrowing it exclusively. Find a method in a codebase you know well and decide which of the three it actually wants. Does its current signature say so?
How to Think in Terms of Ownership
There is a question every program answers and most languages decline to ask: for each thing this program creates, who decides when it is finished?
You have always answered it. In C you answered it by hand and hoped the answer
was consistent across every path through the function. In Java you answered it
for memory by not answering, and then answered it again, badly, for files and
sockets and locks. In Python you answered it with reference counting you could
not see and a with statement you had to remember. The answer existed in all
three cases. It was written in comments, in naming conventions, in the tribal
knowledge that the cache owns the session objects and the request handler only
borrows them, and it was enforced by nothing.
Rust asks you to write the answer in the program. That one requirement is what makes the language feel foreign at first, because an answer that has to be written down has to be decided, and a great many designs turn out to have been avoiding the decision.
Ownership is about responsibility, not access
The first confusion to clear up is that owning a value and being able to use a value are different things.
Owning a value means being responsible for its destruction. The owner is the place the value lives, and when that place goes away, the value is dropped. That is the extent of what ownership means. It says nothing about who may read the value, who may change it, or how many pieces of code have their hands on it right now.
Access is the other axis, and it is handled by borrowing, which the next chapter is about. A function can read a value it does not own. A thousand functions can read it at once. What none of them can do is decide that it is finished, because that decision belongs to one place and the compiler knows which one.
Holding the two apart is the first real skill. New Rust programmers routinely
reach for ownership when they want access — writing fn total(items: Vec<Item>)
when they only meant to add up some numbers, and then discovering that the caller
no longer has the items. The signature said “give me these permanently and I will
destroy them”, which was not what anyone intended.
Every program has an ownership tree
Draw the long-lived data in a program you know and connect each thing to the thing responsible for it. In a web service, the process owns the configuration and the connection pool; the pool owns the connections; a request handler owns the parsed request; the request owns its headers and body. Almost all of it is a tree, and where it isn’t, someone has usually had a bad afternoon.
Rust’s model is that tree, made explicit and checked. A Vec<Item> owns its
items; when the vector is dropped, each item is dropped with it. A struct owns
its fields. A Box<T> owns whatever is on the other end of it. Nesting these
gives you arbitrarily deep ownership, and the destruction order is determined
entirely by the shape:
struct Order {
id: u64,
lines: Vec<Line>,
}
struct Line {
sku: String,
quantity: u32,
}
fn main() {
let order = Order {
id: 1001,
lines: vec![
Line { sku: String::from("A-1"), quantity: 2 },
Line { sku: String::from("B-7"), quantity: 1 },
],
};
println!("order {} has {} lines", order.id, order.lines.len());
}
When order goes out of scope at the end of main, the Vec is dropped, which
drops each Line, which drops each String, which frees each heap buffer. You
did not write any of that and there is no collector involved. The tree in the
source is the deallocation schedule.
The interesting work in Rust design is deciding what this tree should look like, and the interesting difficulties arrive when a design wants a graph instead. Chapter 12 is about that case and it is a real case; most of the time, though, a desire for a graph turns out on inspection to be a desire to avoid deciding, and the decision is available.
Coming from an object-oriented language
Your instinct is that objects refer to each other freely and the runtime sorts it out. An
Orderholds a list ofLineobjects, eachLineholds a reference back to itsOrder, theCustomerholds a list ofOrders and eachOrderpoints back at theCustomer. None of those references means anything about lifetime, which is why you never had to think about it. Rust will make you separate the two: one of those links is ownership and the others are lookups, and sorting one from the other is design work you were previously allowed to skip.
Moves, precisely
Assignment moves. So does passing an argument, returning a value, pushing into a collection, and capturing by value in a closure. In each case a value changes owner, and the previous owner becomes unusable:
fn main() {
let original = String::from("hello");
let moved = original;
println!("{original}");
}
The compiler rejects the last line with borrow of moved value. It is worth
noticing what the compiler is not saying. It is not saying the memory is gone —
the bytes are fine, and at the machine level the move was three words copied from
one stack slot to another. It is saying that responsibility now sits with moved
and that letting you read original would be letting two names lay claim to one
value, which ends with one buffer freed twice.
Two practical consequences follow, and both surprise people.
A move is cheap. It is a shallow bitwise copy of the value itself, and for
anything heap-backed that is a pointer and a couple of integers. Moving a
Vec<Item> with a million items copies three words. The optimizer frequently
removes even that. Programmers arriving from C++ sometimes carry an anxiety about
copying large objects around; in Rust, returning a big struct by value from a
function is the normal thing to do and costs approximately nothing.
A move is not a mutation. let moved = original; does not change the value.
It changes which name is responsible for it. This distinction matters when you
read error messages, because “value moved here” is pointing at a transfer of
responsibility, not at something that damaged your data.
What does not move
Some types are marked Copy, which means that duplicating their bits produces a
second, independent, equally valid value. Integers, floats, bool, char,
shared references, and tuples and arrays of such things are all Copy.
Assignment of a Copy value duplicates it rather than moving it, and the
original stays usable:
fn main() {
let count = 5;
let also_count = count;
println!("{count} and {also_count}");
}
The rule for which types can be Copy is simple and worth understanding rather
than memorizing: a type can be Copy only if duplicating its bits is harmless,
which rules out anything that owns a resource. A String cannot be Copy
because two Strings with the same pointer would both free the same buffer. A
type with a Drop implementation cannot be Copy for the same reason — if
destruction does something, doing it twice is a bug.
Note
Copyis not an optimization and not a performance setting. It is a statement that the type has no resource to be responsible for. Deriving it on a type of six integers is reasonable; wanting it on a type that manages something is a sign that you wantCloneinstead.
Clone is a decision, not a repair
Clone is the explicit request for a second, independent value. Calling
.clone() on a String allocates a new buffer and copies the bytes. Calling it
on a Vec<Order> clones every order in it.
The reason clone is a method call rather than something the language does for
you is so that the cost appears in the source. In a language with implicit
copying, whether a given assignment allocates is a question you answer by reading
the class; in Rust it is a question you answer by looking for the word.
This makes .clone() a fine thing to write on purpose and a bad habit to write
reflexively. The reflex goes like this: the compiler complains about a moved
value, you add .clone(), the complaint stops, and you move on having learned
nothing and having introduced an allocation that the design did not need. Do that
a few hundred times and you have a program that is slower than its Java
equivalent and no easier to reason about.
The useful discipline is to treat every .clone() as answering a question. Ask
what the second owner is for. Sometimes the answer is good — two threads
genuinely need independent copies, or you are about to modify one while keeping
the other. Sometimes the answer is “so that the compiler stops talking”, which
means the real problem is that a function is taking ownership when it wanted a
borrow, or that two parts of the program are arguing about who is responsible for
something.
One legitimate use is worth naming because it is easy to feel guilty about:
cloning a small value to escape a borrow that is awkward to arrange, in code that
is not hot. Cloning a String of forty characters once per request is not a
performance problem. The discipline is about knowing why you did it, not about
never doing it.
A function signature is a statement of intent
With ownership and borrowing both available, the parameter list of a function becomes the clearest documentation in the program. There are four things a signature can say, and picking the right one is most of what API design in Rust consists of.
Taking T says I will take responsibility for this. Use it when the function
stores the value somewhere, consumes it, transforms it into something else, or
sends it to another thread. Vec::push takes its argument by value because the
vector is going to own it from now on.
Taking &T says I will read this and give it back untouched. This is the
default for anything that inspects, computes, formats or compares. It is also
what most methods want for self.
Taking &mut T says I will modify this in place. The caller keeps ownership
and gets a changed value back in the same place it was.
Returning T says the caller is now responsible for this. Constructors,
parsers, builders and any function that produces something fresh are written
this way.
Consider what the wrong choice communicates. A function
#![allow(unused)]
fn main() {
fn summarize(lines: Vec<String>) -> String {
lines.join("; ")
}
}
announces that it eats its input. The caller who wants to log the lines afterwards has to clone them before calling, which pushes the cost of a bad signature onto everyone who uses it. Changing it to
#![allow(unused)]
fn main() {
fn summarize(lines: &[String]) -> String {
lines.join("; ")
}
}
costs nothing at the call site, keeps the caller’s data, and reads as what it is.
The habit to build is to start every parameter at &T, move to &mut T when the
function needs to change something, and move to T only when you can say where
the value is going to live afterward.
Note
&[String]rather than&Vec<String>, and&strrather than&String. A borrowed slice accepts more kinds of caller — arrays, vectors, sub-ranges — at no cost, and there is nothing you can do with&Vec<T>that you cannot do with&[T]. This is a small convention with a large reach, and it is the sort of thing chapter 15 is about.
Moving values out of things
Ownership inside a container is still ownership, which means you can take it
back. A Vec hands an element back out with remove or pop, and the value it
returns is yours:
fn main() {
let mut queue = vec![String::from("first"), String::from("second")];
let taken = queue.remove(0);
println!("took {taken}, {} left", queue.len());
}
Moving a field out of a struct you own works too, and leaves the struct partially moved — usable field by field, but no longer usable as a whole:
struct Envelope {
address: String,
contents: String,
}
fn main() {
let envelope = Envelope {
address: String::from("1 Main St"),
contents: String::from("a letter"),
};
let contents = envelope.contents;
println!("to {}: {contents}", envelope.address);
}
What you cannot do is move a field out through a reference, because the reference does not carry the right to leave a hole behind:
#![allow(unused)]
fn main() {
struct Envelope { address: String, contents: String }
fn take_contents(envelope: &mut Envelope) -> String {
envelope.contents
}
}
The compiler refuses, and it is right to: the caller still owns that Envelope
and is entitled to find both of its fields intact. When you genuinely want to
take a value out through an exclusive reference, you have to put something in its
place, which the standard library provides directly:
#![allow(unused)]
fn main() {
struct Envelope { address: String, contents: String }
fn take_contents(envelope: &mut Envelope) -> String {
std::mem::take(&mut envelope.contents)
}
}
std::mem::take swaps in the type’s default value — an empty String here — and
hands you what was there. Its sibling std::mem::replace lets you choose the
replacement. Both are worth knowing early, because the pattern of “take the
current state out and put a new state in” shows up constantly in code that
manages a state machine, and reaching for Option and clone instead is the
usual first instinct.
The reflex to watch for
The most common moment of friction in early Rust looks like this: you have a
value, two parts of the program need it, and the compiler will not let both have
it. The reflex is to clone, or to reach for Rc<RefCell<T>>, which is the
reference-counted shared-mutable box that makes Rust behave like the language you
came from.
Before either, run through four answers in order.
Does one of them only need to read it? Then it takes a &T, and the question
dissolves. This resolves the majority of cases and it is the one people skip past
because it feels too easy.
Can one of them own it and hand it over when it’s done? A pipeline stage that takes a value, transforms it, and returns it keeps a single owner moving through the program, and that is often what the design wanted.
Do they genuinely need independent values? Then clone, and you have a reason you can state.
Do they need to refer to the same logical thing without either owning it? Then stop thinking about pointers and think about identity. Give the thing an identifier, let one place own the storage, and let everywhere else hold the identifier.
That last answer deserves an example, because it is the one that most often replaces a graph with a tree:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
struct Library {
books: HashMap<u32, Book>,
}
struct Book {
title: String,
}
struct Loan {
book_id: u32,
borrower: String,
}
impl Library {
fn title_of(&self, loan: &Loan) -> Option<&str> {
self.books.get(&loan.book_id).map(|book| book.title.as_str())
}
}
}
The Library owns every Book. A Loan refers to one by identifier rather than
by pointer, which means loans can be stored anywhere, serialized, sent to another
thread, or written to a database without anything changing. The indirection you
gave up — following a pointer straight from a loan to a book — is replaced by a
hash lookup through the one place that owns the data.
This is not a workaround for a language limitation. It is the design that most systems converge on eventually anyway, because identifiers survive serialization and pointers do not, and because having one place responsible for the books is what makes it possible to say what happens when a book is removed. Rust just asks for it on day one.
Conclusion
Ownership is the answer to a question every program has and most languages hide: who decides when this value is finished. Rust makes the answer part of the program text, which means it has to be decided, and deciding it produces a tree of responsibility that also serves as the destruction schedule.
Moving a value transfers that responsibility and costs almost nothing. Duplicating it is a separate, explicit act with a visible price. Reading or modifying a value without taking responsibility for it is borrowing, and it is what most code should be doing most of the time. A function’s parameter list says which of these it means, and choosing accurately is the bulk of good Rust API design.
The next chapter takes up borrowing properly: the rule that governs it, the reason the rule is stricter than it first appears to need to be, and the design shapes that live comfortably inside it.
Questions to think through
Take a class from a system you know and list its fields. For each one, decide whether the class owns that thing, or merely refers to it. Are there fields where the answer differs depending on which instance you are looking at?
A move is a shallow copy plus a promise that the source will not be used again.
What would go wrong if the compiler let you use the source anyway, for a type
with no Drop implementation and no heap allocation? Is that the same answer for
a File?
Find a place in your own code where you copied a collection defensively before passing it to a method. What was the copy protecting against, and which of Rust’s four answers would have covered it?
Borrowing: Reading and Writing Without Owning
Ownership answers who is responsible for a value. It does not answer who gets to use it, and if it had to, Rust would be unbearable — every function that wanted to read a string would have to take the string and give it back.
Borrowing is the answer to the second question, and it comes with one rule:
A value may have any number of shared references, or exactly one exclusive reference, and never both at the same time.
Everything in this chapter is an unpacking of that sentence. It is the rule newcomers spend the most time arguing with, so it is worth starting with why it is stricter than it looks like it needs to be.
Three bugs that are the same bug
Consider three failures from three different eras of programming.
A C program frees a buffer, then reads through a pointer it kept. The memory has been reused for something else, so the read succeeds and returns nonsense, or the allocator’s bookkeeping has moved in and the read corrupts it. Nothing crashes where the mistake was made.
A Java program iterates over a list and adds to it inside the loop. The iterator
notices that the list’s internal state moved out from under it and throws a
ConcurrentModificationException, at runtime, in production, on the one code
path that only runs for customers with more than one address. A C++ program doing
the same thing does not even get the exception; the vector reallocated and the
iterator now points into a freed buffer.
A Go program runs two goroutines over the same map. Sometimes it works. Sometimes the map is left in a state that crashes the runtime an hour later.
These are usually taught as three separate topics with three separate remedies: be careful with pointers, do not modify collections while iterating, use a mutex. They are the same bug. In each case, something was reading a value, and something else changed that value underneath it, and the reader’s view of the world became a lie. The reader can be a pointer, an iterator, or another thread; the change can be a free, a reallocation, or a write. The shape is identical.
Rust does not fix these three problems. It makes the shape they share impossible, which fixes all three and some others nobody had named yet. That is why the rule is about readers and writers rather than about pointers or threads — it is aimed at the general case, and the specific cases fall out.
Note
References are compile-time constructs with no runtime representation beyond being pointers. Borrowing is checked while the program is built and costs nothing while it runs. There is no counter, no flag, no check. What this chapter describes compiles down to the same machine code a C programmer would have written by hand and hoped was correct.
Shared references
&value produces a shared reference. You may have as many as you like, they may
be copied and passed around freely, and through all of them the value is
read-only:
fn main() {
let text = String::from("hello");
let first = &text;
let second = &text;
println!("{first} {second} {text}");
}
The owner can still read it too: text is usable here
because reading is what shared references permit, and one more reader changes
nothing.
Because shared means read-only, a shared reference is a view you can trust. If
you are holding one, nothing can change the value while you hold it. Not another
function, not another thread, not the owner. This is the guarantee that makes
&self methods safe to write without defensive copying, and it is the guarantee
that makes sharing data across threads possible without a lock, which chapter 14
takes up.
Coming from Java or C#
A
finalreference in Java means you cannot reassign the variable. It says nothing about whether the object it points to can change, which is whyfinal List<String> namesstill lets anyone with a handle calladd. Rust’s&Tis the guarantee you thoughtfinalwas: not the binding is fixed, but the data behind it will not change while you are looking.
Exclusive references
&mut value produces an exclusive reference. There can only be one at a time,
and while it exists there may be no shared references either:
fn main() {
let mut text = String::from("hello");
let writer = &mut text;
let reader = &text;
writer.push('!');
println!("{reader}");
}
The compiler rejects this with cannot borrow text as immutable because it is also borrowed as mutable. Run the failure through: push may need to grow the
buffer, growing reallocates, and reallocating leaves reader pointing at freed
memory. The rejection is the C bug from earlier, caught at build time in a program
that never mentioned pointers.
To create a &mut, the owner’s binding must itself be mut. You cannot hand out
the right to change something you do not have the right to change.
A borrow lasts as long as it is used
The rule is about overlapping borrows, and the compiler measures overlap by use rather than by lexical scope. A borrow starts where it is created and ends after its last use, which means this is fine:
fn main() {
let mut text = String::from("hello");
let view = &text;
println!("{view}");
text.push('!');
println!("{text}");
}
view is still in scope on the line where text is modified, but it is never
used again, so its borrow has already ended. Code written under the older,
stricter rule is full of artificial blocks introduced to end borrows early; you
will see them in older examples and you rarely need them now.
This has a practical consequence for how you arrange a function. Borrows that overlap are a problem; borrows that take turns are not. A surprising number of borrow errors are fixed by moving one line above another, because the two accesses did not actually need to happen at once:
fn main() {
let mut scores = vec![10, 20, 30];
let highest = *scores.iter().max().unwrap();
scores.push(highest + 1);
println!("{scores:?}");
}
Reading the maximum out into an owned i32 ends the borrow of scores before
the push begins. Written as scores.push(scores.iter().max().unwrap() + 1) the
same code is rejected, because the shared borrow for the argument is still live
when the exclusive borrow for the method call is taken.
Methods and self
Everything above applies to methods, which take their receiver the same three ways any function takes any parameter.
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
fn value(&self) -> u32 {
self.count
}
fn increment(&mut self) {
self.count += 1;
}
fn into_total(self) -> u32 {
self.count
}
}
fn main() {
let mut counter = Counter::new();
counter.increment();
counter.increment();
println!("{}", counter.value());
let total = counter.into_total();
println!("{total}");
}
Four signatures, four different statements. new takes no receiver and produces
a value, which is how Rust writes constructors. value borrows shared: it reads
and promises to change nothing. increment borrows exclusively: it changes the
counter in place and the caller keeps it. into_total takes self by value: it
consumes the counter, which is why counter cannot be used after that line.
That last one is a genuine design tool rather than a curiosity. A method that
takes self ends the value’s life, which lets you write transitions that cannot
be undone — a builder that turns into the thing it was building, a connection
that turns into a stream, an unvalidated request that turns into a validated one
and leaves no way to reach the unvalidated version. Chapter 6 builds on this
heavily.
Note
The naming convention in the standard library is worth adopting from the start. A method named
as_somethingborrows cheaply,to_somethingclones or converts at a cost, andinto_somethingconsumes the receiver. Three prefixes, and a reader who knows them can tell what a call costs and what happens to the receiver without checking the signature.
The problem you will actually hit
Everything so far has been comfortable. The friction begins when a struct’s methods need two parts of it at once.
The compiler tracks borrows of paths. game.board and game.score are
different paths, so borrowing both at once is fine:
struct Game {
board: Vec<u8>,
score: u32,
}
fn main() {
let mut game = Game { board: vec![0; 9], score: 0 };
let board = &mut game.board;
let score = &mut game.score;
board[4] = 1;
*score += 10;
println!("{:?} {}", game.board, game.score);
}
A method call, though, borrows the whole struct, because a signature says &mut self and the compiler will not look inside the body to find out which fields
were touched. So this is rejected:
struct Game { board: Vec<u8>, score: u32 }
impl Game {
fn cells(&mut self) -> &mut Vec<u8> {
&mut self.board
}
fn score(&self) -> u32 {
self.score
}
}
fn main() {
let mut game = Game { board: vec![0; 9], score: 0 };
let cells = game.cells();
let current = game.score();
cells[0] = 1;
println!("{current}");
}
cells holds an exclusive borrow of the entire Game, so the call to score
cannot take a shared one. The programmer knows these touch different fields; the
signatures do not say so, and signatures are what the compiler has.
This is the single most common source of “the borrow checker is fighting me”, and it has four resolutions, and none of them is a trick.
Finish with one before starting the other. Read score into a local first, or
use cells and let its borrow end. Most occurrences are this, and they are
usually an improvement — the code was interleaving two things that had no reason
to interleave.
Touch the fields directly instead of going through methods. Inside the struct’s
own impl block you have access to the fields, and self.board and self.score
are separate paths. Method calls are the thing that coarsens the borrow, so
inside the type, prefer fields.
Pass the pieces rather than the whole. A free function or an associated
function that takes board: &mut Vec<u8>, score: &mut u32 states exactly what it
needs and composes with anything else touching the rest of the struct. This reads
worse to an object-oriented eye, which wants behavior attached to the object, and
it is frequently the better design: a function that declares it touches two
fields is easier to reason about than one that declares it might touch anything.
Split the struct. If two groups of fields keep wanting to be borrowed
independently, that is evidence they are two things. Pulling Board out of
Game gives you &mut game.board as a whole object with its own methods, and
the tension disappears. Borrow errors that keep recurring in the same place are
often a design telling you where it wants to divide.
Coming from an object-oriented language
The habit being challenged is that all behavior belongs on the object that owns the data, so every operation becomes a method on the largest available class, and that class grows until it is the program. Rust makes the cost of that shape visible immediately, because a method on the big class borrows the big class. Smaller types and freer functions are not a Rust idiosyncrasy; they are what your design already wanted.
Iterating and modifying
The iterator case deserves its own look, because it is where people first meet the rule in anger:
fn main() {
let mut names = vec![String::from("ann"), String::from("bob")];
for name in &names {
if name == "ann" {
names.push(String::from("cleo"));
}
}
}
&names borrows the vector for the whole loop, so the push cannot take an
exclusive borrow. This is the ConcurrentModificationException again, moved from
runtime to build time.
The resolutions are the same ones you would reach for in any language, except
that here you reach for them before the bug exists. Collect what you want to add
into a separate vector and append it after the loop. Iterate over indices when
you need to reach into the collection as you go. Use retain when the goal is
removal, iter_mut when the goal is changing elements in place rather than
changing the collection’s shape:
fn main() {
let mut names = vec![String::from("ann"), String::from("bob")];
for name in names.iter_mut() {
name.push_str(" (checked)");
}
names.retain(|name| !name.starts_with("bob"));
println!("{names:?}");
}
iter_mut hands out one exclusive reference per element while holding an
exclusive borrow of the vector, which is sound because the vector’s shape cannot
change while you hold it. The distinction Rust is drawing — changing the elements
is fine, changing the collection is not — is the distinction that makes iterator
invalidation impossible, and it is one your other languages were relying on you
to observe by convention.
Returning references
A function can hand a reference back out:
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(index) => &text[..index],
None => text,
}
}
fn main() {
let sentence = String::from("borrowing is not sharing");
println!("{}", first_word(&sentence));
}
The returned &str points into the caller’s string. No copy is made, and the
compiler knows the result cannot outlive the input, so the caller cannot keep the
word after dropping the sentence.
How it knows is the subject of the next chapter. For now, the thing to take from this example is the design possibility it opens: a function can return a view into data it was given, and views are free. Parsers, tokenizers, string processing and anything that slices up a buffer all get to work without allocating, and the compiler guarantees nobody holds a view of something that no longer exists.
Conclusion
Borrowing separates access from responsibility. A shared reference is a read-only view and there may be many; an exclusive reference is the only way in and there may be one. The rule exists because a reader whose data changes underneath it is the shape shared by use-after-free, iterator invalidation and data races, and forbidding the shape forbids all three.
Borrows are tracked by path and last until their final use, which means most early friction is either two accesses that could have taken turns, or a method signature that borrowed a whole struct when it wanted one field. The remedies are ordinary design moves: do one thing at a time, name the parts you need, and split types that keep wanting to be borrowed in pieces.
The next chapter is about the piece deliberately left out here — how the compiler knows that a returned reference is still valid, and what it means for a type to hold a reference rather than own its data.
Questions to think through
The three bugs at the start of the chapter are usually taught as unrelated. Write down, in one sentence each, what a reader was relying on and what changed. Does the sentence differ between them?
iter_mut allows changing elements but not the collection. Find a loop you have
written that modified a collection while walking it, and decide which of the two
it actually needed.
A method taking &mut self borrows the entire struct. Take a class you know with
more than a dozen fields and ask which of its methods touch which fields. How
many independent groups are there?
Lifetimes: The Scope of Validity
Lifetimes have a reputation as the hard part of Rust, and the reputation is
mostly a misunderstanding about what they are. The misunderstanding is that a
lifetime annotation does something — that writing 'a creates or extends or
manages the life of a value, the way new and free do in C.
It does not. Every reference in every Rust program already has a lifetime, whether or not anyone wrote it down, and that lifetime was determined by the structure of the code long before the annotation was typed. A lifetime parameter in a signature is a description: it tells the compiler how the lifetimes of the things going in relate to the lifetimes of the things coming out. It is closer to a type annotation than to a memory operation, and like a type annotation, it is often unnecessary because the compiler can work it out.
Most Rust code contains no lifetime annotations at all. The chapters you have
read so far are full of references and free of 'a. What this chapter is about
is the model underneath, so that when you do meet an annotation you can read it,
and when the compiler tells you something does not live long enough, you can hear
the design question in it.
What a lifetime actually is
A lifetime is a region of the program: the span during which a reference is guaranteed to point at something valid. The compiler computes one for every reference and checks a single property, which is that the region a reference is used in fits inside the region the referent exists in.
Here is that check failing:
fn main() {
let outer;
{
let inner = String::from("temporary");
outer = &inner;
}
println!("{outer}");
}
The compiler says inner does not live long enough. It is not being pedantic
about a syntax rule. inner is dropped at the closing brace, its buffer is
freed, and outer would be a pointer into freed memory — the C bug from chapter
3, arriving without a single unsafe or raw pointer in sight. Lifetime checking is
that comparison, applied everywhere references travel.
Nothing in that example needed an annotation. The compiler could see both scopes. Annotations become necessary only at the one place where the compiler cannot see: a function boundary.
Why function signatures need help
A function is compiled once and called from anywhere. When the compiler checks a call site, it does not look inside the function body; it looks at the signature. So the signature has to carry enough information to answer the question the caller cares about, which is how long the returned reference stays valid.
Consider a function that returns whichever of two strings is longer. Written without annotations, it is rejected:
#![allow(unused)]
fn main() {
fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
}
The compiler’s complaint is that it cannot tell whether the return value borrows
from a or from b. That matters: if the result borrows from a, the caller
may drop b immediately and keep using the result, and if it borrows from b,
they may not. The body knows the answer is “either one, depending”. The signature
has to say so:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let title = String::from("the rust thought process");
let subtitle = String::from("a book");
println!("{}", longer(&title, &subtitle));
}
Read the signature as a sentence: for some region 'a, given two string slices
that are both valid for 'a, I return a slice that is valid for 'a. Since the
caller supplies the arguments, 'a ends up being the shorter of the two
borrows, and the result cannot outlive either input. That is precisely the
guarantee the body needs.
Now look at a variant that tells a different story:
fn with_prefix<'a>(text: &'a str, prefix: &str) -> &'a str {
if text.starts_with(prefix) {
&text[prefix.len()..]
} else {
text
}
}
fn main() {
let path = String::from("/usr/local/bin");
let stripped;
{
let prefix = String::from("/usr");
stripped = with_prefix(&path, &prefix);
}
println!("{stripped}");
}
Only text carries 'a. The signature says the result borrows from the first
argument and has nothing to do with the second, which is why main can drop
prefix and go on using the result. Had both parameters shared 'a, that
program would not compile, and the restriction would be a fiction invented by a
careless signature rather than a fact about the code.
This is the thing to take from the chapter. A lifetime annotation is a claim about which inputs the output borrows from, and getting it right is a design decision, not a syntax ritual. The compiler will accept an over-strict signature happily; your callers are the ones who pay.
The cases you never have to annotate
Rust applies three rules before asking you for anything, and between them they cover the overwhelming majority of functions.
Every elided input lifetime becomes its own distinct parameter. A function taking two references is treated as taking two unrelated ones.
If there is exactly one input lifetime, it is assigned to every output lifetime.
This is why fn first_word(text: &str) -> &str from chapter 3 needs no
annotation: there is only one place the result could have come from.
If one of the inputs is &self or &mut self, its lifetime is assigned to every
output lifetime. This is why methods returning references almost never carry
annotations — the answer is nearly always “it borrows from the receiver”, and the
language assumes so.
When none of the three produces an unambiguous answer, the compiler stops and
asks, which is what happened with longer. Treat that as the prompt to decide
which input the result actually comes from.
Note
If you find yourself adding a lifetime parameter because the compiler asked and you are not sure which input is right, stop and work it out rather than trying
'ain different positions until it builds. A signature that compiles is not necessarily the signature you meant, and the wrong one becomes a constraint on every caller.
Types that borrow
A struct can hold a reference, and when it does, it takes a lifetime parameter:
struct Tokenizer<'a> {
input: &'a str,
position: usize,
}
impl<'a> Tokenizer<'a> {
fn new(input: &'a str) -> Tokenizer<'a> {
Tokenizer { input, position: 0 }
}
fn next_token(&mut self) -> Option<&'a str> {
let rest = &self.input[self.position..];
let start = rest.find(|c: char| !c.is_whitespace())?;
let remainder = &rest[start..];
let end = remainder.find(char::is_whitespace).unwrap_or(remainder.len());
self.position += start + end;
Some(&remainder[..end])
}
}
fn main() {
let text = String::from("ownership borrowing lifetimes");
let mut tokenizer = Tokenizer::new(&text);
while let Some(token) = tokenizer.next_token() {
println!("{token}");
}
}
Tokenizer<'a> is not a type that holds a string. It is a type that holds a
view of a string owned by somebody else, and the 'a propagates that fact into
the type system: a Tokenizer can never outlive the text it was built from, and
the compiler enforces it at every place a Tokenizer is stored or returned.
Notice what this buys. The tokenizer allocates nothing. Every token it hands back points into the original buffer, and a program that tokenizes a hundred-megabyte file does no copying at all. This is the design that lifetime parameters exist to make safe, and it is why Rust’s parsers, serializers and text tools are as fast as they are.
When a borrowing type is the wrong answer
That same power is the most common way for a newcomer’s design to go wrong, and the mistake is worth naming clearly because it costs people weeks.
The shape is this. You write a struct that owns its data. Somewhere a borrow
error appears. You notice that changing a String field to a &str field makes
the error go away, so you add a lifetime parameter. Now the struct that contains
that struct needs one too. Then the function returning it needs one, and the
trait implementation will not accept one, and eventually you are threading 'a
through eleven types to store a configuration value that is forty bytes long.
The diagnosis is that a lifetime parameter on a type is a statement that the type is a temporary view, and most types are not. Ask what the type is for. If it is created, used and dropped inside one function or one loop iteration — a parser over a buffer, an iterator, a formatter, a visitor walking a tree — then it is a view, and a lifetime is the right tool. If it is stored in a collection, returned up several layers, held across an await point, put in a struct that lives as long as the program, or sent to another thread, then it is storage, and it should own its data.
The cost of owning is an allocation and a copy. Compared to threading a lifetime parameter through a type hierarchy so that a config struct can avoid copying a path name once at startup, the allocation is free.
Coming from C++
The instinct that copying is expensive and must be avoided by default is correct in a language where a copy might be a deep copy of an object graph and a
const¶meter is how you opt out. Rust moves rather than copying, so passing by value is already cheap, andStringversus&stris a question about who owns the bytes rather than about how much work the call does. Reserve the borrowing design for the places where it removes real work — parsing, scanning, slicing large buffers — and let ordinary structs own what they hold.
The 'static misunderstanding
'static has two meanings that look identical and are not, and conflating them
produces some of the most confusing error messages in the language.
As the lifetime of a reference, &'static str means the referent lives for the
duration of the program. String literals are the everyday example: they are baked
into the binary, so let name: &'static str = "anderix"; is fine.
As a bound on a generic parameter, T: 'static means something weaker and much
more common: T contains no references with a lifetime shorter than the whole
program. Every owned type satisfies it. String is 'static. Vec<u64> is
'static. A struct of owned fields is 'static. What is not 'static is
Tokenizer<'a> from earlier, because it holds a borrow of something that might
go away.
So when std::thread::spawn requires its closure to be 'static, it is not
demanding that the closure run forever. It is saying that the closure may not
hold a reference to a local variable in the spawning function, because that
function may return while the thread is still running. Chapter 14 comes back to
this. For now, read T: 'static as “owns its data” and the errors stop being
mysterious.
Reading lifetime errors as design questions
Three messages account for most of what you will see, and each has a small set of sensible responses.
x does not live long enough means a reference is being kept past the life of
its referent. Either the referent should live longer — declare it in an outer
scope, or hoist it out of the loop — or the thing holding the reference should
own its data instead.
Cannot return reference to local variable means a function is trying to hand out a view of something it is about to drop. The function should return the owned value, or take a destination to write into.
Missing lifetime specifier means a signature is ambiguous about which input the output borrows from. Decide which one, and if the answer is “neither, it’s something I built in here”, the function should return an owned value.
In all three, the productive move is the one chapter 1 described: read the error as a question about the design. The failing shapes are genuinely broken programs, and the compiler is naming the break.
Conclusion
A lifetime is the region in which a reference is valid, and the compiler computes it for every reference in the program. Annotations describe relationships across a function boundary, where the compiler can see only the signature. Three elision rules cover the ordinary cases, so most code carries no annotations at all.
A lifetime parameter on a type turns that type into a view: it cannot outlive what it borrows from, which is precisely what you want for parsers and iterators and precisely what you do not want for anything the program stores. When a lifetime parameter starts spreading through your types, that is evidence you asked a storage type to be a view.
The next chapter turns from references to the types themselves, and to how a Rust type is assembled out of data, behavior and the invariants you want it to guarantee.
Questions to think through
The with_prefix example gives 'a to one parameter and not the other. Write
down what would break for a caller if both parameters shared 'a. Is anything in
the function body different?
A Tokenizer<'a> cannot be stored in a struct that outlives its input. Find a
type in a codebase you know that holds a pointer or reference to data it does not
own, and ask what currently prevents that data from being freed first.
T: 'static is satisfied by every owned type. Why is that the right bound for
thread::spawn, and what would go wrong if the bound were dropped?
The Anatomy of a Type
A class in Java or C# is one construct doing four jobs at once. It declares the shape of some data, it declares the operations on that data, it declares a position in an inheritance hierarchy, and it declares which interfaces are satisfied. Because the four arrive together, it is easy to go a whole career without noticing they are separable.
Rust separates them. A struct or an enum declares the shape of the data and
nothing else. An impl block declares operations. A second kind of impl block
declares that the type satisfies a trait. There is no hierarchy at all. This
chapter walks through a type from the outside in, in the order you would build
one, and points out where each of those jobs has gone.
The data declaration
Rust has two ways to build a type out of other types, and the distinction between them runs through every design decision in the language.
A struct is data held together. Every field exists at once, and the size of the struct is the sum of its fields:
#![allow(unused)]
fn main() {
struct Invoice {
number: u64,
customer: String,
total_cents: i64,
paid: bool,
}
}
An enum is a choice between alternatives. Exactly one variant is present, and the size of the enum is the size of its largest variant plus a small tag:
#![allow(unused)]
fn main() {
enum Payment {
Cash,
Card { last_four: String, authorization: u64 },
Check(u32),
}
}
Notice that Payment::Cash carries no data, Payment::Card carries named
fields, and Payment::Check carries an unnamed one. All three shapes are
available in both structs and enum variants, and picking between named and
unnamed fields is a readability question: name them unless the type is obvious
from context, which in practice means name them except for single-field wrappers.
Those two constructs, combined with each other to any depth, are how every data structure in Rust is built. There is no third mechanism and no escape hatch. When you find yourself wanting one, the answer is almost always an enum you have not thought of yet, which is what chapter 6 is about.
Coming from Java or C#
The enum you know is a fixed set of named constants, possibly with fields attached, all of the same shape. Rust’s enum is a different construct wearing the same keyword: each variant has its own shape, and the type is the union of them. The closest thing you have met is a sealed class hierarchy with a switch over subtypes, and the comparison is worth holding onto, because the designs that use a sealed hierarchy in Java are usually the designs that want an enum in Rust.
Fields are private, and private means something different
By default every field is private. Adding pub makes one visible:
#![allow(unused)]
fn main() {
pub struct Invoice {
pub number: u64,
customer: String,
total_cents: i64,
paid: bool,
}
}
The part that surprises people is the boundary that privacy is measured against.
In Java, private means private to the class. In Rust, private means private to
the module — so a struct’s private fields are readable and writable by any code
in the same file, including other types’ methods and free functions.
This sounds looser and works out stricter, because Rust modules are small and
explicit. The unit of encapsulation is the module rather than the type, which
means a set of types that genuinely need to cooperate can live in a module and
see each other’s internals, while the outside world sees only what is pub.
There is no friend, no package-private awkwardness, and no reflection to work
around it.
The practical consequence for design is that you should think of a module as the thing with an interface, not a type. Chapter 15 develops this.
Behavior lives in a separate block
Operations go in an impl block, which may be anywhere in the same crate as the
type and may be split into as many blocks as you like:
pub struct Invoice {
number: u64,
customer: String,
total_cents: i64,
paid: bool,
}
impl Invoice {
pub fn new(number: u64, customer: String, total_cents: i64) -> Invoice {
Invoice { number, customer, total_cents, paid: false }
}
pub fn number(&self) -> u64 {
self.number
}
pub fn is_overdue(&self, days_elapsed: u32) -> bool {
!self.paid && days_elapsed > 30
}
pub fn mark_paid(&mut self) {
self.paid = true;
}
}
fn main() {
let mut invoice = Invoice::new(1001, String::from("Acme"), 250_00);
println!("{} overdue: {}", invoice.number(), invoice.is_overdue(45));
invoice.mark_paid();
println!("overdue now: {}", invoice.is_overdue(45));
}
Two kinds of function appear here. new takes no self and is an associated
function, called as Invoice::new(...). The others take a form of self and
are methods, called as invoice.number(). The only difference is the receiver;
there is no separate static keyword and no separate declaration syntax.
Separating data from behavior has a consequence worth noticing. Because an impl
block is not part of the type declaration, the standard library’s types can gain
methods from your crate through traits, your types can gain methods from a
library’s traits, and a large type’s operations can be grouped into several
blocks by subject rather than piled into one class body. The declaration of what
an Invoice is stays four lines long no matter how much behavior accumulates.
Constructors are a convention, not a feature
Rust has no constructors. new is an ordinary associated function with a name
everyone agreed on, and nothing enforces its existence, its name, or its
signature.
This sounds like a gap and removes a whole category of problem. A constructor in
an object-oriented language runs against a partially built object: the fields
have default values, this exists and can escape, a virtual call can dispatch to
a subclass method that runs before the subclass’s own initialization, and the
object can be observed in a state the class’s invariants do not allow. Rust’s
version builds the value and then returns it, so there is no moment at which an
Invoice exists but is not yet an Invoice.
The consequences show up immediately. There is no null, so a field never holds
one. There is no default initialization, so the compiler rejects a struct literal
that omits a field. A type with private fields and no public constructor cannot
be built from outside its module at all, which makes “you may only obtain one of
these through parse” an enforceable rule rather than a documented wish.
Multiple constructors are just multiple functions, and the standard library’s
naming conventions are worth following. new is the obvious way to build one.
with_capacity, with_timeout and friends are variants taking configuration.
from_str, from_bytes and similar name what they convert from. A constructor
that can fail returns Result:
pub struct Percentage(f64);
impl Percentage {
pub fn new(value: f64) -> Result<Percentage, String> {
if (0.0..=100.0).contains(&value) {
Ok(Percentage(value))
} else {
Err(format!("{value} is not between 0 and 100"))
}
}
}
fn main() {
println!("{:?}", Percentage::new(45.0).map(|p| p.0));
println!("{:?}", Percentage::new(145.0).map(|p| p.0));
}
Once that function is the only way to obtain a Percentage, every Percentage
in the program is between zero and a hundred, and no downstream code needs to
check. That is the idea chapter 6 is built on.
Derives
Most types want a handful of standard capabilities, and asking for them by hand would be tedious, so the compiler will write them:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Sku {
code: String,
revision: u8,
}
fn main() {
let a = Sku { code: String::from("A-1"), revision: 2 };
let b = a.clone();
println!("{a:?} == {b:?} is {}", a == b);
}
derive generates an implementation of a trait based on the fields. Debug
gives you {:?} formatting for logs and tests. Clone gives you explicit
duplication, field by field. PartialEq and Eq give equality, again field by
field. Hash lets the type be a key in a HashMap. PartialOrd and Ord give
ordering, comparing fields in declaration order — which means field order is
semantically significant on an ordered type, a detail that has surprised people.
Deriving is cheap enough that the temptation is to put all of them on everything.
Resist it slightly. Each derive is a public promise: deriving Clone says
duplicating this is meaningful, deriving PartialEq says two of these can be
compared and that comparing all fields is the right comparison. For a type where
one field is a cache or a generated identifier, field-by-field equality is the
wrong answer, and writing the implementation by hand is the right one.
Copy deserves a separate thought. Deriving it changes assignment from a move to
a duplication, which changes how every piece of code using the type behaves. It
is appropriate for small aggregates of numbers — a Point, a Rgb, a
Duration — and inappropriate for anything with an identity or a resource.
Destruction
The counterpart to construction is the Drop trait, and unlike a constructor it
really is a language feature, because the compiler calls it for you:
struct TempFile {
path: String,
}
impl Drop for TempFile {
fn drop(&mut self) {
println!("removing {}", self.path);
}
}
fn main() {
let _scratch = TempFile { path: String::from("/tmp/work.tmp") };
println!("doing work");
}
Drop order is deterministic and worth knowing: local variables are dropped in
reverse declaration order, struct fields in declaration order, and a value’s own
drop runs before its fields are dropped. A Vec drops its elements in order,
front to back.
Most types never implement Drop. The derived behavior — drop every field, which
recursively drops everything they own — is correct for anything that does not
manage a resource outside Rust’s knowledge. You write one when you hold a raw
handle, a lock, a connection, a temporary file, or anything else where “finished”
means an action rather than merely freeing memory. That pattern has a name, and
the TempFile above is an instance of it: a value whose destruction performs the
cleanup, so that no code path can skip it. Chapter 17 returns to it as the guard
pattern.
Note
A type that implements
Dropcannot beCopy, cannot have fields moved out of it individually, and has a few other restrictions. These all come from the same place: if destruction does something, the compiler must guarantee it happens once, on a complete value.
The newtype
The smallest structure in the language is a struct with one unnamed field:
#![allow(unused)]
fn main() {
struct Meters(f64);
struct Feet(f64);
}
At runtime a Meters is an f64 and nothing more: no header, no indirection, no
cost. At compile time it is a distinct type that cannot be passed where Feet is
expected, cannot be added to a bare f64 by accident, and can carry its own
methods and trait implementations.
This is one of the highest-value moves available in Rust and one of the most
underused by newcomers, because it looks like ceremony. It is not. Every
identifier in your program that is currently a String — user IDs, order
numbers, file paths, email addresses, SQL fragments — is a candidate, and every
one you wrap is a class of mix-up the compiler now catches. The Mars Climate
Orbiter was lost to a units mismatch that struct Newtons(f64) would have
caught at build time.
A type alias is not the same thing and does not do this:
#![allow(unused)]
fn main() {
type Meters = f64;
}
That introduces a name for f64 and nothing else. Every f64 is a Meters and
every Meters is an f64, so nothing is checked. Aliases are for shortening
long generic types, not for making distinctions.
Conclusion
A Rust type is assembled rather than declared in one piece. Data shape comes from
structs and enums, held together or chosen between. Behavior comes from impl
blocks that can be split, added later, and written in other crates through
traits. Visibility is measured against the module rather than the type.
Construction is an ordinary function returning a finished value, which is why
there is no partially built object and no null. Destruction is automatic,
ordered by structure, and can be given meaning by implementing Drop.
Two of these do more than they first appear to. A private field plus a fallible constructor makes an invariant that holds everywhere in the program, and a one-field newtype makes a distinction the compiler will police for free. The next chapter is about spending both of them deliberately.
Questions to think through
Pick a class you have written with a constructor that can throw. What does an instance look like between the first assignment and the throw, and who can observe it?
Field-by-field equality is right for some types and wrong for others. Find a type you know where two instances should be considered equal despite differing in some field, and say which field and why.
List the String fields in a type you own. How many of them are really
identifiers that should never be compared to, assigned from, or concatenated with
a different kind of identifier?
Making Illegal States Unrepresentable
Here is a claim worth taking seriously: every runtime check in your program is a fact you failed to write into a type.
That is too strong as an absolute — some facts arrive from the outside world at
runtime and cannot be known earlier — but as a lens it is remarkably productive.
Point it at a codebase and you find the same thing everywhere. A method that
begins by checking whether the object has been initialized. A comment saying that
endDate is only meaningful when status is CLOSED. A test asserting that you
cannot call send() twice. A null check written by someone who had been burned,
sitting next to an identical one written by someone else.
Each of those is a fact about which states are legal, enforced by vigilance. Chapter 5 introduced the two tools that let you enforce them by construction instead: an enum that only permits the shapes you want, and a private field with a fallible constructor. This chapter is about spending them.
Count the states
Start with a type that looks entirely reasonable:
#![allow(unused)]
fn main() {
struct Socket {
handle: u32,
}
struct Connection {
address: String,
socket: Option<Socket>,
connected: bool,
last_error: Option<String>,
}
}
Four fields, and every codebase in every language contains something like it.
Now count what it can represent. Ignoring the address, there are two choices for
socket, two for connected and two for last_error: eight combinations.
How many are legal? Connected with a socket and no error, yes. Disconnected with no socket and an error, yes. Disconnected with no socket and no error — the state before the first attempt — yes. Three legal states out of eight.
The other five are the bugs. Connected with no socket is a null dereference waiting to happen, and it is exactly what you get if a reconnection path clears the socket and forgets the flag. Connected with an error is a state where two pieces of code disagree about what happened. Every method on this type has to decide what to do about combinations that are not supposed to occur, and the usual decision is not to think about them.
The enum version cannot express them:
struct Socket {
handle: u32,
}
enum Connection {
Untried { address: String },
Failed { address: String, error: String },
Live { address: String, socket: Socket },
}
impl Connection {
fn address(&self) -> &str {
match self {
Connection::Untried { address } => address,
Connection::Failed { address, .. } => address,
Connection::Live { address, .. } => address,
}
}
fn handle(&self) -> Option<u32> {
match self {
Connection::Live { socket, .. } => Some(socket.handle),
_ => None,
}
}
}
fn main() {
let connection = Connection::Live {
address: String::from("db.internal:5432"),
socket: Socket { handle: 7 },
};
println!("{} {:?}", connection.address(), connection.handle());
}
Three states, three variants, and the five broken combinations no longer have a
spelling. A socket exists only inside the variant where a socket is meaningful,
which means no code anywhere can reach for one that is not there. The Option
that remains, on handle, is doing real work: it reports that a connection which
is not live has no handle, and the caller has to decide what that means.
This is the core move, and it generalizes: when two fields are only meaningful
in particular combinations, they belong in variants of an enum rather than
alongside each other in a struct. The tell is a boolean that governs whether
another field means anything, or an Option whose documentation says when it is
Some.
Coming from an object-oriented language
Encapsulation was aiming at this. Making fields private and forcing access through methods is an attempt to stop objects reaching states the class does not want, and it half works: the methods can enforce rules, but the object still has all those fields, and any method that sets one can leave the combination invalid until another one runs. The enum removes the intermediate state rather than defending it.
Parse, do not validate
The second tool addresses a different failure, which is a fact that gets checked repeatedly because nothing remembers that it was checked.
#![allow(unused)]
fn main() {
fn send_receipt(email: &str, body: &str) -> Result<(), String> {
if !email.contains('@') {
return Err(String::from("invalid email"));
}
println!("sending to {email}: {body}");
Ok(())
}
}
The check is fine. The problem is that email is a String, so the next
function that receives it knows nothing, and it checks again. And the one after
that forgets to. The validation is a property of a moment rather than of the
value, so it evaporates the instant the value is passed along.
Move the check into a type and it stops evaporating:
pub struct Email(String);
impl Email {
pub fn parse(raw: &str) -> Result<Email, String> {
let trimmed = raw.trim();
match trimmed.split_once('@') {
Some((local, domain)) if !local.is_empty() && domain.contains('.') => {
Ok(Email(trimmed.to_lowercase()))
}
_ => Err(format!("{raw:?} is not an email address")),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn send_receipt(email: &Email, body: &str) {
println!("sending to {}: {body}", email.as_str());
}
fn main() {
match Email::parse(" David.Anderson@Excelano.com ") {
Ok(email) => send_receipt(&email, "thanks"),
Err(message) => println!("{message}"),
}
}
send_receipt no longer validates and no longer returns a Result, because it
can no longer fail. The only way to obtain an Email is parse, the field is
private so nobody can construct one around an arbitrary string, and every
function downstream can treat the value as trustworthy without a comment saying
so.
The name for this discipline is parse, don’t validate. A validating function takes a value and returns a verdict, throwing the knowledge away. A parsing function takes a value and returns a different, narrower type that carries the knowledge in its identity. Same check, same cost, and the result is remembered by the type system instead of by the programmer.
Notice the second thing the parser did: it trimmed and lowercased. A parse
function is the natural place to normalize, because it is the one place every
value passes through. Two Email values that mean the same address now compare
equal, which is a bug class quietly closed on the way past.
The standard library does this to itself. NonZeroU32 is an integer that cannot
be zero, so a function taking one does not check and a division by it cannot
trap. String is a Vec<u8> that has been parsed as UTF-8, which is why
&str methods can assume valid encoding rather than checking on every call.
Making illegal transitions unrepresentable
The third variety is about sequence rather than shape. Some operations are only
legal in a particular order, and the usual enforcement is a status field plus
some if statements:
#![allow(unused)]
fn main() {
struct Article {
body: String,
status: String,
reviewer: Option<String>,
}
impl Article {
fn publish(&mut self) -> Result<(), String> {
if self.status != "approved" {
return Err(String::from("not approved"));
}
self.status = String::from("published");
Ok(())
}
}
}
Everything about that is checkable only at runtime, the status strings are
uncontrolled, and nothing stops a second publish.
Chapter 3 mentioned that a method taking self by value consumes the receiver.
That is the tool here: make each stage its own type, and make the transitions
consume the previous stage so it cannot be used again.
pub struct Draft {
body: String,
}
pub struct InReview {
body: String,
reviewer: String,
}
pub struct Published {
body: String,
}
impl Draft {
pub fn new(body: String) -> Draft {
Draft { body }
}
pub fn submit(self, reviewer: String) -> InReview {
InReview { body: self.body, reviewer }
}
}
impl InReview {
pub fn approve(self) -> Published {
println!("approved by {}", self.reviewer);
Published { body: self.body }
}
pub fn reject(self) -> Draft {
Draft { body: self.body }
}
}
impl Published {
pub fn body(&self) -> &str {
&self.body
}
}
fn main() {
let article = Draft::new(String::from("how to think in ownership"));
let published = article.submit(String::from("editor")).approve();
println!("{}", published.body());
}
There is no status field, no check, and no Result on any transition, because
none of them can fail. publish is not a method on Draft at all, so calling it
too early is not a runtime error — it is a compile error saying no such method
exists. Publishing twice is impossible because approve consumed the InReview
and there is nothing left to call it on again. The state machine is enforced by
which methods exist on which type, which means the compiler checks the diagram.
The pattern is called typestate, and there is a more elaborate version using a
generic parameter and PhantomData that keeps the fields in one struct. The
version above, with a struct per state, is easier to read and is the one to reach
for first.
Note
Watch for this in the standard library and in crates you use, now that you know the shape. A
Veciterator consuming the vector, a builder’sbuildtakingself, a lock guard that releases on drop, aFileopened for reading having nowritemethod: all the same idea. Once you see it you will notice how much of Rust’s library surface is state machines encoded in which methods exist.
What this costs, and when not to bother
The technique has a real price and the price is paid in conversions. A program
with Email, CustomerId, Sku and Percentage types has more moving parts
than one that uses String, String, String and f64. Serialization needs
attention. Reading one out to concatenate with something means calling
as_str(). A newtype with no methods is annoying to use, so each one accumulates
a small surface of its own.
Three guides for where to spend it.
Spend it at boundaries. Data arriving from a network, a file, a database or a
user is untrusted and heterogeneous, and the boundary is where parsing belongs.
Inside the program, past the boundary, the narrow types are what make the rest of
the code simple. A function deep in the core that takes Email and Sku cannot
be called with the arguments in the wrong order, and that mistake is otherwise
invisible to every tool you own.
Spend it where the fact has many readers. A rule checked in one function and never again is fine as a check. A rule that six modules depend on wants to be a type, because six modules is six chances to forget.
Do not spend it on local intermediate values. A number computed on one line and consumed on the next does not need a newtype. The technique is about facts that travel; a value that never leaves the function has nowhere to travel to.
There is also a genuine limit. Some invariants cannot be expressed in types without more machinery than they are worth — that a discount never exceeds the subtotal, that two collections stay the same length, that a timestamp is in the past. For those, a private field and a method that upholds the rule is the right answer, and it is still better than a public field and a convention.
Conclusion
A type can represent states that a program considers illegal, and every one of those states is a bug waiting for a code path to produce it. Three moves shrink the gap. Fields that are only meaningful together belong in enum variants, which deletes the invalid combinations outright. A validating check belongs in a constructor that returns a narrower type, so the knowledge survives being passed along. A sequence of allowed operations belongs in a series of types whose transitions consume the previous one, so the order is checked at build time.
All three replace vigilance with structure, and the test for whether they are paying off is simple: count the checks that disappeared, and count the states that no longer have a spelling.
The next chapter goes back to the tool that does most of this work — the enum,
and the match that makes it safe to extend.
Questions to think through
Take a class you know with a status or state field. Count the fields whose meaning depends on that status, and write down which combinations of status and field are actually legal. How many representable states are there, and how many of those are legal?
The Email::parse function normalizes as well as validates. What would break in
a system that validated email addresses everywhere but normalized them nowhere?
The typestate example has no Result on any transition. Find an operation in
your own code that returns an error solely because it might be called at the
wrong time, and sketch what the types would have to be for that error to become
impossible.
Enums, Matching, and the Absence of Null
Tony Hoare introduced the null reference to ALGOL W in 1965 and called it his billion-dollar mistake in a talk forty-four years later, on the grounds that it had caused “innumerable errors, vulnerabilities, and system crashes”. The interesting part of his account is why he did it: it was easy to implement, and the type checker could not object.
That last clause is where the trouble starts. Null is a value that belongs to every
reference type while satisfying none of their contracts, so the type system
cannot distinguish a String from a maybe-String, and every piece of code that
handles one must be written as though it might be handling the other. The
discipline required is uniform vigilance, and uniform vigilance is not something
humans supply.
Rust has no null. What it has instead is an enum, and this chapter is about the enum and the pattern matching that makes it work — the mechanism that most distinguishes reading Rust from reading the languages you already know.
Match is the primary control structure
match takes a value and a set of patterns, finds the first pattern that fits,
binds any names in it, and evaluates the corresponding arm:
enum Command {
Quit,
Move { x: i32, y: i32 },
Say(String),
Repeat(Box<Command>, u32),
}
fn describe(command: &Command) -> String {
match command {
Command::Quit => String::from("quit"),
Command::Move { x, y } => format!("move to {x},{y}"),
Command::Say(message) => format!("say {message:?}"),
Command::Repeat(inner, times) => {
format!("{} times: {}", times, describe(inner))
}
}
}
fn main() {
let script = Command::Repeat(Box::new(Command::Say(String::from("hello"))), 3);
println!("{}", describe(&script));
}
Three things in that example are worth separating out.
A pattern destructures as it matches. Command::Move { x, y } both tests which
variant is present and pulls the fields out as local bindings, so there is no
separate cast and no accessor call. In a language with a sealed hierarchy you
would test the type, cast, and then read the fields; the pattern does all three
and cannot get them out of step.
A match is an expression. Every arm produces a value, the arms must agree on a
type, and the result is the value of the construct. That is why the
function has no return and no mutable accumulator.
And Command::Repeat holds a Box<Command>, which is how a recursive type is
written. A Command containing a Command directly would have no finite size,
so the recursion goes through a box, one pointer wide regardless of what
it points at. Every tree-shaped type in Rust — expression trees, JSON documents,
directory hierarchies — is built this way.
Exhaustiveness is a refactoring tool
The compiler requires that a match cover every possible value, and rejects one
that does not. On its own that prevents a forgotten case. What makes it
valuable, though, is what happens to a program when a variant is added.
Suppose Command gains a Wait(u32) variant. The describe function above no
longer compiles, and the error names the file, the line, and the variant that is
not handled. If the program has forty matches on Command, you get forty errors,
which is a complete work list, generated by the compiler, of every place in the
codebase that has to think about waiting.
Compare the same change in a language that switches on a type tag or a string. It compiles. The tests that cover the new case fail, the ones that do not, pass, and the missing branch surfaces in production some weeks later in a module nobody associated with the change. The difference is not that Rust programmers are more careful. It is that adding a variant is a mechanical, compiler-verified refactor in one language and an archaeology exercise in the other.
This leads directly to a rule about the wildcard pattern. _ matches anything,
which you want sometimes and which also switches exhaustiveness off:
#![allow(unused)]
fn main() {
enum Command { Quit, Move { x: i32, y: i32 }, Say(String) }
fn is_quit(command: &Command) -> bool {
match command {
Command::Quit => true,
_ => false,
}
}
}
That is fine, because the question really is “quit or not” and a new variant
should indeed answer false. But a _ arm in a function that genuinely needs to
handle each case is a silent opt-out from the work list, and it is how a
carefully designed enum stops paying for itself. The habit worth building is to
ask, each time you write _, whether a future variant should fall into it. When
the answer is no, list the variants explicitly and let the compiler find you
later.
Note
For the “is it this one” question specifically,
matches!is shorter and makes the intent clearer than a two-armed match:matches!(command, Command::Quit). It takes patterns, including guards, somatches!(n, 1..=9)works too.
Patterns are a small language
Patterns appear in match, in if let, in let, and in function parameters,
and they are more capable than the examples so far suggest.
#[derive(Debug)]
enum Event {
Key { code: u8, shift: bool },
Click { x: i32, y: i32, button: u8 },
Scroll(i32),
}
fn interpret(event: &Event) -> String {
match event {
Event::Key { code: 27, .. } => String::from("escape"),
Event::Key { code, shift: true } if *code >= b'a' && *code <= b'z' => {
format!("shifted {}", (*code as char).to_ascii_uppercase())
}
Event::Key { code, .. } => format!("key {code}"),
Event::Click { button: 1 | 2, x, y } => format!("click at {x},{y}"),
Event::Click { button, .. } => format!("unknown button {button}"),
Event::Scroll(amount @ i32::MIN..=-1) => format!("scroll up {}", -amount),
Event::Scroll(0) => String::from("no scroll"),
Event::Scroll(amount) => format!("scroll down {amount}"),
}
}
fn main() {
println!("{}", interpret(&Event::Key { code: 27, shift: false }));
println!("{}", interpret(&Event::Click { x: 10, y: 20, button: 1 }));
println!("{}", interpret(&Event::Scroll(-3)));
}
A literal in a field position matches only that value. .. ignores the rest of
the fields. A guard after if adds a condition the pattern itself cannot express.
| matches alternatives. A range pattern matches a span of values. And name @ pattern binds the matched value to a name while still constraining it, so
amount is available in an arm that also restricts its range.
Patterns nest, so a Some(Event::Click { button: 1, .. }) is a single pattern,
and reaching three or four levels into a structure in one line is ordinary Rust.
This is the feature that most changes how code looks. Logic that would be a
staircase of type tests and null checks in another language flattens into a table
of shapes and their responses.
Option, and deciding at the point of use
Option<T> is an ordinary enum — Some(T) or None — with no special
privileges. It replaces null by being a different type from T, so the compiler
knows which one you are holding and will not let you use a maybe-value as a
value.
The consequence is that absence has to be handled where it is introduced rather than surfacing later as a crash. What you write at that point depends on what absence means there, and the standard library provides a vocabulary so that the answer is usually one short line:
| When absence means | Write |
|---|---|
| substitute a value | unwrap_or(default), unwrap_or_else(|| compute()), unwrap_or_default() |
| transform if present | map(|v| ...), and_then(|v| ...) for a function that also returns Option |
| becomes an error | ok_or(err), ok_or_else(|| err()) |
| narrows further | filter(|v| ...) |
| do something if present | if let Some(v) = opt |
| stop early | let Some(v) = opt else { return ... }; or ? in a function returning Option |
| cannot happen | expect("why it cannot happen") |
Put together, these let a chain of fallible steps read as one expression:
use std::collections::HashMap;
fn display_name(directory: &HashMap<u32, String>, id: u32) -> String {
directory
.get(&id)
.map(|name| name.trim())
.filter(|name| !name.is_empty())
.map(|name| name.to_uppercase())
.unwrap_or_else(|| format!("user {id}"))
}
fn main() {
let mut directory = HashMap::new();
directory.insert(1, String::from(" ada "));
directory.insert(2, String::from(" "));
println!("{}", display_name(&directory, 1));
println!("{}", display_name(&directory, 2));
println!("{}", display_name(&directory, 3));
}
Three different ways of having no name — missing key, blank string, unknown id — collapse into one path, and none of them can produce a crash further down. The equivalent written with null checks is a dozen lines and has a hole in it.
let else deserves a separate mention because it is the shape that most improves
ordinary imperative code:
fn parse_port(raw: &str) -> Option<u16> {
let Ok(port) = raw.parse::<u16>() else {
return None;
};
if port < 1024 { None } else { Some(port) }
}
fn main() {
println!("{:?} {:?}", parse_port("8080"), parse_port("80"));
}
The binding stays in scope for the rest of the function rather than being trapped
inside an if let block, so guard clauses stay flat instead of marching to the
right.
The unwrap question
unwrap takes the value out of a Some and panics on None. expect does the
same with a message. Both are the escape hatch, and how you feel about them is a
reasonable proxy for how much Rust you have written.
Newcomers use them everywhere, because they make the compiler stop asking questions. That produces a program that crashes exactly where a null-dereferencing program would, having spent the ceremony and collected none of the benefit.
The overcorrection is to treat them as forbidden, which produces elaborate handling for cases that cannot occur and error types with variants that are never constructed.
The line is whether you can state, in one sentence, why None is impossible
here — and whether that sentence is a fact about the code rather than a hope. A
regular expression compiled from a literal that you wrote and that the tests
exercise will parse; expect("literal pattern is valid") is a statement of that.
An element you inserted two lines ago is present. In tests, a failed unwrap is a
failed test, and that is the correct outcome.
What fails the line is anything involving input, configuration, the network, the
filesystem or another process. Those are the cases the type was drawing your
attention to, and unwrap there is a decision to crash rather than an argument
that crashing is impossible.
Prefer expect to unwrap whenever you do reach for one. The message costs
nothing and turns a panic that says called Option::unwrap() on a None value at
some line number into one that says why the author thought it could not happen —
which is precisely the information the person debugging it needs.
Enums carry behavior too
An enum is a type, so it has impl blocks like any other, and putting behavior
on the enum rather than in a free function keeps the match next to the thing it
matches on:
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
Kelvin(f64),
}
impl Temperature {
fn as_celsius(&self) -> f64 {
match self {
Temperature::Celsius(degrees) => *degrees,
Temperature::Fahrenheit(degrees) => (degrees - 32.0) * 5.0 / 9.0,
Temperature::Kelvin(degrees) => degrees - 273.15,
}
}
fn is_freezing(&self) -> bool {
self.as_celsius() <= 0.0
}
}
fn main() {
let readings = [
Temperature::Fahrenheit(28.0),
Temperature::Kelvin(300.0),
Temperature::Celsius(12.5),
];
for reading in &readings {
println!("{:.1}C freezing={}", reading.as_celsius(), reading.is_freezing());
}
}
There is a design tension here worth naming, because it is the one place where
enums and traits genuinely compete. Adding a variant to an enum means touching
every match; adding an operation means one new method in one place. With a
trait and a set of implementing types it is reversed: a new type is one new
impl and touches nothing, while a new operation means editing every
implementation.
So the choice follows the axis you expect to grow. A closed set of cases with operations that keep accumulating — a syntax tree, a protocol message, a set of states — wants an enum. An open set of implementations sharing a stable set of operations — a plugin, a storage backend, a renderer — wants a trait. Chapter 9 takes up the other half.
Conclusion
Null exists because a type system was allowed to look away. Rust’s replacement is
an ordinary enum, which the type system cannot look away from, and the machinery
that makes ordinary enums pleasant to use — destructuring patterns, guards,
ranges, exhaustiveness checking — is the same machinery that makes Option and
Result practical rather than tedious.
Exhaustiveness is the feature with the longest reach. It turns adding a case from an exercise in remembering into a compiler-generated work list, and the price of keeping that property is being deliberate about the wildcard pattern.
For absence specifically, the thought to build is that None is a case to be
answered at the point where it appears, and that the standard library has a short
way to write nearly every answer. unwrap is one of those answers, appropriate
when you can say why the absent case cannot occur, and a decision to crash when
you cannot.
The next chapter takes the same apparatus and applies it to failure, the other thing every program has to represent and most languages represent badly.
Questions to think through
Pick a switch statement in code you know that dispatches on a type tag or a string constant. What happens today when someone adds a new case and misses that switch? What would have to be true for the compiler to find it?
The chapter claims _ silently opts out of exhaustiveness checking. Find a
default: branch in a language you use and decide whether a newly added case
falling into it would be correct or a bug.
Take three unwrap-shaped operations from your own code — a cast, a lookup, a
parse. For each, write the sentence explaining why failure is impossible. Which
sentences are about the code, and which are about what you expect the world to
do?
Errors as Values
Every program deals with two categories of bad news, and most languages give them the same mechanism, which is why error handling is such a mess in practice.
The first category is expected failure. A file might not exist. A user might type letters into a number field. A network might be down. None of these is a defect; they are conditions the program was written to encounter, and handling them is ordinary work that belongs in the design.
The second category is defects. An index past the end of an array. An invariant the code itself broke. An assertion that should have been true. These are not conditions to handle — they are evidence that the program is wrong, and the best response is usually to stop before the damage spreads.
Rust separates them at the language level. Expected failure is a value of type
Result<T, E>, returned and handled like any other value. Defects are panics,
which unwind the stack and end the thread. This chapter is mostly about the
first, and about the fact that putting failure in the return type changes how you
design a program.
Why not exceptions
The case against exceptions is not that they are ugly. It is that they are invisible.
Look at a Java or Python or C# method signature and you cannot tell what it does
when things go wrong. In Java, checked exceptions were an attempt to fix that,
and they failed for a social reason rather than a technical one: the
compiler forced a decision at every call site, the cheapest decision was
catch (Exception e) {} or adding throws to the caller and pushing the problem
up, and within a decade the ecosystem had settled on unchecked exceptions and
convention. Modern Java libraries throw runtime exceptions and document them,
which is to say they do not document them.
The deeper cost is to control flow. Any line in a function with a try block may
be the last one executed, which means the code between two statements is not
guaranteed to run, which means every piece of cleanup has to go in a finally or
be handled by a scope guard. Rust does not need those, because Drop handles
cleanup structurally, and it does not need try to see the branch, because the
branch is in the type.
use std::num::ParseIntError;
fn parse_age(raw: &str) -> Result<u8, ParseIntError> {
raw.trim().parse::<u8>()
}
fn main() {
match parse_age(" 41 ") {
Ok(age) => println!("age {age}"),
Err(error) => println!("bad age: {error}"),
}
}
The signature says this can fail and says what the failure looks like. A reader skimming the module sees which functions are fallible without reading any bodies, and a reviewer can tell whether a new failure mode was added by looking at the diff of the signature.
The question mark
Handling every error with a match would be unbearable, and early Rust was
unbearable for that reason. The ? operator is what makes error-as-value
practical: it unwraps an Ok, and on Err it returns from the enclosing
function with that error, converting it on the way out.
use std::num::ParseIntError;
fn sum_numbers(lines: &str) -> Result<i64, ParseIntError> {
let mut total = 0;
for line in lines.lines() {
let value: i64 = line.trim().parse()?;
total += value;
}
Ok(total)
}
fn main() {
println!("{:?}", sum_numbers("1\n2\n3"));
println!("{:?}", sum_numbers("1\ntwo\n3"));
}
The happy path reads like code with no error handling in it, and the failure path is one character per call. That is the ergonomic trade that makes the whole approach work: the cost of propagating an error is small enough that nobody is tempted to swallow it.
The conversion ? performs is the part worth understanding, because it is the
mechanism the rest of this chapter depends on. When ? returns an error, it
calls From::from on it to convert into the function’s declared error type. So a
function can call three libraries that each fail differently, and as long as its
own error type knows how to be built from each of theirs, ? does the plumbing.
Designing an error type
A module that can fail in several ways wants one enum naming those ways:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum ConfigError {
Missing { key: String },
NotANumber { key: String, source: ParseIntError },
OutOfRange { key: String, value: i64 },
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Missing { key } => {
write!(f, "required setting {key} is missing")
}
ConfigError::NotANumber { key, .. } => {
write!(f, "setting {key} must be a number")
}
ConfigError::OutOfRange { key, value } => {
write!(f, "setting {key} is {value}, which is out of range")
}
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::NotANumber { source, .. } => Some(source),
_ => None,
}
}
}
fn read_port(settings: &[(&str, &str)]) -> Result<u16, ConfigError> {
let (_, raw) = settings
.iter()
.find(|(key, _)| *key == "port")
.ok_or_else(|| ConfigError::Missing { key: String::from("port") })?;
let value: i64 = raw.trim().parse().map_err(|source| ConfigError::NotANumber {
key: String::from("port"),
source,
})?;
u16::try_from(value).map_err(|_| ConfigError::OutOfRange {
key: String::from("port"),
value,
})
}
fn main() {
println!("{:?}", read_port(&[("port", "8080")]));
for settings in [&[("host", "a")][..], &[("port", "eight")][..], &[("port", "99999")][..]] {
match read_port(settings) {
Ok(port) => println!("port {port}"),
Err(error) => println!("error: {error}"),
}
}
}
Four things in that are deliberate and worth copying.
The variants name failures in the vocabulary of the problem, not of the
implementation. NotANumber tells a caller something actionable; a variant
called ParseError wrapping whatever the parser said tells them to go read the
parser’s documentation.
Each variant carries the context needed to act. Knowing that a setting failed to parse is nearly useless without knowing which setting, and that information is available where the error is created and nowhere else. Errors that lose it get reported as “invalid configuration” and cost somebody an afternoon.
Display writes one line, lowercase, with no trailing period and no “Error:”
prefix, because the caller decides the presentation and will often be printing it
inside a larger sentence. Debug is derived and carries the structure for logs.
source links to the underlying error rather than folding its text into the
message. That gives the caller a chain it can walk — “failed to start server:
setting port must be a number: invalid digit found in string” — with each layer
adding its own context and none of them repeating the layer below.
Note
Writing
Display,Errorand theFromconversions by hand gets repetitive across a dozen error types. Thethiserrorcrate generates all of it from attributes on the enum, and is the standard answer for library error types. It is worth writing one by hand first, as above, so that what the macro generates is familiar rather than magical.
Libraries and applications want different things
This is the design decision people most often get wrong, and the two cases really do pull in opposite directions.
A library does not know what its caller will do with a failure, so it should say precisely what went wrong and let them decide. That means a concrete error enum, exhaustively matchable, with variants that distinguish cases a caller might handle differently. Returning an opaque or stringly-typed error from a library forecloses the caller’s options: they cannot retry only on timeouts, or fall back only when a file is missing, because they cannot tell those apart without parsing your prose.
An application usually has one policy for most failures — log it and exit,
or log it and return a 500 — and the cost of defining a variant for every way a
program can fail is not repaid. There the answer is a type that can hold any
error plus some context. Box<dyn std::error::Error> does this with nothing but
the standard library; the anyhow crate does it with better ergonomics and a
context method, and is what most binaries use.
The line between them is about who decides, and it moves. An application’s core domain logic, where the caller is other code in the same program that genuinely branches on failure, wants library-style typed errors. The outer shell of the same program, where everything ends in a log line, wants the boxed kind.
use std::error::Error;
fn parse_setting(raw: &str) -> Result<i64, Box<dyn Error>> {
let value: i64 = raw.trim().parse()?;
if value < 0 {
return Err("setting must not be negative".into());
}
Ok(value)
}
fn main() -> Result<(), Box<dyn Error>> {
println!("{}", parse_setting(" 42 ")?);
match parse_setting("-1") {
Ok(value) => println!("{value}"),
Err(error) => println!("error: {error}"),
}
Ok(())
}
main can return a Result, and an Err makes the process exit non-zero after
printing the error’s Debug form. For a small tool that is all the error
handling it needs.
Panics are for defects
panic! unwinds the current thread, running destructors as it goes, and by
default ends the process. It is not an exception and should not be used as one:
there is catch_unwind, but it exists for thread boundaries and test harnesses,
and using it for control flow is a way of writing a language other than Rust.
The rule for which mechanism to use is about who is at fault. If the failure came
from the outside world — input, a file, a network, a user — it is expected, and
it is a Result. If the failure means the code is wrong — an index out of
bounds, a None where the invariant said Some, arithmetic that overflowed a
type you chose — it is a panic, and the panic is doing you a favor by stopping
near the mistake.
The corollary matters for library authors. A library that panics on bad input
takes the decision away from its caller, who may have wanted to report the
problem and carry on. A library panics only on defects in its own code, or on
uses that violate a documented contract — slice[10] on a slice of three, for
instance, is a panic because the contract says indices must be in range.
Arithmetic overflow deserves a mention because it is the one that catches people.
In debug builds, integer overflow panics; in release builds, it wraps. Neither is
what you want silently, so when overflow is a real possibility the type’s methods
say what should happen: checked_add returns Option, saturating_add clamps,
wrapping_add wraps on purpose, and overflowing_add reports both.
Context is what makes an error useful
The error that arrives at a log file should let somebody fix the problem without reproducing it. That means each layer adds what it knows, because the layer below does not know it:
use std::fmt;
#[derive(Debug)]
struct StartupError {
stage: &'static str,
detail: String,
}
impl fmt::Display for StartupError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} failed: {}", self.stage, self.detail)
}
}
fn load_settings(path: &str) -> Result<String, StartupError> {
Err(StartupError {
stage: "loading settings",
detail: format!("no such file: {path}"),
})
}
fn main() {
match load_settings("/etc/app/config.toml") {
Ok(settings) => println!("{settings}"),
Err(error) => println!("startup: {error}"),
}
}
An error saying No such file or directory (os error 2) is nearly worthless,
because the one fact needed to act on it — which file — is the fact the operating
system did not include. Adding the path at the layer that knows it is a small
habit with a large return, and it is what anyhow’s .context("reading config")
exists to make convenient.
The opposite failure is worth naming too: an error chain in which every layer restates the layer below in slightly different words produces six lines of log saying the same thing. Add context when you have context to add.
What to avoid
Some patterns show up repeatedly in early Rust and are worth recognizing before you write them.
A String as an error type works and throws away every distinction a caller
could act on. It is acceptable in a prototype and in tests, and it is a
maintenance problem anywhere a caller might branch.
A single catch-all variant, Other(String), that slowly accumulates all the
cases nobody wanted to name, ends in the same place.
unwrap in library code converts a caller’s recoverable situation into a crash
in their process, which they cannot catch and did not agree to.
Swallowing an error with let _ = ... or an empty match arm is the same bug as
an empty catch block, made slightly more visible. Where ignoring really is
correct — a best-effort cleanup, say — write it so the next reader can see it was
a decision, with a comment or an explicit if let Err(e) = ... { log(e) }.
Conclusion
Putting failure in the return type makes it visible in signatures, checkable by
the compiler, and cheap to propagate with ?. That visibility is the point:
error handling stops being an invisible second control flow layered over the
program and becomes part of the program’s design, subject to the same review as
everything else.
The design work is in the error type. A library wants an enum whose variants
distinguish the cases a caller might handle differently, each carrying the
context needed to act, with Display for humans and source for the chain. An
application wants one flexible type and context added at each layer. Panics are
reserved for defects, where stopping near the mistake is the most useful thing
the program can do.
The next chapter turns to traits, which are how a type declares what it can do — including, as it happens, how it reports errors.
Questions to think through
Take a method you have written that throws. Can a caller tell from the signature what it throws, and can they distinguish the failure they want to retry from the one they want to report?
The chapter argues that a library should return typed errors and an application should not. Find a piece of code you own that sits on that line and decide which side it is on. Who consumes its failures?
Pick an error message from a log you have had to debug. What was the one fact that would have made it actionable, and which layer of the program knew it?
Traits: Contracts, Not Hierarchies
Rust has no inheritance. No struct extends another struct, there is no base
class, no super, no overriding, and no protected members. For a programmer
arriving from Java, C# or Python this is the most disorienting fact about the
language, and the usual first reaction is to ask how anything gets reused.
The answer begins by noticing that inheritance was never one feature. It was four features that happened to share a keyword.
It provided subtyping: a Dog can be used where an Animal is expected. It
provided code reuse: Dog gets Animal’s method bodies without rewriting
them. It provided interface conformance: Dog is guaranteed to have everything
Animal has. And it provided a shared construction path: Dog’s constructor
runs Animal’s first.
Bundling those was convenient and it is why the fragile base class problem exists, why “prefer composition over inheritance” has been standard advice since 1994, and why every serious object-oriented codebase eventually contains a class hierarchy that nobody dares change. Rust unbundles them. Interface conformance goes to traits. Code reuse goes to composition, default methods and generics. Construction goes to ordinary functions. Subtyping mostly disappears, and where it is genuinely wanted, trait objects provide it.
This chapter is about the trait, which carries the largest share of the load.
A trait is a set of capabilities
A trait names some operations. A type declares it has them by implementing the trait:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &'static str;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn name(&self) -> &'static str {
"circle"
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> &'static str {
"rectangle"
}
}
fn report(shape: &impl Shape) {
println!("{} has area {:.2}", shape.name(), shape.area());
}
fn main() {
report(&Circle { radius: 1.0 });
report(&Rectangle { width: 2.0, height: 3.0 });
}
Circle and Rectangle have no relationship to each other and no shared
ancestor. Neither contains any part of the other. What they have in common is
that both satisfy Shape, and report asks for that and nothing more.
The difference from an interface in Java or C# is smaller than the difference from a base class, but there are two, and both matter.
The first is that implementations are written separately from the type, which means you can implement your own trait for a type you did not define. This is how the standard library’s types acquire behavior from your crate:
trait Pluralize {
fn pluralize(&self, count: usize) -> String;
}
impl Pluralize for str {
fn pluralize(&self, count: usize) -> String {
if count == 1 {
format!("1 {self}")
} else {
format!("{count} {self}s")
}
}
}
fn main() {
println!("{}", "invoice".pluralize(1));
println!("{}", "invoice".pluralize(4));
}
str is defined in the standard library and now has a method defined in this
crate. No wrapper, no static helper, no extension-method ceremony.
The second difference is default methods, which let a trait supply behavior rather than only requiring it.
Default methods are where reuse went
A trait method may have a body. Implementors get it for free and may override it:
trait Greeter {
fn name(&self) -> String;
fn greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
fn shout(&self) -> String {
self.greeting().to_uppercase()
}
}
struct Person {
first: String,
}
impl Greeter for Person {
fn name(&self) -> String {
self.first.clone()
}
}
struct Robot;
impl Greeter for Robot {
fn name(&self) -> String {
String::from("unit 7")
}
fn greeting(&self) -> String {
format!("GREETINGS {}", self.name())
}
}
fn main() {
println!("{}", Person { first: String::from("Ada") }.shout());
println!("{}", Robot.shout());
}
Person implements one method and receives two. Robot overrides one of the
defaults and the other default picks up the override, because shout calls
greeting through the trait.
That last sentence describes the template method pattern, which in an object-oriented language requires an abstract base class with a final method calling abstract ones. Here it is a trait with a default method calling a required one, and it carries none of inheritance’s baggage: no shared fields, no constructor chain, no fragile base class, and a type can do this with as many traits as it likes.
Notice what a trait cannot do, because it is a real limitation. A trait has
no fields. It can require methods, including accessor methods, but it cannot
say “every implementor has a name: String”. Shared data is not what traits
are for. Chapter 11 takes that up.
The orphan rule
There is one restriction on implementing traits, and it exists to prevent a
specific disaster. You may write impl Trait for Type only if the trait is
yours, or the type is yours, or both. Implementing somebody else’s trait for
somebody else’s type is forbidden.
The reason is coherence. If two crates could both write impl Display for Vec<u8>, a program depending on both would have two answers for the same method
call, and which one it got would depend on link order. Every language with
after-the-fact extension has to deal with this; Scala uses scoped implicits, C#
resolves at compile time by using directive, and Rust’s answer is that exactly
one crate can own any given trait-type pairing, decided by a rule that requires
no configuration.
When you need to implement a foreign trait for a foreign type, the newtype from chapter 5 is the way through:
use std::fmt;
struct Csv(Vec<String>);
impl fmt::Display for Csv {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.join(","))
}
}
fn main() {
println!("{}", Csv(vec![String::from("a"), String::from("b")]));
}
Csv is your type, so you may implement anything for it.
The standard traits are a shared vocabulary
Rust’s libraries interoperate to an unusual degree, and the reason is that the standard library defines a small set of traits that everything agrees to speak. Learning them is most of learning to write idiomatic Rust, because implementing the right one gives your type access to machinery written by people who never heard of it.
Debug and Display are the two formatting traits: Debug for programmers,
derived, shown by {:?}, and Display for users, written by hand, shown by
{}. Any type with Display gets to_string free.
From and Into are the conversion traits, and the convention is to implement
From; Into comes automatically through a blanket implementation. This is the
trait ? uses to convert error types, and implementing From<io::Error> for
your error enum is what lets ? work across a library boundary.
Default supplies a zero-argument constructor and is what unwrap_or_default
and struct update syntax rely on.
PartialEq, Eq, PartialOrd and Ord are equality and ordering. Hash makes
a type usable as a map key. Deriving these together is so common that a type
meant to be a key usually derives all of them at once.
Iterator is the largest of them, and implementing its single required method
grants about seventy adapters. Chapter 13 is about that.
AsRef and Borrow express “can be viewed as”, letting a function
accept String, &str, PathBuf and &Path through one parameter.
Send and Sync are thread-safety markers the compiler implements for you.
Chapter 14 is about what they mean.
Deref deserves a warning. It makes one type usable as another by automatic
dereferencing, which is how String gets all of &str’s methods. It is designed
for smart pointers — Box, Rc, MutexGuard — and using it on a newtype to
inherit the inner type’s methods is a documented anti-pattern. It produces
something that looks like inheritance, behaves like inheritance in the good case,
and fails confusingly in the bad one, because method resolution and trait
resolution do not follow the same path.
Coming from Java or C#
FromandIntoreplace conversion constructors and cast operators.DisplayreplacestoString.PartialEqreplacesequals, with the difference that the compiler writes it and that there is no inherited default comparing identity.Defaultreplaces the no-argument constructor. The pattern to notice is that each of these is a separate opt-in, so a type never silently inherits a wrong implementation of anything.
Bounds: asking for capability
A generic function says which capabilities it needs, and the compiler checks that callers supply them:
use std::fmt::Display;
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut best = items[0];
for &item in items {
if item > best {
best = item;
}
}
best
}
fn announce<T>(label: &str, value: T)
where
T: Display,
{
println!("{label}: {value}");
}
fn main() {
println!("{}", largest(&[3, 7, 2]));
println!("{}", largest(&[2.5, 1.0]));
announce("total", 42);
announce("name", "anderix");
}
T: PartialOrd + Copy is a contract: any type that can be compared and copied
may be passed. The where clause is the same thing with better formatting for
longer lists, and is the conventional form once there is more than one bound.
Read a bound as a requirement rather than as a category. T: Display does not
say that T is a kind of displayable thing in some hierarchy; it says the
function will call fmt on it and needs that to exist. This is why bounds
compose so freely — asking for three capabilities is just asking for three
things, and there is no diamond to resolve.
Supertraits are not inheritance
A trait can require another trait:
use std::fmt::Display;
trait Loggable: Display {
fn log_level(&self) -> &'static str;
fn log_line(&self) -> String {
format!("[{}] {}", self.log_level(), self)
}
}
struct Alert(String);
impl Display for Alert {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Loggable for Alert {
fn log_level(&self) -> &'static str {
"WARN"
}
}
fn main() {
println!("{}", Alert(String::from("disk almost full")).log_line());
}
Loggable: Display means an implementor must also implement Display, which
lets log_line use {} on self. It looks like extends and is doing
something different: it composes requirements rather than inheriting
implementation, Alert had to write its own Display, and no data is shared.
Supertraits are how you build a small vocabulary of capabilities into a larger
one, and they do not produce a hierarchy of types — only of requirements.
Designing a trait
Two questions decide whether a trait is any good.
Who implements it, and how many of them are there? A trait with one implementor is usually not earning anything; the abstraction was introduced because it felt like good practice, and it costs a layer of indirection for nothing. Traits pay when there are several implementors, or when there is one now and a plugin boundary where others will arrive, or when a test needs a stand-in.
Is it one capability or several? Small traits compose and large ones do not. A
Storage trait requiring twelve methods forces every implementor to supply
twelve, including the ones they have no use for, which produces
unimplemented!() bodies and a contract that lies. Splitting it into Read,
Write and Seek — which is what the standard library did — lets a type
implement what it can actually do and lets a function ask for what it actually
needs.
The test for the second question is to look at the functions that take the trait as a bound. If most of them need two of the twelve methods, the trait is too big.
Conclusion
Inheritance bundled subtyping, code reuse, interface conformance and construction. Rust separates them, and the trait takes interface conformance plus a meaningful share of code reuse through default methods. What a trait cannot do is share fields, and that is composition’s job.
Implementations are written separately from types, so behavior can be added to foreign types, bounded only by the orphan rule that keeps any trait-type pairing owned by one crate. The standard traits form a vocabulary that makes unrelated libraries interoperate, and implementing the right ones is most of what makes a type feel native.
A bound is a requirement, not a classification, so capabilities compose without hierarchy. The next chapter is about what the compiler does with those bounds — the choice between generating specialized code for each type and dispatching through a pointer at runtime, and what that choice means for a design.
Questions to think through
Take an abstract base class from a system you know. Separate what it provides into the four jobs named at the start of this chapter. Which of the four was the reason it exists, and which came along for the ride?
The chapter says a trait with one implementor is usually not earning anything. Find an interface in your own code with exactly one implementing class. What would be lost by deleting it?
Deref makes one type usable as another, and using it for reuse is an
anti-pattern. Why would method resolution through Deref be a worse foundation
for reuse than a field and a few delegating methods?
Generics, Monomorphization, and Dynamic Dispatch
A trait says what a type can do. This chapter is about the two ways a program can use that fact, and about a choice that most languages make for you.
In Java, calling a method through an interface is a virtual call: the object carries a pointer to a table of method implementations, and the call goes through it at runtime. In C++, a template is expanded at compile time into a separate copy of the code for each type it is used with, and a virtual method is a table lookup, and the two mechanisms are entirely separate.
Rust has both and makes you pick, with a syntax difference of a few characters. Understanding what each one costs is the difference between writing code that is genuinely as fast as C and writing code that merely looks like it.
Generics are expanded, not shared
A generic function is a template. Nothing about it exists at runtime until it is used, and when it is used the compiler generates a copy specialized for the concrete types:
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("{}", largest(&[3, 7, 2]));
println!("{}", largest(&["pear", "apple"]));
}
The compiled binary contains two largest functions: one taking &[i32] and one
taking &[&str]. Neither is generic, neither looks at a method table, and the
comparison inside each is a direct call that the optimizer can inline. This
process is called monomorphization, and it is why generic Rust code has no
runtime penalty at all compared to code written by hand for one type.
The cost shows up elsewhere. Each instantiation is real code that has to be compiled and occupies space in the binary. A generic function used with fifteen types produces fifteen copies; a generic function that calls other generic functions multiplies. This is the main reason Rust compiles slowly and why binaries are larger than the equivalent C, and on a large project it is worth knowing about before it becomes a problem.
impl Trait in argument position is the same mechanism with less ceremony:
use std::fmt::Display;
fn announce(value: impl Display) {
println!("-> {value}");
}
fn announce_explicit<T: Display>(value: T) {
println!("-> {value}");
}
fn main() {
announce(42);
announce_explicit("forty-two");
}
Those two functions compile to the same thing. The impl Trait form is shorter
and the explicit form lets callers specify the type with turbofish syntax and
lets the same parameter appear twice, so use the short one until you need the
other.
In return position, impl Trait means something different and useful: the
function returns one specific type that the caller is not told the name of.
fn evens(limit: u32) -> impl Iterator<Item = u32> {
(0..limit).filter(|n| n % 2 == 0)
}
fn main() {
println!("{:?}", evens(10).collect::<Vec<_>>());
}
The actual return type is an unnameable compound of Filter and Range and a
closure. impl Iterator<Item = u32> lets the function keep that private while
the caller still gets static dispatch and full inlining. Every iterator chain you
return from a function will use this.
Trait objects are dispatched at runtime
The other mechanism is dyn Trait, which erases the concrete type:
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &'static str;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn name(&self) -> &'static str { "circle" }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn name(&self) -> &'static str { "square" }
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
Box::new(Circle { radius: 0.5 }),
];
let total: f64 = shapes.iter().map(|shape| shape.area()).sum();
for shape in &shapes {
println!("{} {:.3}", shape.name(), shape.area());
}
println!("total {total:.3}");
}
This is the thing generics cannot do. A Vec<T> holds one type; a
Vec<Box<dyn Shape>> holds a mixture, because each element is a pointer to a
value plus a pointer to that value’s method table. A reference to a trait object
is therefore two machine words rather than one, which is why you meet dyn Trait
almost always behind a Box, an Rc, or a reference — the value itself has no
size known at compile time.
The method table, or vtable, holds the function pointers for the trait’s methods plus the value’s size, alignment and destructor. A call through it is an indirect jump: cheap in absolute terms, and not inlinable, which is the real cost. A tight loop calling a trait object’s method a million times loses the optimizations that matter, and the same loop over a generic type keeps them.
Note
Box<dyn Error>from chapter 8 is a trait object, and so is every closure stored in a struct asBox<dyn Fn(i32) -> i32>. You have probably already used them without naming them.
Not every trait can be a trait object
Erasing the type means the compiler must be able to build a vtable, and some trait definitions make that impossible. The rules are called object safety, and in practice two of them account for almost everything you will hit.
A method that is generic over a type parameter cannot go in a vtable, because
there would have to be an entry for every type it might be called with, and that
set is unbounded. A method returning Self by value cannot either, because the
caller would not know how large the returned value is.
#![allow(unused)]
fn main() {
trait Spawn {
fn duplicate(&self) -> Self;
}
fn make_two(source: &dyn Spawn) {
let _copy = source.duplicate();
}
}
That fails with an object-safety error naming duplicate. The usual fix is to
move the offending method behind a where Self: Sized clause, which makes it
available on concrete types and excludes it from the object, or to return
Box<Self> instead of Self. Clone is the standard library’s example: it is
not object safe, which is why Box<dyn Trait> is awkward to clone and why
crates exist solely to work around it.
This matters at design time rather than at use time. A trait intended as an extension point should be checked for object safety early, because discovering that it cannot be one after twenty types implement it is an unpleasant refactoring.
Choosing between them
The decision is usually clear once you ask the right question, and the right question is not “which is faster”.
Do you need a collection, or a field, holding several different types? Then it is a trait object. Generics cannot express a heterogeneous container, and there is no amount of cleverness that changes that. A list of plugins, a stack of middleware, a vector of shapes, a set of event handlers: all trait objects.
Is the type known at the call site, in code that runs a great many times? Then it is generics. Sorting, numeric work, iterator pipelines, anything inside a hot loop. Inlining is where most of the performance comes from, and dynamic dispatch gives it up.
Is the trait crossing a boundary where the concrete types are not knowable? A plugin loaded at runtime, a callback registered by the user of your library, a dependency injected for testing: trait object, because there is no compile-time set to enumerate.
Is compile time or binary size hurting? Then dynamic dispatch is a lever. Generic code instantiated in a hundred places can often keep a generic outer layer for ergonomics while immediately converting to a trait object inside:
use std::fmt::Debug;
fn log_all(items: &[&dyn Debug]) {
for item in items {
println!("{item:?}");
}
}
fn log_one(item: &impl Debug) {
log_all(&[item]);
}
fn main() {
log_one(&42);
log_one(&"text");
}
log_one is generic and gets one small instantiation per type; log_all holds
the real body and is compiled once. For a function with a large body called with
many types, this can cut compile time noticeably and costs a few nanoseconds per
call. The standard library does this internally in several places.
When none of the above applies, and the code is not hot, either works. Start with generics — the default in Rust — and switch when you find a reason.
Associated types
A trait can declare a type as part of its contract rather than taking it as a parameter:
trait Repository {
type Item;
type Key;
fn get(&self, key: &Self::Key) -> Option<&Self::Item>;
fn put(&mut self, key: Self::Key, item: Self::Item);
}
use std::collections::HashMap;
struct UserRepo {
users: HashMap<u32, String>,
}
impl Repository for UserRepo {
type Item = String;
type Key = u32;
fn get(&self, key: &u32) -> Option<&String> {
self.users.get(key)
}
fn put(&mut self, key: u32, item: String) {
self.users.insert(key, item);
}
}
fn main() {
let mut repo = UserRepo { users: HashMap::new() };
repo.put(1, String::from("ada"));
println!("{:?}", repo.get(&1));
}
The distinction between an associated type and a type parameter is about how many times a type may implement the trait.
Use an associated type when each implementor has one natural answer.
Iterator has type Item because a given iterator yields one kind of thing; if
it were Iterator<T>, a type could implement it several times and every call to
next would be ambiguous.
Use a type parameter when a type should implement the trait repeatedly with
different types. From<T> is the example: a single error enum implements
From<io::Error> and From<ParseIntError> and as many more as it needs, and
being able to do so repeatedly is the reason it takes a parameter.
Getting this backwards produces traits that are painful in a way that is hard to diagnose, so the question — one answer per implementor, or many? — is worth asking explicitly when you write a trait.
Generic types and conditional behavior
Structs take type parameters too, and their impl blocks can add capability
conditionally:
use std::fmt::Display;
struct Pair<T> {
left: T,
right: T,
}
impl<T> Pair<T> {
fn new(left: T, right: T) -> Pair<T> {
Pair { left, right }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn announce_larger(&self) {
if self.left >= self.right {
println!("larger is {}", self.left);
} else {
println!("larger is {}", self.right);
}
}
}
fn main() {
Pair::new(3, 8).announce_larger();
Pair::new("b", "a").announce_larger();
}
Every Pair<T> has new. Only a Pair<T> whose T can be displayed and
compared has announce_larger. A Pair<Socket> would compile fine and simply
not have that method, and the error for calling it names the missing bound.
This is a genuinely different capability from anything in a class-based language,
where a class either has a method or does not. It is how the standard library
gives Vec<T> a sort only when T: Ord, and Option<T> a copied only when
T: Copy, and it is worth reaching for when a type’s behavior really does depend
on what it contains.
Conclusion
Generics are expanded at compile time into one specialized copy per type, so they cost nothing at runtime and cost compile time and binary size instead. Trait objects erase the type behind a pointer pair, so they cost an indirect call and the inlining it prevents, and they buy the one thing generics cannot do: holding several different types behind one interface.
The choice follows the shape of the problem rather than a general preference. Heterogeneous collections and runtime extension points need trait objects. Hot paths with known types want generics. A large generic body called from many places can be both: a thin generic shell over a single dynamically dispatched implementation.
Associated types and type parameters look similar and answer different questions: one answer per implementor, or many. The next chapter puts traits and composition together and works through what happens to an inheritance hierarchy when it is rewritten in a language that has neither base classes nor a way to miss them.
Questions to think through
A generic function used with fifteen types produces fifteen copies. Find a generic or template function in a codebase you know that is called with many types. Would you rather have the speed or the compile time, and how would you find out which you are getting?
Clone is not object safe, so Box<dyn Trait> cannot be cloned in general.
Reason out why: what would the vtable entry for clone have to return, and why
can the caller not handle it?
Pick an interface you have defined with a generic method. Could it be a trait object in Rust? If not, what would you change about it, and would the change make it better or only make it legal?
Designing with Composition
“Favor composition over inheritance” has been the standard advice since the Gang of Four wrote it down in 1994, and it has been ignored for thirty years for an understandable reason: in a language with inheritance, inheritance is easier. It is one keyword. Composition means declaring a field, writing delegating methods, and deciding what to expose, which is more typing on the first day and better on every day after.
Rust removes the easy option. There is no inheritance to fall back on, so every design that would have been a hierarchy has to be something else. This chapter is about what that something else is, and it is the most direct answer this book gives to The Object-Oriented Thought Process, whose two longest chapters are about inheritance and composition and how to choose.
What a hierarchy was doing
Take a small hierarchy of the kind that appears in every business system.
An abstract Notifier holds a recipient and a retry count, provides a concrete
send that formats a message and calls an abstract deliver, and is extended by
EmailNotifier and SmsNotifier, each supplying deliver. Perhaps
SmsNotifier also overrides format to truncate.
Three separate things are happening, and untangling them is the work.
The recipient and retry count are shared data. Every notifier has them, and they are identical in every subclass.
The send method is shared behavior built on a hole. It is the same for
everyone, and it calls something each subclass supplies.
deliver is varying behavior — the reason there is a hierarchy at all.
Rust handles those three with three different constructs, and the resulting design is clearer than the original because the three are visible rather than merged.
Shared data is a field
#![allow(unused)]
fn main() {
struct Recipient {
name: String,
address: String,
}
struct DeliverySettings {
retries: u8,
timeout_seconds: u32,
}
}
Data that several types have in common goes in a struct that those types hold. Not inherited, held. The relationship is has-a, and it is written out:
#![allow(unused)]
fn main() {
struct Recipient { name: String, address: String }
struct DeliverySettings { retries: u8, timeout_seconds: u32 }
struct Notification {
recipient: Recipient,
settings: DeliverySettings,
body: String,
}
}
The cost compared to inheritance is one extra level of naming: notification.recipient.name
rather than notification.name. The benefit is that Recipient is now a type,
which means it can be validated once, passed on its own, tested on its own, and
reused by things that are not notifications at all. Most of the time the extra
level is also more accurate — a notification is not a person, and writing
notification.name always was a small lie about what the field meant.
Varying behavior is a trait
The part that differed between subclasses becomes a trait with one method:
#![allow(unused)]
fn main() {
struct Recipient { name: String, address: String }
trait Channel {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String>;
fn label(&self) -> &'static str;
}
struct Email {
smtp_host: String,
}
struct Sms {
gateway_id: u32,
}
impl Channel for Email {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String> {
println!("[{}] to {} <{}>: {}", self.smtp_host, to.name, to.address, body);
Ok(())
}
fn label(&self) -> &'static str {
"email"
}
}
impl Channel for Sms {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String> {
let truncated: String = body.chars().take(160).collect();
println!("[gateway {}] to {}: {}", self.gateway_id, to.address, truncated);
Ok(())
}
fn label(&self) -> &'static str {
"sms"
}
}
}
Note that Email and Sms carry their own configuration. Under inheritance, an
smtp_host field on EmailNotifier and a gateway_id on SmsNotifier sit
alongside the inherited recipient and retry count in one object, and nothing
distinguishes the channel’s configuration from the message’s. Here they are
separate types and the separation is visible.
Shared behavior with a hole in it
The send method — same for everyone, calling into the part that varies — has
two good homes in Rust, and choosing between them is a real decision.
The first is a default method on the trait, which is the direct translation of a template method:
#![allow(unused)]
fn main() {
struct Recipient { name: String, address: String }
trait ChannelBase { fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String>; }
trait Channel {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String>;
fn send_with_retry(&self, to: &Recipient, body: &str, retries: u8) -> Result<(), String> {
let mut attempt = 0;
loop {
match self.deliver(to, body) {
Ok(()) => return Ok(()),
Err(error) if attempt < retries => {
attempt += 1;
println!("retry {attempt} after: {error}");
}
Err(error) => return Err(error),
}
}
}
}
}
The second is a separate type that holds a channel and adds the behavior around it:
#![allow(unused)]
fn main() {
struct Recipient { name: String, address: String }
trait Channel { fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String>; fn label(&self) -> &'static str; }
struct Notifier {
channel: Box<dyn Channel>,
retries: u8,
}
impl Notifier {
fn new(channel: Box<dyn Channel>, retries: u8) -> Notifier {
Notifier { channel, retries }
}
fn send(&self, to: &Recipient, body: &str) -> Result<(), String> {
let mut attempt = 0;
loop {
match self.channel.deliver(to, body) {
Ok(()) => return Ok(()),
Err(error) if attempt < self.retries => {
attempt += 1;
println!("retry {attempt} on {}: {error}", self.channel.label());
}
Err(error) => return Err(error),
}
}
}
}
}
The default-method version keeps everything in one trait and gives the behavior to every implementor automatically. The wrapper version separates the retry policy from the delivery mechanism entirely, which means the policy can be tested against a stub channel, swapped for a different policy, or applied to channels that were written without knowing it exists.
The wrapper is usually the better design, and the reason is worth stating plainly:
retrying is not something a channel is, it is something done to a channel.
Putting it on the trait makes every implementor carry a policy decision that
belongs to the caller. Default methods are best kept for behavior that genuinely
belongs to the capability — convenience methods derived from the required ones,
like Iterator’s adapters — rather than for policy.
Putting the pieces together:
struct Recipient {
name: String,
address: String,
}
trait Channel {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String>;
fn label(&self) -> &'static str;
}
struct Email;
struct Sms;
impl Channel for Email {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String> {
println!("email to {} <{}>: {}", to.name, to.address, body);
Ok(())
}
fn label(&self) -> &'static str { "email" }
}
impl Channel for Sms {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String> {
println!("sms to {}: {}", to.address, &body[..body.len().min(160)]);
Ok(())
}
fn label(&self) -> &'static str { "sms" }
}
struct Notifier {
channel: Box<dyn Channel>,
retries: u8,
}
impl Notifier {
fn send(&self, to: &Recipient, body: &str) -> Result<(), String> {
println!("(via {}, up to {} retries)", self.channel.label(), self.retries);
self.channel.deliver(to, body)
}
}
fn main() {
let recipient = Recipient {
name: String::from("Ada"),
address: String::from("ada@example.com"),
};
for channel in [Box::new(Email) as Box<dyn Channel>, Box::new(Sms)] {
let notifier = Notifier { channel, retries: 2 };
let _ = notifier.send(&recipient, "your invoice is ready");
}
}
Four small types, each of which can be understood, tested and changed alone. The hierarchy it replaces had two classes and one of them was abstract.
When the set is closed, use an enum
Trait objects are the right answer when new implementations may arrive from anywhere. When the set of alternatives is fixed and known, an enum is simpler and gives you exhaustiveness checking back:
#![allow(unused)]
fn main() {
struct Recipient { name: String, address: String }
enum Channel {
Email { smtp_host: String },
Sms { gateway_id: u32 },
Webhook { url: String, secret: String },
}
impl Channel {
fn deliver(&self, to: &Recipient, body: &str) -> Result<(), String> {
match self {
Channel::Email { smtp_host } => {
println!("[{smtp_host}] {} {body}", to.address);
Ok(())
}
Channel::Sms { gateway_id } => {
println!("[sms {gateway_id}] {} {body}", to.address);
Ok(())
}
Channel::Webhook { url, .. } => {
println!("[POST {url}] {body}");
Ok(())
}
}
}
}
}
No boxing, no dynamic dispatch, no trait, and adding a fourth channel produces a compile error at every match that needs updating. Chapter 7 laid out the trade: enums make adding operations easy and adding cases expensive, traits the reverse. For a set of channels that ships with the application, the enum is usually right. For a set that third parties extend, it cannot be.
Coming from an object-oriented language
The instinct that an enum is a primitive thing and a class hierarchy is the grown-up solution is worth examining. A sealed hierarchy with a visitor is a laborious reconstruction of what an enum and a match do directly, and the reason Java grew sealed classes and pattern matching in recent versions is that the reconstruction was not paying.
Delegation is explicit, and that is the point
Composition’s reputation for verbosity comes from delegation. If Notifier holds
a Channel and callers need the label, Notifier writes:
#![allow(unused)]
fn main() {
trait Channel { fn label(&self) -> &'static str; }
struct Notifier { channel: Box<dyn Channel> }
impl Notifier {
fn label(&self) -> &'static str {
self.channel.label()
}
}
}
Three lines to forward one method, where inheritance would have given it for free. Rust offers no delegation syntax, and proposals for one have been debated for years.
The verbosity is a real cost and it buys something specific: the public surface
of Notifier is a list you wrote, rather than everything its parts happen to
expose. A class inheriting from a base class inherits its entire public interface,
including the methods that make no sense on the subclass, and the famous examples
— Stack extends Vector, so you can insert into the middle of a stack — are
this failure. With delegation, a method exists on the outer type because
someone decided it should.
In practice the amount of forwarding stays small, because a composed type usually exposes a different interface rather than the same one. If you find yourself forwarding fifteen methods unchanged, that is a signal: either the wrapper is not adding anything and callers should hold the inner type directly, or the wrapper wants to expose the inner type through an accessor and let callers use it.
What you should not do is reach for Deref to get the methods automatically. It
looks like it works and produces a type whose methods appear and disappear
depending on whether the call goes through method resolution or trait resolution,
which is a worse debugging experience than typing three lines.
Behavior as a parameter
The lightest form of composition is passing behavior in as a closure, and Rust programmers use it far more than an object-oriented programmer expects, because there is no ceremony:
struct Report {
rows: Vec<(String, i64)>,
}
impl Report {
fn render(&self, format_amount: impl Fn(i64) -> String) -> String {
self.rows
.iter()
.map(|(label, amount)| format!("{label}: {}", format_amount(*amount)))
.collect::<Vec<_>>()
.join("\n")
}
}
fn main() {
let report = Report {
rows: vec![
(String::from("subtotal"), 12_50),
(String::from("tax"), 1_06),
],
};
println!("{}", report.render(|cents| format!("${}.{:02}", cents / 100, cents % 100)));
println!("{}", report.render(|cents| cents.to_string()));
}
In an object-oriented design this would be a Formatter interface, one or two
implementing classes, and a constructor parameter. Here it is a closure. The
trait-and-implementations version is worth its weight when the behavior has state,
several methods, or needs a name for its own sake; when it is one function, a
closure says so.
Conclusion
Inheritance merged shared data, shared behavior and varying behavior into one construct. Composition separates them: shared data is a field of a held type, varying behavior is a trait, and shared behavior is either a default method on that trait or — usually better — a wrapper type that holds an implementation and adds something around it.
A closed set of alternatives is an enum, with exhaustiveness checking as the reward. An open set is a trait object. One function’s worth of varying behavior is a closure and needs no type at all.
Delegation has to be written out, which is the cost, and the return is that a composed type’s public interface is chosen rather than inherited. When the forwarding grows large, that is information about the design rather than a reason to look for a shortcut.
The next chapter deals with the case this one has been avoiding: designs where the ownership tree genuinely is not a tree, and several parts of a program need to share the same mutable thing.
Questions to think through
Take a class hierarchy you know with three or more levels. For each level, separate what it contributes into shared data, shared behavior, and varying behavior. Does any level contribute nothing but a name?
The chapter argues that retry policy belongs in a wrapper rather than on the channel trait. Find a method on a base class in your own code that is policy rather than capability. What would hold it instead?
Stack extends Vector exposes methods that break the stack’s contract. Find an
inherited method in your own code that should not exist on the subclass. What
currently prevents someone from calling it?
Shared Ownership and Interior Mutability
Chapter 2 claimed that the long-lived data in most programs forms a tree, and that a design wanting a graph is usually a design avoiding a decision. That is true often enough to be worth saying first, and it is not true always. Some structures are genuinely graphs. A scene graph where nodes know their parents. A cache several subsystems read and one updates. An interpreter’s environment chain. A GUI where a button needs to reach the model it edits.
For those, Rust provides shared ownership and interior mutability. This chapter is about what they are, what they cost, and the discipline that keeps them from becoming the way you write everything.
The discipline matters because these tools are the exit from the model the rest of the book describes. Reach for them when the design needs them and they are sound engineering. Reach for them the first time the borrow checker objects and you have reinvented the language you came from, with worse syntax and a runtime panic where the compile error used to be.
Rc: many owners, counted
Rc<T> is a reference-counted pointer. Cloning it produces another handle to the
same value and increments a count; dropping a handle decrements it; the value is
dropped when the count reaches zero.
use std::rc::Rc;
struct Config {
endpoint: String,
}
struct Client {
name: &'static str,
config: Rc<Config>,
}
fn main() {
let config = Rc::new(Config { endpoint: String::from("https://api.example.com") });
println!("count after creation: {}", Rc::strong_count(&config));
let clients = vec![
Client { name: "reader", config: Rc::clone(&config) },
Client { name: "writer", config: Rc::clone(&config) },
];
println!("count with two clients: {}", Rc::strong_count(&config));
for client in &clients {
println!("{} -> {}", client.name, client.config.endpoint);
}
drop(clients);
println!("count after dropping clients: {}", Rc::strong_count(&config));
}
Ownership is no longer a single place, so the destruction schedule is no longer readable off the source. That is the trade: you gave up knowing statically when the value dies in exchange for not having to decide who owns it.
Two conventions are worth adopting. Write Rc::clone(&x) rather than x.clone()
— they do the same thing, and the explicit form makes it visible in review that
this is a cheap refcount bump rather than a deep copy. And note that Rc is
single-threaded: it uses non-atomic increments, which is why it is fast and why
the compiler will not let you send one to another thread. The threaded version is
Arc, and chapter 14 covers it.
The restriction that matters for design is that Rc<T> gives you shared
access, which means read-only access. There is no Rc::get_mut unless you are
the only owner. This follows directly from the rule in chapter 3: many readers or
one writer, and Rc is how you get many readers.
Interior mutability moves the check to runtime
When you need many owners and mutation, you need a type that can hand out an
exclusive reference from behind a shared one. That inversion is called interior
mutability, and RefCell<T> is the general-purpose version:
use std::cell::RefCell;
fn main() {
let log = RefCell::new(Vec::new());
log.borrow_mut().push("started");
log.borrow_mut().push("working");
println!("{} entries: {:?}", log.borrow().len(), log.borrow());
}
borrow() returns a Ref<T>, which acts like &T. borrow_mut() returns a
RefMut<T>, which acts like &mut T. RefCell keeps a counter and enforces the
same rule the compiler would have: many shared borrows or one exclusive borrow.
The difference is when it is enforced. Break the rule and the program panics:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let reader = data.borrow();
let mut writer = data.borrow_mut();
writer.push(4);
println!("{reader:?}");
}
That panics with already borrowed: BorrowMutError. The bug is the same one
chapter 3 described, and it is now a runtime failure in production rather than a
build failure on your machine. That is the price of interior mutability, and it
is why RefCell is a tool for cases where the compiler genuinely cannot verify a
pattern that is nevertheless correct, rather than a way to stop it asking.
The guards are ordinary values with Drop implementations, so a borrow lasts
until its guard goes out of scope. Most RefCell panics come from holding one
longer than intended:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let total: i32 = data.borrow().iter().sum();
data.borrow_mut().push(total);
println!("{:?}", data.borrow());
}
Reading the sum into a local ends the shared borrow before the mutable one
begins. Writing data.borrow_mut().push(data.borrow().iter().sum()) compiles and
panics, because the argument’s borrow is still alive when borrow_mut runs. The
discipline is to keep borrows short and never to hold one across a call that
might borrow again.
Note
For a
Copytype that only needs get and set,Cell<T>is simpler and cannot panic: it moves values in and out rather than handing out references, so there is nothing to track. A counter, a flag, a cached length —Cell<u32>andCell<bool>do the job with no runtime bookkeeping at all.
The combination, and its reputation
Putting the two together gives Rc<RefCell<T>>: shared ownership of something
mutable. It is the shape newcomers converge on, because it makes Rust behave like
a language with references and mutable objects.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Account {
balance: i64,
}
fn main() {
let account = Rc::new(RefCell::new(Account { balance: 0 }));
let teller = Rc::clone(&account);
let auditor = Rc::clone(&account);
teller.borrow_mut().balance += 100;
teller.borrow_mut().balance -= 30;
println!("auditor sees {}", auditor.borrow().balance);
}
This works and is sometimes right. What makes it an anti-pattern when it is the default is that it gives up both guarantees at once: no static knowledge of who owns the value, and no static knowledge of who is writing to it. Everything chapters 2 and 3 bought is now checked at runtime, in exchange for a design you did not have to think about.
Before writing it, walk down the ladder. Can one place own the value, with
everyone else borrowing? Can the parts that need it take it in turn? Can the
thing be identified rather than pointed at, so that one owner holds the storage
and others hold identifiers? Can the mutation be gathered into one place — a
single update function that everyone calls — rather than distributed among
holders? Each of those is a real design, and each keeps the checks at compile
time. Rc<RefCell<T>> is what you use when all of them have been considered and
the structure really is a shared mutable graph.
Cycles leak
Reference counting has a known failure, and Rust does not fix it: two values that
hold Rc handles to each other keep each other’s count above zero, so neither is
ever dropped.
This is not undefined behavior and not a safety violation — Rust’s safety guarantee is about memory corruption, not about leaks — but it is still a bug, and the classic case is a tree whose children point back at their parents.
The tool is Weak<T>: a handle that does not contribute to the count and does
not keep the value alive. Getting at the value requires upgrade(), which
returns an Option<Rc<T>> and tells you whether the value is still there.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: String,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(name: &str) -> Rc<Node> {
Rc::new(Node {
name: String::from(name),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<Node>, child: Rc<Node>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
fn path(&self) -> String {
match self.parent.borrow().upgrade() {
Some(parent) => format!("{}/{}", parent.path(), self.name),
None => self.name.clone(),
}
}
}
fn main() {
let root = Node::new("root");
let docs = Node::new("docs");
let chapter = Node::new("ch01.md");
Node::add_child(&root, Rc::clone(&docs));
Node::add_child(&docs, Rc::clone(&chapter));
println!("{}", chapter.path());
println!("root has {} child", root.children.borrow().len());
}
The direction of ownership is the design decision, and it is the same decision
you would make in any language that had to answer it: the parent owns its
children, the child refers to its parent. Delete the root and the subtree
goes with it; delete a child and the parent survives. Writing it with Rc in both
directions compiles, runs, and leaks everything.
The other answer: identifiers and an arena
The Rc-and-Weak tree above is the direct translation of a pointer-based
design. There is a second approach that is often better in Rust and is
underappreciated by people arriving from languages where it is unusual: store the
nodes in one collection and refer to them by index.
struct Tree {
nodes: Vec<Node>,
}
struct Node {
name: String,
parent: Option<usize>,
children: Vec<usize>,
}
impl Tree {
fn new(root: &str) -> Tree {
Tree {
nodes: vec![Node { name: String::from(root), parent: None, children: Vec::new() }],
}
}
fn add(&mut self, parent: usize, name: &str) -> usize {
let id = self.nodes.len();
self.nodes.push(Node {
name: String::from(name),
parent: Some(parent),
children: Vec::new(),
});
self.nodes[parent].children.push(id);
id
}
fn path(&self, id: usize) -> String {
match self.nodes[id].parent {
Some(parent) => format!("{}/{}", self.path(parent), self.nodes[id].name),
None => self.nodes[id].name.clone(),
}
}
}
fn main() {
let mut tree = Tree::new("root");
let docs = tree.add(0, "docs");
let chapter = tree.add(docs, "ch01.md");
println!("{}", tree.path(chapter));
println!("{} nodes", tree.nodes.len());
}
Everything is owned by the Tree, so there is no reference counting, no interior
mutability, no cycle problem, and no runtime borrow tracking. Nodes sit
contiguously in memory, which makes traversal faster than chasing pointers.
Serializing the structure is trivial, because indices survive a round trip
through a file and pointers do not. The structure can be cloned, sent
between threads, or snapshotted by copying one Vec.
The costs are real and worth stating: an index is not checked for validity the
way a pointer is, so a stale index after a removal reads the wrong node rather
than failing. Production crates such as slotmap and generational-arena solve
that with generation counters, and are worth reaching for when removal is part of
the design.
For graphs and trees that live in one place and are traversed often — parsers,
ECS systems, dependency graphs, document models — this pattern is usually the
better Rust design. Try it before Rc<RefCell<T>>.
Conclusion
Shared ownership and interior mutability are the tools for structures that are
genuinely not trees. Rc gives many owners and read-only access. RefCell moves
the borrow rule from compile time to runtime, with a panic where there used to be
an error. Together they reproduce the reference-and-mutate model of other
languages, and reproduce its debugging experience as well.
The discipline is to earn them. Single ownership with borrowing, values taken in
turn, identifiers instead of pointers, and mutation gathered into one place all
keep the checks static, and they cover most of what looks at first like a case
for sharing. When the structure really is a shared mutable graph, use the tools
deliberately, decide which direction owns and which refers, and use Weak for
the back edges.
The index-and-arena design deserves a first look rather than a last one. It is faster, it serializes, it has no cycles to leak, and it keeps a graph-shaped structure inside a single owner.
The next chapter goes back to code that most Rust programs are made of, and to the abstraction that makes it fast and readable at once.
Questions to think through
Find a place in a system you know where two objects hold references to each other. Which one conceptually owns the other, and what would happen today if the owner were destroyed first?
RefCell converts a compile error into a runtime panic. Under what circumstances
is that a good trade, and what does it require of your test coverage?
The arena version of the tree uses usize indices, which are not validated. What
would go wrong if a node were removed, and what does a generation counter add
that a plain index does not?
Thinking in Iterators
An iterator in Rust is a value that produces a sequence, one item at a time, on
request. That description fits iterators in most languages, and it undersells
this one badly, because Rust’s iterators occupy a place in the language that
for loops occupy elsewhere. A large fraction of ordinary Rust — collection
processing, string handling, file reading, numeric work — is written as iterator
chains, and learning to read and write them is most of learning to read and write
Rust.
The trait itself is almost nothing:
#![allow(unused)]
fn main() {
trait MyIterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
}
One associated type and one method. next returns Some(item) until the
sequence is exhausted and then None. Everything else — around seventy adapter
and consumer methods — is built on that, supplied as default methods, and
available to anything that implements the one.
Laziness is the mechanism
The first thing to understand is that adapters do no work. map does not
transform anything; it constructs a Map value holding the source iterator and
the closure. filter constructs a Filter. Chaining five adapters builds a
five-layer struct and executes nothing.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let pipeline = numbers
.iter()
.map(|n| {
println!(" mapping {n}");
n * 10
})
.filter(|n| n % 20 == 0);
println!("nothing has run yet");
let result: Vec<i32> = pipeline.collect();
println!("{result:?}");
}
The mapping messages appear after the “nothing has run yet” line, and each item passes through every stage before the next one starts. Work happens only when something asks for items, and asking is what a consumer does.
Two consequences follow. An infinite sequence is fine as long as something
bounds it, so (1..).map(|n| n * n).take_while(|&n| n < 100) terminates. And a
chain is not a series of passes over the data building intermediate collections
— it is one pass, with each item threaded through every stage, which is why
these chains are as fast as hand-written loops.
Three families
Iterator code reads as a sentence with three parts, and knowing which family a method belongs to makes unfamiliar chains legible.
Creation gets an iterator from something. On collections, iter(),
iter_mut() and into_iter(), which differ in ownership and are covered below.
Ranges are iterators: 0..10, 1..=100, (1..) unbounded. Strings offer
chars(), bytes(), lines(), split_whitespace() and split(pattern). Files
offer lines() through BufRead. std::iter supplies once, repeat,
empty, and successors for generating a sequence from a rule.
Adapters take an iterator and return another one. map transforms, filter
keeps, filter_map does both and drops the Nones, take and skip and their
_while forms bound the sequence, enumerate pairs items with indices, zip
walks two sequences together, chain concatenates, rev reverses, flatten and
flat_map unwrap a level of nesting, peekable allows looking ahead,
step_by samples, scan carries state along, and inspect prints without
changing anything, and is the debugging tool for chains.
Consumers run the chain and produce something that is not an iterator.
collect builds a collection, sum, product, min, max, count reduce to
one value, fold reduces with a closure, any, all, find, position search
and stop early, partition splits in two, for_each runs a side effect, and
last and nth pick.
fn main() {
let text = "the quick brown fox jumps over the lazy dog";
let long_words: Vec<&str> = text
.split_whitespace()
.filter(|word| word.len() > 3)
.collect();
let initials: String = text
.split_whitespace()
.filter_map(|word| word.chars().next())
.collect();
let total_length: usize = text.split_whitespace().map(str::len).sum();
println!("{long_words:?}");
println!("{initials}");
println!("{total_length}");
}
The ownership dimension
This is the part that trips people, and it is the place where iterators connect back to the rest of the book. A collection offers three ways to iterate, and they differ in what you get and what happens to the collection.
iter() yields &T. The collection is borrowed and usable afterward. This is
what for item in &collection does.
iter_mut() yields &mut T. The collection is exclusively borrowed, the items
can be modified in place, and the collection’s shape cannot change while you
hold it. This is what for item in &mut collection does.
into_iter() yields T. The collection is consumed and gone. This is what
for item in collection does.
fn main() {
let mut names = vec![String::from("ada"), String::from("grace")];
for name in &names {
println!("borrowed {name}");
}
for name in &mut names {
name.push('!');
}
let lengths: Vec<usize> = names.iter().map(|name| name.len()).collect();
println!("{lengths:?} {names:?}");
let owned: Vec<String> = names.into_iter().map(|name| name.to_uppercase()).collect();
println!("{owned:?}");
}
That last line consumes names, which is why it comes last. Choosing among the
three is the same decision as choosing among T, &T and &mut T in a function
signature: does this need the values, or only to look at them, or to change them
in place?
A frequent small confusion: .iter().map(...) gives the closure a &T, so a
closure over a Vec<i32> receives &i32 and often needs a * or a pattern like
|&n|. .copied() or .cloned() convert a &T stream to a T stream when T
allows it, and are usually cleaner than dereferencing inside every closure.
Collect is more capable than it looks
collect builds any type that implements FromIterator, which includes every
standard collection and several types that are not obviously collections:
use std::collections::{HashMap, HashSet, BTreeMap};
fn main() {
let pairs = vec![("a", 1), ("b", 2), ("a", 3)];
let map: HashMap<&str, i32> = pairs.iter().copied().collect();
let ordered: BTreeMap<&str, i32> = pairs.iter().copied().collect();
let keys: HashSet<&str> = pairs.iter().map(|(key, _)| *key).collect();
let joined: String = vec!["a", "b", "c"].into_iter().collect();
println!("{}", map.len());
println!("{ordered:?}");
println!("{}", keys.len());
println!("{joined}");
}
Because the target type is what decides the behavior, collect usually needs an
annotation — either on the binding, as above, or with turbofish:
collect::<Vec<_>>(). The _ lets the element type be inferred.
The most useful trick in the method is collecting fallible items. An iterator of
Result can be collected into a Result of a collection, which stops at the
first error:
fn main() {
let good = vec!["1", "2", "3"];
let bad = vec!["1", "two", "3"];
let parsed: Result<Vec<i32>, _> = good.iter().map(|s| s.parse::<i32>()).collect();
let failed: Result<Vec<i32>, _> = bad.iter().map(|s| s.parse::<i32>()).collect();
println!("{parsed:?}");
println!("{}", failed.is_err());
}
Written by hand that is a loop, a mutable vector, a match and an early return.
The same works for Option. This one method turns “parse all of these and fail
if any fails” into a single expression, and it appears constantly in real code.
Writing your own
Implementing Iterator requires one method, and everything else comes free:
struct Fibonacci {
current: u64,
next: u64,
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.current;
self.current = self.next;
self.next = value.checked_add(self.next)?;
Some(value)
}
}
fn fibonacci() -> Fibonacci {
Fibonacci { current: 0, next: 1 }
}
fn main() {
let values: Vec<u64> = fibonacci().take(10).collect();
println!("{values:?}");
let large = fibonacci().find(|&n| n > 1_000_000).unwrap();
println!("{large}");
println!("{}", fibonacci().take_while(|&n| n < 100).filter(|n| n % 2 == 0).sum::<u64>());
}
Thirteen lines of implementation, and the type now works with take, find,
filter, sum, zip, for loops and everything else. The checked_add
returning None through ? ends the sequence at overflow rather than panicking,
the kind of thing that is easy to get right here and easy to forget in a
hand-written loop.
Implementing IntoIterator for your own collection type is the companion move,
and it is what makes for item in my_collection work. Doing it for T, &T and
&mut T gives users the same three choices the standard collections offer.
What the performance claim means
The claim usually made is that iterator chains compile to the same machine code as the equivalent loop. In the common cases that is true, and the reason is monomorphization from chapter 10: every adapter is a concrete type with a concrete closure, nothing is dispatched dynamically, and the optimizer sees one fused loop body with no allocation between stages.
Iterators sometimes beat the hand-written version, because iterating without indices removes bounds checks that an index-based loop has to keep.
Where the claim weakens is worth knowing. collect into a collection allocates,
and a chain that collects in the middle to feed another chain has paid for an
intermediate structure; usually the fix is to keep the chain going. Some
adapters over some sources defeat the optimizer, notably chains through zip on
sources with unknown length. And an iterator holding a trait object goes back to
dynamic dispatch.
None of that changes the practical advice: write the iterator version first, because it is shorter and harder to get wrong, and to measure before rewriting it as a loop for speed. What you should not do is avoid iterators because they feel like they must cost something; that intuition is imported from languages where it was correct.
When a loop is the better answer
Iterator chains are not always clearer. Three situations favor an ordinary loop.
When the body has complex control flow — several early exits, a continue that
skips different amounts of work depending on a condition, or a branch that needs
to do two different things — a chain turns into nested closures returning
Option and is harder to read than the loop it replaced.
When several things are being mutated at once, a loop says so directly.
for_each with a closure capturing three mutable variables is a loop wearing a
costume.
When the logic is genuinely sequential and stateful — a parser stepping through
tokens with a mode, a state machine — the iterator version needs scan or
fold with a tuple accumulator, and reads worse than the statements it encodes.
The test is what a reader has to reconstruct. numbers.iter().filter(...).sum()
states an intent. A five-deep chain with a scan carrying a three-field tuple
states a mechanism, badly.
Note
The
itertoolscrate adds the adapters the standard library left out —chunk_by,sorted,unique,dedup,tuple_windows,joinand several dozen more. If a chain is getting awkward, it is worth checking whether the thing you are hand-rolling is already there.
A related habit: the entry API
Iterators are one instance of a broader idea. The standard library
gives you a way to say what you mean in one operation instead of a read followed
by a write. HashMap’s entry API is the other one you will use daily:
use std::collections::HashMap;
fn main() {
let text = "the quick brown the lazy the";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
let mut sorted: Vec<_> = counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
println!("{sorted:?}");
}
entry looks the key up once and returns a handle to the slot, occupied or not.
The alternative — contains_key, then get_mut or insert — hashes twice, and
in a language with references it would also be a chance for the two operations to
disagree. Reach for entry whenever you find yourself checking for a key before
doing something with it.
Conclusion
Iterator is one method with seventy conveniences attached, and it is lazy, so a
chain of adapters describes a computation rather than performing one. Reading
Rust fluently means reading these chains as sentences: something creates, some
things adapt, one thing consumes.
The ownership question runs through it. iter, iter_mut and into_iter are
the same choice as &T, &mut T and T in a signature, and choosing wrong is
the most common source of confusion for newcomers. collect is more capable than
it looks, particularly its ability to turn a sequence of Result into a Result
of a sequence.
Implementing the trait for your own type costs one method and buys the whole vocabulary, which makes it one of the best-value implementations in the language.
The next chapter takes the ownership model across thread boundaries, where the same rule that has been preventing dangling pointers turns out to prevent data races as well.
Questions to think through
Take a loop from your own code that builds a collection. Rewrite it as a chain in your head and decide which version a reader would understand faster. What made the difference?
iter(), iter_mut() and into_iter() correspond to three ways of taking a
parameter. Find a place in code you know where a method iterates a collection and
decide which of the three it should be using.
Collecting into Result<Vec<T>, E> stops at the first error. When would you want
all the errors instead, and what would the chain look like?
Ownership Across Threads
This chapter is where the bill from the first four chapters gets paid.
Everything so far has been about a single thread: who owns a value, who may read it, who may change it, how long it stays valid. The discipline was justified by memory safety, and memory safety alone would have been a reasonable return. But the rule chapter 3 stated — many readers or one writer, never both — is the same rule that makes concurrent access safe, and because the compiler has already been enforcing it everywhere, it gets thread safety without a second mechanism.
A data race requires three things at once: two or more threads touching the same
memory, at least one of them writing, and no synchronization between them. Rust
forbids the combination by construction. If the data is shared, you have a &T
and cannot write. If you can write, you have a &mut T and nobody else has
anything. There is no third case, so there is no race.
That result has a name in the community — fearless concurrency — and the feeling it describes is real. Writing threaded code in Rust does not carry the low-grade dread that writing it in Java or C++ does, because the class of bug that makes threading frightening cannot compile.
Two marker traits
The compiler needs to know which types are safe to move between threads and which are safe to share, and it tracks that with two traits that have no methods.
Send means a value of this type can be moved to another thread. Almost
everything is Send. i32, String, Vec<T> when T: Send, and any struct
whose fields are all Send.
Sync means &T is Send — that a reference to this type can be shared
with another thread, which is to say that concurrent reads are safe. Again most
things qualify, and a type is Sync when its fields are.
Both are derived automatically by the compiler; you never implement them, except
in unsafe code wrapping something the compiler cannot see into.
What matters is the exceptions, because they are the cases where the compiler stops you.
Rc<T> is neither Send nor Sync. Its reference count is a plain integer
incremented without synchronization, so two threads cloning the same Rc could
lose an increment and free the value while it is still in use. The threaded
equivalent, Arc<T>, uses atomic operations and is Send and Sync, and pays a
small cost for it. That cost is why both exist.
RefCell<T> is Send but not Sync. Moving one to another thread is fine;
sharing it is not, because its borrow counter has the same problem. The threaded
equivalent is Mutex<T> or RwLock<T>.
Raw pointers are neither, so unsafe code that shares them is
something you have to vouch for explicitly.
The payoff is that these two traits turn a category of design error into a type error:
use std::rc::Rc;
use std::thread;
fn main() {
let shared = Rc::new(vec![1, 2, 3]);
let handle = thread::spawn(move || {
println!("{:?}", shared);
});
handle.join().unwrap();
}
The compiler rejects it with Rc<Vec<i32>> cannot be sent between threads safely. No documentation to read, no convention to remember, no race to
reproduce at three in the morning.
Spawning, and why 'static appears
use std::thread;
fn main() {
let mut handles = Vec::new();
for id in 0..4 {
handles.push(thread::spawn(move || {
let total: u64 = (1..=1000u64).map(|n| n * id).sum();
(id, total)
}));
}
for handle in handles {
let (id, total) = handle.join().unwrap();
println!("thread {id} computed {total}");
}
}
The move keyword forces the closure to take ownership of what it captures
rather than borrowing. That is required, and the reason is in the signature:
thread::spawn demands F: Send + 'static.
Chapter 4 explained what that 'static bound means — the closure may hold no
reference with a lifetime shorter than the program. It is not a claim that the
thread runs forever. It is the compiler pointing out that spawn returns
immediately, main might return while the new thread is still running, and any
borrow of a local in main would then dangle.
When you want a thread to borrow local data, the answer is a scoped thread, which guarantees the threads finish before the scope ends:
use std::thread;
fn main() {
let readings = vec![12, 45, 7, 88, 23];
let mut maximum = 0;
thread::scope(|scope| {
let handle = scope.spawn(|| readings.iter().sum::<i32>());
maximum = *readings.iter().max().unwrap();
println!("sum {}", handle.join().unwrap());
});
println!("max {maximum}, readings still here: {readings:?}");
}
The closure borrows readings without cloning or wrapping it, main reads it at
the same time — both are shared borrows, so that is allowed — and readings is
usable afterward. thread::scope does not return until every thread spawned
inside has finished, and that is what makes the borrow sound.
Channels move ownership
The mechanism that fits Rust’s model most naturally is not shared memory at all. A channel moves values from one thread to another, and because a move transfers ownership, the sending thread cannot touch what it sent:
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel();
for worker in 0..3 {
let sender = sender.clone();
thread::spawn(move || {
for job in 0..3 {
sender.send(format!("worker {worker} finished job {job}")).unwrap();
}
});
}
drop(sender);
let mut messages: Vec<String> = receiver.iter().collect();
messages.sort();
for message in messages {
println!("{message}");
}
}
Two details make this work. Each worker gets its own clone of the sender, and the
original is dropped in main; the receiver’s iterator ends when every sender has
been dropped, so forgetting that drop hangs the program. And the String sent
is gone from the worker’s side — there is no shared buffer to guard, because
ownership moved with the value.
This is Rust’s version of the advice to communicate by sharing memory only when you must. The difference from Go, where the same advice is a slogan, is that here the compiler enforces which things you have handed over.
Mutex owns its data
When threads really must share mutable state, the standard library’s mutex has a shape worth studying, because it is different from every mutex you have used before.
In Java, C and Go, a lock and the data it protects are separate. The association between them lives in a comment or a naming convention, and nothing stops code from touching the data without taking the lock. Every codebase with threads has at least one place where somebody did.
In Rust, the mutex contains the data, and the only way to reach the data is to lock:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let tally = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..8 {
let tally = Arc::clone(&tally);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
let mut guard = tally.lock().unwrap();
*guard += 1;
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("{}", tally.lock().unwrap());
}
The program prints 8000, every time. lock() returns a guard that dereferences
to the data, and the guard releases the lock when it is dropped — which happens
at the end of the iteration here, automatically, on every path including a panic.
There is no unlock to forget and no finally block to write.
Arc<Mutex<T>> is the threaded counterpart of Rc<RefCell<T>> from chapter 12,
and the same advice applies: it is correct when the design genuinely needs shared
mutable state, and it is a code smell when it is the first thing you reach for.
Channels and scoped threads cover more cases than people expect.
Two details. lock() returns a Result because a mutex becomes poisoned if a
thread panics while holding it — the data may be in a half-updated state, and the
Err is a warning that it might be inconsistent. Most code calls unwrap on it,
which propagates the panic, and that is usually right.
And RwLock<T> is the variant that permits many readers or one writer — the
same rule the borrow checker applies, moved to runtime. Use it when reads
dominate heavily; for anything close to balanced, a Mutex is faster because it
is simpler.
What Rust does not prevent
The guarantee is precise and it is worth knowing its edges, because “fearless concurrency” gets read as more than it claims.
Deadlock is still possible. Two threads taking two locks in opposite orders
will deadlock in Rust as they would in any other language. The type system tracks
who may access what, not the order in which locks are acquired. The usual
disciplines — a global lock ordering, holding one lock at a time, using
try_lock with a timeout — all still apply.
Race conditions in the logical sense are still possible. A program that reads a balance, computes a new one, and writes it back in three separate locked operations has a bug that no amount of locking each step fixes. Rust prevents data races, which is a memory-level property. It does not know what your invariants are.
Livelock, starvation and priority inversion are unaffected.
Leaks are still possible, including through reference cycles, as chapter 12 described.
What you get is that the failures above are the ones that remain, and they are the ones that can be reasoned about. The failure that cannot be reasoned about — memory being read while it is written, producing a value that was never stored — is gone.
Data parallelism
For the common case where the work is a loop over a collection, threads are more
machinery than the problem needs. The rayon crate turns a sequential iterator
chain into a parallel one by changing the first method call:
use rayon::prelude::*;
let total: u64 = readings.par_iter().map(|r| expensive(r)).sum();
par_iter splits the work across a thread pool and joins the results. It is safe
for the same reason everything else in this chapter is: the closure gets &T,
Sync guarantees that sharing is sound, and the compiler checks it. For
CPU-bound work over a collection this is usually the right answer, and it is one
line.
A word on async
Asynchronous Rust is a large subject and this book is not the place for it, but the connection to ownership is worth drawing because it explains most of what people find difficult about it.
An async fn compiles into a state machine. Everything alive across an .await
point has to be stored in that state machine, which is why holding a MutexGuard
across an await is a problem — the guard is now part of a value that may be moved
and may live for an unknown time. Spawning a task on a multi-threaded executor
requires the future to be Send, for the same reason thread::spawn does, and
usually 'static, because the executor decides when it runs.
So the questions async asks are the questions this book has been asking throughout: what owns this, how long does it live, can it move to another thread. The syntax is new; the model is not. Tokio’s documentation is the place to go next, and it will make more sense with these four chapters behind you.
Conclusion
Data race freedom is not a separate feature in Rust. It is the borrowing rule,
applied across threads, enforced by two marker traits that the compiler derives
and checks. Send says a value can move between threads; Sync says it can be
shared. The types that are neither — Rc, RefCell, raw pointers — are exactly
the ones whose internal bookkeeping is unsynchronized.
Three shapes cover most threaded code. Channels move ownership between threads
and need no locks. Scoped threads borrow local data with a compiler-checked
guarantee that they finish first. Arc<Mutex<T>> shares mutable state, with the
lock owning the data so there is no way to reach it unlocked.
What remains unprevented is deadlock, logical races, and leaks. Those are real and they are tractable, unlike the class of bug that is now impossible.
The next chapter zooms out from values and types to the unit a Rust programmer actually ships: the crate, its modules, and the public surface you choose to expose.
Questions to think through
Rc is not Send because its count is not atomic. Describe the interleaving of
two threads that would lose an increment, and say what the program does
afterward.
A mutex that owns its data cannot be bypassed. Find a place in code you know where a lock and the data it protects are separate. What currently stops someone from reading the data without the lock, and how would you know if they had?
The chapter says Rust does not prevent logical race conditions. Write down an operation from your own system that reads, computes and writes, and say what would have to be true for it to be safe under concurrency.
Designing a Crate
The chapters so far have been about values, types and the relationships between them. This one is about the container they ship in.
A crate is the unit of compilation and the unit of distribution: one library or one binary, compiled together, published as one thing, versioned as one thing. Inside it, modules organize the code and — more importantly — decide what the outside world can see. Getting that boundary right is the part of Rust design that most resembles the architecture work you already do, and it is where a decision made casually on the first day becomes a breaking change on the hundredth.
Modules are the privacy boundary
Chapter 5 mentioned that private in Rust means private to the module rather
than private to the type. That is the fact the module system turns on.
mod billing {
pub struct Invoice {
number: u64,
total_cents: i64,
}
impl Invoice {
pub fn new(number: u64, total_cents: i64) -> Invoice {
Invoice { number, total_cents }
}
pub fn total_cents(&self) -> i64 {
self.total_cents
}
}
pub fn apply_discount(invoice: &mut Invoice, percent: i64) {
invoice.total_cents -= invoice.total_cents * percent / 100;
}
fn audit_log(_invoice: &Invoice) {}
}
fn main() {
let mut invoice = billing::Invoice::new(1, 10_000);
billing::apply_discount(&mut invoice, 10);
println!("{}", invoice.total_cents());
}
apply_discount is a free function, not a method, and it reaches straight into
Invoice’s private field because it lives in the same module. From main, that
field does not exist. audit_log is not visible outside billing at all.
This has a direct consequence for design. A module, not a type, is the unit
whose invariants you reason about. A set of types that genuinely need to
cooperate — a parser and its token type, a connection and its pool, a document
and its nodes — belong in one module, where they can see each other’s internals
and the outside world sees only the public surface. That is closer to how you
would have wanted package-private to work than how it does.
Four visibility levels cover everything: private by default, pub(super) for the
parent module, pub(crate) for everywhere in this crate but nowhere outside, and
pub for the world. pub(crate) is the one people underuse. A type that several
modules share but that no external user should depend on is pub(crate), and
marking it so means you can change it freely forever.
Files map onto modules by name — src/billing.rs or src/billing/mod.rs is the
billing module, src/lib.rs is the crate root — and mod billing; in the
parent pulls it in. The layout follows from the module structure rather than the
other way round.
The public surface is a promise
Anything pub in a library is something you have promised not to break. Rust’s
tooling holds you to semantic versioning, and the changes that constitute a break
are broader than they first appear.
Adding a field to a public struct breaks every caller who constructed it with a
struct literal. Adding a variant to a public enum breaks every caller who matched
on it exhaustively — which, given chapter 7, is all of them. Adding a method to a
public trait breaks every implementor, unless the method has a default body.
Tightening a bound, renaming anything, changing a parameter from &str to
&Path: all breaking.
Two habits make this manageable.
The first is to expose less. Every public item is a commitment; a field that
could be a method, a struct that could be opaque, a helper that could be
pub(crate) all cost nothing to hide now and cost a major version to hide later.
The default answer to “should this be public” is no.
The second is #[non_exhaustive], which tells the compiler that a type may grow:
#![allow(unused)]
fn main() {
#[non_exhaustive]
pub struct Settings {
pub timeout_seconds: u32,
pub retries: u8,
}
#[non_exhaustive]
pub enum Failure {
Timeout,
Refused,
}
}
An external crate can no longer build Settings with a struct literal — it must
go through a constructor you provide — and must include a wildcard arm when
matching Failure. Both restrictions are precisely what lets you add a field or
a variant later without a major version. Put it on error enums and on
configuration structs from the start, because adding it afterward is itself a
breaking change.
Signatures that are kind to callers
The Rust API Guidelines are worth reading in full; a handful of their recommendations account for most of the difference between an API that feels native and one that fights its users.
Take the most general parameter you can use. A function that needs to read a
path should take impl AsRef<Path>, which accepts &str, String, &Path and
PathBuf without the caller converting anything. A function that will store a
string should take impl Into<String>, which accepts both &str and String
and copies only when it must.
use std::path::Path;
fn describe(path: impl AsRef<Path>) -> String {
let path = path.as_ref();
format!("{} ({})", path.display(), if path.is_absolute() { "absolute" } else { "relative" })
}
fn main() {
println!("{}", describe("/etc/hosts"));
println!("{}", describe(String::from("notes/todo.md")));
}
Return concrete types. Generality helps in parameters and hurts in return
values, because a caller cannot get back what you erased. Return Vec<T> rather
than impl Iterator when the caller may want to index or keep it; return your
own struct rather than Box<dyn Trait> when the type is nameable.
Follow the naming conventions, because they carry cost information. as_
borrows and is cheap, to_ allocates or converts, into_ consumes. iter,
iter_mut and into_iter mean what they mean everywhere else. A getter is
name(), not get_name(). Constructors are new, then with_* and from_*.
Implement the standard traits. A public type should almost always derive
Debug — a user cannot write a useful test or log line without it. Clone,
Default, PartialEq where they make sense. Display for anything a user will
print. From for the conversions that are natural and infallible, TryFrom for
the ones that are not. Each one is small and each one removes a paper cut.
Keep Send and Sync. These are derived automatically, so a single Rc or
RefCell in a private field silently makes your public type unusable
across threads. That is a real breaking change that no signature shows, so if a
type is meant to be used from threaded code, a test that asserts it is
Send + Sync is cheap insurance.
The three kinds of test
Rust’s test story is unusual in that the three kinds correspond to three different audiences, and the third one is the reason the ecosystem’s documentation is as reliable as it is.
Unit tests live in the module they test, inside a #[cfg(test)] block, and
can see private items:
pub fn normalize(raw: &str) -> String {
raw.trim().to_lowercase()
}
fn is_blank(value: &str) -> bool {
value.trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_case_and_space() {
assert_eq!(normalize(" David "), "david");
}
#[test]
fn detects_blank() {
assert!(is_blank(" "));
}
}
fn main() {}
Keeping them beside the code rather than in a parallel tree is deliberate: the test is documentation of the unit, it moves when the unit moves, and it has access to internals so that a private invariant can be tested directly.
Integration tests live in tests/, each file a separate crate that links
your library from outside. They can only use the public API, which makes them the
real check on whether that API is usable. If an integration test needs
something you have not exported, your users will need it too.
Documentation tests are the code blocks in your doc comments, and cargo test compiles and runs them:
/// Splits a delimited line into fields, honoring quoted sections.
///
/// ```
/// # fn split_fields(line: &str) -> Vec<String> {
/// # line.split(',').map(|f| f.trim().to_string()).collect()
/// # }
/// let fields = split_fields("a, b, c");
/// assert_eq!(fields, vec!["a", "b", "c"]);
/// ```
pub fn split_fields(line: &str) -> Vec<String> {
line.split(',').map(|field| field.trim().to_string()).collect()
}
fn main() {}
An example in the documentation cannot drift out of date, because a change that
breaks it breaks the build. This is why Rust library documentation is generally
trustworthy and why writing examples in doc comments rather than in a separate
README is the convention. /// documents the item that follows; //! documents
the module or crate it is inside, which is how a crate’s front page is written at
the top of lib.rs.
Re-exports shape what users type
Internal structure and public structure do not have to match. pub use lifts an
item to where you want users to find it:
mod parser {
pub mod tokens {
#[derive(Debug)]
pub struct Token(pub String);
}
}
pub use parser::tokens::Token;
fn main() {
println!("{:?}", Token(String::from("let")));
}
Users write crate_name::Token while the code lives four levels deep. This lets
you organize source by what makes it easy to work on and organize the API by what
makes it easy to use, and it means moving a file is not a breaking change.
The convention for a crate with more than a handful of items is a flat public
surface with the common types re-exported at the root, and a prelude module for
the traits users need in scope. Deep public paths are a sign the internal
structure leaked.
When one crate becomes several
A workspace holds several crates that share a lock file, a target directory and a
cargo test invocation. It is the right structure when a project has genuinely
separable pieces: a core library, a command-line front end, a server, a shared
type definition crate used by both ends of a protocol.
The reasons to split are practical rather than aesthetic. Compilation is parallelized across crates and cached per crate, so a large project that recompiles everything on every change is a candidate. A binary that depends on a library forces the library to have a public API somebody uses. And publishing a core separately lets others use it without your command-line interface.
The reason not to split is that crate boundaries are firmer than module boundaries: no shared private access, and circular dependencies are impossible. Start with modules in one crate, and split when the boundary has proven itself.
Conclusion
The crate is what you ship, and the module is where privacy lives. Because private means private-to-module, a module is the unit whose invariants can be reasoned about, and cooperating types belong in one rather than being forced apart by a type-level privacy rule.
A public item is a promise. Exposing less is the main defense, #[non_exhaustive]
is the other, and both are far cheaper applied at the start than retrofitted.
Signatures should be general in their parameters and concrete in their returns,
follow the naming conventions that carry cost information, and implement the
standard traits that let a type participate in the ecosystem.
Tests come in three kinds for three audiences, and the documentation test is the one that makes a crate’s examples permanently true.
The next chapter puts the book’s ideas together on a single small system, from requirements to working code.
Questions to think through
Take a library you maintain. List its public API and mark each item as something you would defend in a design review or something that is public because it was easier. How much of the second kind is there?
#[non_exhaustive] on an error enum lets you add variants without a major
version. What does it cost the caller, and is that cost different for an
application than for another library?
The chapter says an integration test can only use the public API, and that this makes it the real check. What would you learn from writing one for code you currently only unit-test?
Case Study: A System Designed in Rust
Everything in this book so far has been one idea at a time. This chapter takes a small system from a requirements paragraph to working code, so that the ideas appear where they actually arise — which is not in the order a book presents them.
The system is a library’s circulation desk. It is small enough to show completely and has the properties that make design interesting: entities that refer to each other, rules that must not be violated, states that change over time, and money.
The requirements
A library holds books, each with some number of physical copies. Members borrow books and return them. A member may have at most three books out at once, and a book may not be lent when every copy is already out. A loan is due back after twenty-one days, and a late return incurs a fine of twenty-five cents per day, added to the member’s account. The desk staff need a list of overdue loans.
That is the sort of paragraph you actually get. Read it again and notice that it mixes three kinds of statement: things that exist, rules about them, and policy numbers that will change the first time a committee meets.
What owns what
Chapter 2 said the first design question is who is responsible for each thing. Answer it before writing any types, because it decides the shape of everything else.
Books exist whether or not anyone has borrowed them, so something must own the catalog. Members likewise. A loan is a relationship between a book and a member — it has no independent existence, and both of its ends outlive it.
So the ownership tree has one root. A Library owns the books, owns the members,
and owns the loans. A loan does not own a book or a member; it refers to them.
That last decision is the one that would have gone differently in Java, where a
Loan would hold a Book reference and a Member reference and nobody would
think about it. Here, holding references means lifetimes on Loan, lifetimes on
anything holding a Loan, and a structure that cannot be stored in the same
Library that owns the books it points at. The alternative from chapter 2 —
identity instead of pointers — is the right answer, and it brings benefits that
have nothing to do with the borrow checker: loans can be written to a file,
compared, sent between threads, and reasoned about without chasing pointers.
The nouns become types, and most become newtypes
The requirements name books, copies, members, loans, days, and money. Each one gets a type, and the small ones get newtypes:
#![allow(unused)]
fn main() {
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BookId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MemberId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Day(u32);
impl Day {
pub fn plus(self, days: u32) -> Day {
Day(self.0 + days)
}
pub fn days_since(self, earlier: Day) -> u32 {
self.0.saturating_sub(earlier.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Money(i64);
impl Money {
pub const ZERO: Money = Money(0);
pub fn cents(amount: i64) -> Money {
Money(amount)
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.0 / 100, (self.0 % 100).abs())
}
}
}
Four newtypes, all Copy, all zero cost at runtime. What they buy is that
check_out(book, member) cannot be called with the arguments swapped, that a
number of days cannot be passed where a day is expected, and that money cannot be
accidentally added to an identifier. Every one of those is a bug that a system
built on u32 and f64 will eventually have, invisibly.
Money holds an i64 of cents rather than a float, which is not a Rust point
but is the most common bug in systems of this kind. Putting it in a type means
the decision is made once and cannot be undone by a careless division somewhere
else.
Day is a deliberately crude stand-in for a real date type. In a production
system this would be chrono::NaiveDate or time::Date — and the point of
having a type at all is that swapping it is a change to one file.
The states, and the ones we refuse to represent
A loan is either out or returned. A returned loan has a return date and a fine; a
loan that is out has neither, and has a due date. Chapter 6’s test applies: if
this were a struct with returned: bool, return_date: Option<Day>, due: Option<Day> and fine: Option<Money>, it would represent sixteen combinations
of which three or four are meaningful.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct BookId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MemberId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Day(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Money(i64);
#[derive(Debug)]
pub enum Loan {
Out { book: BookId, member: MemberId, due: Day },
Returned { book: BookId, member: MemberId, on: Day, fine: Money },
}
impl Loan {
pub fn book(&self) -> BookId {
match self {
Loan::Out { book, .. } | Loan::Returned { book, .. } => *book,
}
}
pub fn member(&self) -> MemberId {
match self {
Loan::Out { member, .. } | Loan::Returned { member, .. } => *member,
}
}
pub fn is_out(&self) -> bool {
matches!(self, Loan::Out { .. })
}
}
}
Two states, each carrying exactly the data that state has. A returned loan cannot be missing its fine and an outstanding loan cannot have one. The accessors use an or-pattern to pull the common fields out of either variant, which is the idiomatic way to say “both variants have this”.
There is a design question hidden here that the requirements do not answer: is a
returned loan still a loan, or is it history? Keeping both in one Vec<Loan>
means the overdue report has to filter, and it means the loan history is
preserved. Moving returned loans to a separate collection would make each
collection simpler and would throw away the ability to ask what a member has
borrowed before. Keeping them together is the choice here, and it is the kind of
choice that should be made deliberately rather than discovered later.
The failures become an enum
Three of the requirements are rules, and a rule is something that can be violated. Chapter 8’s guidance is that each violation a caller might handle differently gets its own variant, carrying what the caller needs to act:
#![allow(unused)]
fn main() {
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct BookId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MemberId(u32);
#[derive(Debug)]
pub enum LendingError {
NoSuchBook(BookId),
NoSuchMember(MemberId),
AllCopiesOut { book: BookId, title: String },
LimitReached { member: MemberId, limit: usize },
NotOnLoan { book: BookId, member: MemberId },
}
impl fmt::Display for LendingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LendingError::NoSuchBook(id) => write!(f, "no book with id {}", id.0),
LendingError::NoSuchMember(id) => write!(f, "no member with id {}", id.0),
LendingError::AllCopiesOut { title, .. } => {
write!(f, "every copy of {title:?} is on loan")
}
LendingError::LimitReached { limit, .. } => {
write!(f, "member already has {limit} books out")
}
LendingError::NotOnLoan { book, .. } => {
write!(f, "book {} is not on loan to that member", book.0)
}
}
}
}
impl std::error::Error for LendingError {}
}
AllCopiesOut carries the title as well as the identifier, because the desk
software will want to say which book, and it would otherwise have to look it up
again. LimitReached carries the limit, because the message should say what the
limit is and the caller does not know the policy. These are small decisions and
they are the difference between an error a user can act on and one that sends
them to the manual.
The policy becomes a trait
Twenty-one days, three books, twenty-five cents. Those numbers are in the requirements, they will change, and different branches of the same library system will want different ones. They are also the numbers a test needs to control.
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Money(i64);
impl Money { pub fn cents(amount: i64) -> Money { Money(amount) } }
pub trait Policy {
fn loan_days(&self) -> u32;
fn borrowing_limit(&self) -> usize;
fn fine_for(&self, days_late: u32) -> Money;
}
pub struct StandardPolicy;
impl Policy for StandardPolicy {
fn loan_days(&self) -> u32 {
21
}
fn borrowing_limit(&self) -> usize {
3
}
fn fine_for(&self, days_late: u32) -> Money {
Money::cents(i64::from(days_late) * 25)
}
}
}
This trait passes chapter 9’s test: there will be more than one
implementation, and one of them is a test fixture. A Policy that lends
for one day with no limit makes the overdue path testable without simulating a
month. Compare a Storage trait added at the same time — with one implementation
and no immediate second — which would be abstraction bought on speculation.
Box<dyn Policy> rather than a generic parameter, because the choice is made
once at startup, the calls are not hot, and a generic parameter on Library
would propagate into every type and function that mentions it. Chapter 10’s
decision, made in the direction chapter 10 recommends for this shape.
The system
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BookId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MemberId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Day(u32);
impl Day {
pub fn plus(self, days: u32) -> Day { Day(self.0 + days) }
pub fn days_since(self, earlier: Day) -> u32 { self.0.saturating_sub(earlier.0) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Money(i64);
impl Money {
pub const ZERO: Money = Money(0);
pub fn cents(amount: i64) -> Money { Money(amount) }
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.0 / 100, (self.0 % 100).abs())
}
}
#[derive(Debug)]
pub struct Book { pub title: String, pub copies: u8 }
#[derive(Debug)]
pub struct Member { pub name: String, pub fines: Money }
#[derive(Debug)]
pub enum Loan {
Out { book: BookId, member: MemberId, due: Day },
Returned { book: BookId, member: MemberId, on: Day, fine: Money },
}
impl Loan {
pub fn book(&self) -> BookId {
match self { Loan::Out { book, .. } | Loan::Returned { book, .. } => *book }
}
pub fn member(&self) -> MemberId {
match self { Loan::Out { member, .. } | Loan::Returned { member, .. } => *member }
}
pub fn is_out(&self) -> bool { matches!(self, Loan::Out { .. }) }
}
#[derive(Debug)]
pub enum LendingError {
NoSuchBook(BookId),
NoSuchMember(MemberId),
AllCopiesOut { book: BookId, title: String },
LimitReached { member: MemberId, limit: usize },
NotOnLoan { book: BookId, member: MemberId },
}
impl fmt::Display for LendingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LendingError::NoSuchBook(id) => write!(f, "no book with id {}", id.0),
LendingError::NoSuchMember(id) => write!(f, "no member with id {}", id.0),
LendingError::AllCopiesOut { title, .. } => write!(f, "every copy of {title:?} is on loan"),
LendingError::LimitReached { limit, .. } => write!(f, "member already has {limit} books out"),
LendingError::NotOnLoan { book, .. } => write!(f, "book {} is not on loan to that member", book.0),
}
}
}
impl std::error::Error for LendingError {}
pub trait Policy {
fn loan_days(&self) -> u32;
fn borrowing_limit(&self) -> usize;
fn fine_for(&self, days_late: u32) -> Money;
}
pub struct StandardPolicy;
impl Policy for StandardPolicy {
fn loan_days(&self) -> u32 { 21 }
fn borrowing_limit(&self) -> usize { 3 }
fn fine_for(&self, days_late: u32) -> Money { Money::cents(i64::from(days_late) * 25) }
}
pub struct Library {
books: HashMap<BookId, Book>,
members: HashMap<MemberId, Member>,
loans: Vec<Loan>,
policy: Box<dyn Policy>,
}
impl Library {
pub fn new(policy: Box<dyn Policy>) -> Library {
Library {
books: HashMap::new(),
members: HashMap::new(),
loans: Vec::new(),
policy,
}
}
pub fn add_book(&mut self, id: u32, title: &str, copies: u8) -> BookId {
let id = BookId(id);
self.books.insert(id, Book { title: String::from(title), copies });
id
}
pub fn enroll(&mut self, id: u32, name: &str) -> MemberId {
let id = MemberId(id);
self.members.insert(id, Member { name: String::from(name), fines: Money::ZERO });
id
}
fn copies_out(&self, book: BookId) -> usize {
self.loans.iter().filter(|loan| loan.is_out() && loan.book() == book).count()
}
fn books_held_by(&self, member: MemberId) -> usize {
self.loans.iter().filter(|loan| loan.is_out() && loan.member() == member).count()
}
pub fn check_out(&mut self, book: BookId, member: MemberId, today: Day)
-> Result<Day, LendingError>
{
let record = self.books.get(&book).ok_or(LendingError::NoSuchBook(book))?;
if !self.members.contains_key(&member) {
return Err(LendingError::NoSuchMember(member));
}
if self.copies_out(book) >= usize::from(record.copies) {
return Err(LendingError::AllCopiesOut { book, title: record.title.clone() });
}
let limit = self.policy.borrowing_limit();
if self.books_held_by(member) >= limit {
return Err(LendingError::LimitReached { member, limit });
}
let due = today.plus(self.policy.loan_days());
self.loans.push(Loan::Out { book, member, due });
Ok(due)
}
pub fn check_in(&mut self, book: BookId, member: MemberId, today: Day)
-> Result<Money, LendingError>
{
let index = self
.loans
.iter()
.position(|loan| loan.is_out() && loan.book() == book && loan.member() == member)
.ok_or(LendingError::NotOnLoan { book, member })?;
let due = match &self.loans[index] {
Loan::Out { due, .. } => *due,
Loan::Returned { .. } => unreachable!("position matched an outstanding loan"),
};
let fine = self.policy.fine_for(today.days_since(due));
self.loans[index] = Loan::Returned { book, member, on: today, fine };
if let Some(record) = self.members.get_mut(&member) {
record.fines = Money(record.fines.0 + fine.0);
}
Ok(fine)
}
pub fn overdue(&self, today: Day) -> impl Iterator<Item = (&str, &str, u32)> {
self.loans.iter().filter_map(move |loan| match loan {
Loan::Out { book, member, due } if *due < today => {
let title = self.books.get(book)?.title.as_str();
let name = self.members.get(member)?.name.as_str();
Some((name, title, today.days_since(*due)))
}
_ => None,
})
}
}
fn main() {
let mut library = Library::new(Box::new(StandardPolicy));
let dune = library.add_book(1, "Dune", 1);
let sicp = library.add_book(2, "Structure and Interpretation", 2);
let ada = library.enroll(10, "Ada");
let grace = library.enroll(11, "Grace");
let opening_day = Day(1);
match library.check_out(dune, ada, opening_day) {
Ok(due) => println!("Dune due on day {}", due.0),
Err(error) => println!("error: {error}"),
}
match library.check_out(dune, grace, opening_day) {
Ok(due) => println!("Dune due on day {}", due.0),
Err(error) => println!("error: {error}"),
}
library.check_out(sicp, grace, opening_day).unwrap();
let today = Day(30);
for (name, title, days) in library.overdue(today) {
println!("{name} has {title} {days} days overdue");
}
match library.check_in(dune, ada, today) {
Ok(fine) => println!("returned Dune, fine {fine}"),
Err(error) => println!("error: {error}"),
}
match library.check_in(dune, ada, today) {
Ok(fine) => println!("returned Dune, fine {fine}"),
Err(error) => println!("error: {error}"),
}
}
Running it prints:
Dune due on day 22
error: every copy of "Dune" is on loan
Ada has Dune 8 days overdue
Grace has Structure and Interpretation 8 days overdue
returned Dune, fine $2.00
error: book 1 is not on loan to that member
The last two lines are the ones to look at. Returning the same book twice is
rejected without any explicit check for double-return, because the first
check_in replaced the Loan::Out with a Loan::Returned and position no
longer finds an outstanding loan. The rule is enforced by the state model rather
than by a guard.
What the compiler made us decide
Four decisions in that design were forced, and all four are decisions the same system needs in any language.
Loans refer to books by identifier. The alternative would have required
lifetimes and would have made loans impossible to store alongside the books. In
Java this decision is made by accident, and the consequence — that a Loan
object is not serializable, comparable or movable without dragging half the
object graph with it — is discovered in the second year.
check_in reads the due date out before mutating. Writing self.loans[index] = Loan::Returned { ..., fine: self.policy.fine_for(...) } in one expression
fails to compile, because it borrows self immutably for the policy call while
borrowing it mutably for the assignment. Chapter 3’s advice — do one thing, then
the other — applies, and the version that compiles is easier to read.
copies_out and books_held_by are separate &self methods called before the
mutation. Trying to inline them into check_out after taking a mutable borrow
of self.loans fails. Small read-only helpers that finish before the write
begins are a shape the borrow checker teaches you, and it happens to be good
structure anyway.
overdue returns an iterator borrowing the library. Its signature says the
returned iterator cannot outlive the Library, which is a fact about the data
that no other language would tell the caller. If the desk software needed to
store that report, the fix is to collect it into owned values — and the compiler
is the thing that raises the question.
What would change at scale
The design above is right for the system described and would need three changes under real load.
A linear scan over loans is fine for thousands and wrong for millions. The
shape that replaces it keeps the Vec as the owner of the data and adds indices
beside it: a HashMap<MemberId, Vec<usize>> of outstanding loans per member.
That is chapter 12’s arena pattern in miniature, and the ownership story does not
change.
Persistence would be added by making the Library the in-memory projection of a
store rather than the store itself, with a Repository trait — which at that
point has two implementations and a reason to exist, where at the start it would
not have.
Day would become a real date type, and Money would either stay as it is or
gain a currency field. Because both are newtypes with a small surface, that
change touches their own impl blocks and almost nothing else, which is what
introducing them bought.
Conclusion
The design process here ran in the order the book has been teaching. Decide what owns what, and prefer identity to pointers when the relationship is a reference rather than containment. Give the nouns types, and give the small ones newtypes, so that the compiler polices the distinctions your prose was already making. Model states as variants carrying their own data, so that the combinations you never want have no spelling. Name the failures in an enum, with the context a caller needs. Put policy behind a trait when there will really be more than one implementation, and let the test fixture be the second.
None of that is a Rust technique. It is ordinary design, and its only unusual property is that the compiler checks whether you did it.
The last chapter collects the recurring shapes — the ones worth recognizing on sight, and the ones worth recognizing so you can stop writing them.
Questions to think through
The design keeps returned loans in the same collection as outstanding ones. Write down what becomes harder and what becomes possible. Which would you choose for a system that has to answer “what has this member borrowed in the last five years”?
check_out clones the title into the AllCopiesOut error. What would it cost to
avoid the clone, and is that a trade you would make?
The Policy trait has one real implementation and one test implementation.
Chapter 9 said a trait with one implementor is usually not earning anything. Does
a test fixture count as a second implementor? Argue both sides.
Patterns and Antipatterns
A pattern is a shape that recurs because it solves a recurring problem. The Gang
of Four catalogued twenty-three of them for object-oriented design, and a good
half of those exist to work around the absence of first-class functions or sum
types. Rust has both, so several of the classic patterns dissolve: Strategy is a
closure, Command is a closure, Visitor is a match, Iterator is in the standard
library, and Singleton is usually a mistake.
What is left is a smaller set of shapes that are genuinely Rust’s, plus a list of habits that produce code which compiles and fights you. This chapter is both, written to be recognized on sight rather than read through.
Patterns
Newtype
A single-field tuple struct wrapping another type, to make a distinction the compiler enforces.
pub struct Sanitized(String);
impl Sanitized {
pub fn new(raw: &str) -> Sanitized {
Sanitized(raw.replace('<', "<").replace('>', ">"))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn render(content: &Sanitized) -> String {
format!("<p>{}</p>", content.as_str())
}
fn main() {
println!("{}", render(&Sanitized::new("<script>alert(1)</script>")));
}
Use it for identifiers, units, validated values, and anything where two values of the same underlying type mean different things. It costs nothing at runtime, and it is also the way past the orphan rule when you need to implement a foreign trait for a foreign type.
Builder
A separate type that accumulates configuration and produces the real value, solving the problem that Rust has neither optional parameters nor overloading.
pub struct Server {
host: String,
port: u16,
threads: usize,
}
pub struct ServerBuilder {
host: String,
port: u16,
threads: usize,
}
impl ServerBuilder {
pub fn new() -> ServerBuilder {
ServerBuilder { host: String::from("localhost"), port: 8080, threads: 4 }
}
pub fn host(mut self, host: &str) -> ServerBuilder {
self.host = String::from(host);
self
}
pub fn port(mut self, port: u16) -> ServerBuilder {
self.port = port;
self
}
pub fn build(self) -> Server {
Server { host: self.host, port: self.port, threads: self.threads }
}
}
fn main() {
let server = ServerBuilder::new().host("0.0.0.0").port(443).build();
println!("{}:{} on {} threads", server.host, server.port, server.threads);
}
Each setter takes self and returns it, which is what makes the calls chain.
Reach for it when a type has more than three or four construction parameters, or
when most of them have sensible defaults. For the simpler case where the defaults
are all you need, #[derive(Default)] plus struct update syntax —
Server { port: 443, ..Default::default() } — does the job with no builder at
all.
Typestate
A state machine encoded as separate types, with transitions that consume the
previous state. Chapter 6 developed this at length. The signature is a method
taking self by value and returning a different type, and the payoff is that
calling an operation in the wrong state is a compile error rather than a runtime
one.
The RAII guard
A value whose destruction performs an action, so that the action cannot be skipped.
struct Timer {
label: &'static str,
}
impl Timer {
fn start(label: &'static str) -> Timer {
println!("[{label}] start");
Timer { label }
}
}
impl Drop for Timer {
fn drop(&mut self) {
println!("[{}] done", self.label);
}
}
fn process() {
let _timer = Timer::start("process");
println!("working");
}
fn main() {
process();
}
MutexGuard, Ref, File and TempDir are all instances. Write one whenever
“finished” means an action rather than merely freeing memory: releasing a lock,
committing or rolling back, restoring a terminal, removing a temporary file,
closing a span in a trace. Bind it to a name — let _guard = ..., not let _ = ..., which drops it immediately.
The extension trait
A trait defined solely to add methods to a type you do not own.
trait Truncate {
fn ellipsize(&self, width: usize) -> String;
}
impl Truncate for str {
fn ellipsize(&self, width: usize) -> String {
if self.chars().count() <= width {
self.to_string()
} else {
let kept: String = self.chars().take(width.saturating_sub(1)).collect();
format!("{kept}…")
}
}
}
fn main() {
println!("{}", "the rust thought process".ellipsize(12));
}
Name them ...Ext by convention when they are part of a public API. It is how
crates add capability to standard types without wrappers.
Sans-IO
Separate the logic that decides from the code that performs input and output. A parser that takes bytes and returns events, a protocol state machine that takes messages and returns messages to send, a rules engine that takes facts and returns decisions — none of them touching a socket or a file.
The Rust-specific reason this pays is that pure logic needs no async, no
lifetimes tied to a connection, and no mocking framework. It is testable with
ordinary values, it works under any runtime, and the I/O shell around it is small
enough to review. The Policy trait in chapter 16 is a small instance of the
same instinct.
Identity over pointers
When two things refer to each other, give one an identifier and let the other hold it. Chapter 2 introduced this and chapter 12 developed it into the arena. Recognize it as a pattern rather than a workaround: identifiers serialize, compare, hash, cross threads, and survive the thing they name being moved in memory. Pointers do none of that.
Antipatterns
Cloning to make the compiler stop
The most common, and the one with the longest tail. A borrow error appears,
.clone() removes it, and the design question goes unasked.
The problem is not the allocation. It is that each clone hides a decision that was never made — usually that a function is taking ownership when it wanted a borrow, or that two parts of the program are arguing about who owns something. Chapter 2’s four answers are the checklist: does one of them only need to read, can they take turns, do they genuinely need independent values, or should the thing be identified rather than pointed at.
Cloning deliberately, in cold code, for a reason you can state, is fine.
unwrap as punctuation
Option and Result exist to force a decision at the point where absence or
failure appears. unwrap answers “crash”, which is sometimes right and is rarely
the answer you would have chosen if asked.
The test from chapter 7: can you state, in one sentence, why the failure is
impossible, and is that sentence about the code rather than about what you hope
the world will do? If yes, use expect with that sentence as the message. If no,
the type was pointing at a case you have not designed.
Rc<RefCell<T>> by default
Reaching for shared ownership plus interior mutability the first time the borrow checker objects reproduces the language you came from, at the cost of runtime panics where compile errors used to be and no static knowledge of who owns anything.
It is the right tool for genuine shared mutable graphs. It is the wrong tool for “I could not work out how to structure this”, and the difference is whether you walked down chapter 12’s ladder first.
Stringly-typed everything
Identifiers, states, kinds and categories represented as String, compared with
== against literals, with typos discovered at runtime. A status field holding
"approved" is an enum that has not been written yet, and a String parameter
named customer_id is a newtype that has not been written yet.
The tell is a comparison against a string literal, or a function whose parameters
are three Strings in an order nobody can remember.
Lifetimes on a storage type
Adding <'a> to a struct so that a field can be &str instead of String, and
then threading the parameter through every type that contains it. Chapter 4’s
rule: a lifetime parameter says the type is a temporary view. If the type is
stored, returned up several layers, or held across an await, it should own its
data, and the allocation you avoided was cheaper than the complexity you bought.
Deref for reuse
Implementing Deref on a newtype so that the inner type’s methods appear on the
outer one, as a substitute for inheritance. It works until it does not: method
resolution follows Deref and trait resolution does not, so the type appears to
have methods it cannot use generically, and the error messages are poor. Deref
is for smart pointers. Write the delegating methods.
&String, &Vec<T>, &Box<T> in signatures
Taking a reference to an owned container when a slice would do. &str accepts
string literals and sub-ranges; &String does not. &[T] accepts arrays,
vectors and slices; &Vec<T> does not. There is nothing you can do with the
narrow form that you cannot do with the wide one, so the narrow form only
restricts callers.
The same applies to impl AsRef<Path> versus &PathBuf, and to taking String
by value when the function only reads it.
The god struct
One type with forty fields and a hundred methods, because everything needs
everything. Rust makes this painful earlier than other languages do, since a
method taking &mut self borrows all of it and blocks every other access.
Chapter 3 named the signal: when the same borrow conflict keeps appearing, the fields involved want to be their own type. Treat recurring borrow errors in one place as structural feedback rather than as an obstacle.
Abstraction bought on speculation
A trait with one implementor, a generic parameter with one instantiation, a
Box<dyn Trait> where a concrete type would do. Each costs indirection,
compile time and a layer for the reader to see through, and returns nothing until
the second implementation arrives.
The Rust-specific version is over-generic function signatures: six type parameters with eight bounds, where two concrete types would have been clearer and just as reusable. Write the concrete version, and generalize when the second case shows up.
Ignoring a Result
let _ = fallible(); discards a failure silently, which is the empty catch
block with better syntax. Where ignoring is genuinely right — best-effort
cleanup, a log write that must not take down the program — make the decision
visible by logging the error explicitly, so the next reader sees a choice rather
than an oversight.
Conclusion
The patterns worth knowing in Rust are mostly small: a wrapper that makes a distinction real, a builder for types with many options, a state machine in the type system, a value whose destruction does the cleanup, a trait that adds methods from outside, logic separated from I/O, and identity in place of pointers. Several classic object-oriented patterns are absent because the language provides directly what they were reconstructing.
The antipatterns share a single root, which is using a mechanism to stop the
compiler talking rather than to answer what it asked. clone, unwrap,
Rc<RefCell<T>> and a speculative lifetime parameter are all legitimate tools
and all common ways to avoid a decision. The question that separates the two
cases is the same every time: can you say what the design reason was?
That question is what this book has been about. The compiler asks who owns this, who may change it, how long it lives, and what states it can be in. Those are questions about your program, not about Rust, and they had answers before you started writing Rust. Now they are written down, and something checks them.
Questions to think through
Pick three patterns from the Gang of Four catalogue and decide what each becomes in Rust. Which ones disappear entirely, and what feature replaced them?
Search a Rust project you have written for .clone(). For each one, write the
sentence explaining why a second independent value was needed. How many
sentences are about the design, and how many are about the compiler?
The chapter claims the antipatterns share one root. Test it: take one you recognize from your own code and ask what question you were avoiding when you wrote it.
Appendix A: Habits and Their Replacements
This appendix is a lookup table for the reflexes you brought with you. Each entry names a habit, says why it was correct where you learned it, and gives the Rust move that replaces it, with the chapter that explains why.
The habits are not mistakes. Every one of them was the right answer to a real constraint in the language it came from, and knowing which constraint it answered is what makes it possible to put down.
From Java and C#
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
Check for null before use | Any reference might be null | The type says whether a value can be absent; handle None where it appears | 7 |
| Extend an abstract base class | Bundled interface, reuse and shared fields | Trait for the interface, default methods for shared behavior, a field for shared data | 9, 11 |
| Copy a collection defensively before passing it | Callee might mutate it | A &T cannot mutate; nothing to defend against | 3 |
| Getters and setters on every field | Fields are the class’s business | Public fields where the value is just data; methods where an invariant needs upholding | 5, 6 |
try/finally to close resources | Exceptions can skip the close | Drop runs on every path, including panic | 1, 17 |
| Declare a variable, assign it later | Fields have default values | Build the value and return it; there is no partly built state | 5 |
| Interface with a single implementing class | Dependency injection wants one | A trait with one implementor is usually not paying; use the concrete type | 9 |
| Throw on invalid input | Exceptions are the error channel | Return Result; panic only for defects in your own code | 8 |
instanceof then cast | Recovering a subtype | match on an enum, which binds the data as it tests | 7 |
A status String or int field | Enums were weak | An enum with data per variant, checked exhaustively | 6, 7 |
| Synchronize on a lock object | Lock and data are separate | Mutex<T> owns the data; access requires locking | 14 |
equals and hashCode by hand | Inherited defaults compare identity | #[derive(PartialEq, Eq, Hash)], or write them when field-by-field is wrong | 5 |
The largest single adjustment is the first one. Null checking is not replaced by more careful null checking; it is replaced by the observation that a nullable reference was two types pretending to be one.
From Python and Ruby
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
| Duck typing | Runtime dispatch on whatever is there | Trait bounds: say which capability you need and the compiler checks | 9 |
| Dictionaries as ad-hoc records | No cost to a dict | A struct, which is faster and names its fields | 5 |
None as a sentinel returned from anything | Dynamic types absorb it | Option<T> in the signature, so callers cannot ignore it | 7 |
| Mutating a list while iterating, carefully | It usually works | iter_mut to change elements, retain to remove, collect-then-append to add | 3, 13 |
try/except around anything that might fail | Exceptions are cheap and pervasive | Result and ?, which propagate without hiding the path | 8 |
| Monkey-patching a library class | Classes are open | An extension trait, which is scoped and checked | 9, 17 |
| Passing dicts or tuples between functions | Structure is implicit | A struct or enum, so that adding a field is checked | 5, 6 |
| Everything is a reference and lives forever | The collector deals with it | Decide what owns each value; most designs are trees | 2 |
The adjustment here is less about safety and more about when you find out. Almost everything on this list moves a discovery from runtime to build time, and the cost is having to be specific earlier than you are used to.
From Go
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
if err != nil after every call | Errors are values, checked by convention | ?, which is the same idea with the boilerplate removed and the check enforced | 8 |
interface{} or generics with any | Escape hatch for heterogeneous data | An enum when the set is closed, Box<dyn Trait> when it is open | 7, 10 |
| A struct with an embedded struct | Go’s promotion gives delegation free | An explicit field and delegating methods; there is no promotion | 11 |
| Return a zero value and a bool | No sum types | Option<T> or Result<T, E> | 7, 8 |
| Share memory by communicating | Channels are the idiomatic primitive | Channels work the same way, and moving a value into one really does hand it over | 14 |
defer for cleanup | Cleanup is attached to the function | Drop attaches it to the value instead, so it cannot be forgotten at a new return | 1, 17 |
| Nil pointer receivers as an option | Zero values are useful | Option<T>, or a type that cannot be constructed invalid | 6 |
| Small interfaces | Go’s culture already says this | Keep it: small traits compose, large ones force stub implementations | 9 |
Go is the closest starting point for the error handling and concurrency chapters, and the furthest for anything involving ownership, since Go’s collector makes the same design question optional.
From C and C++
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
Manual free, or a comment saying who owns it | Nothing else tracked it | Ownership is in the type; destruction is scheduled by scope | 2 |
const& parameters to avoid copies | Copies could be deep and expensive | Moves are cheap; pass by value freely, borrow when you need the caller to keep it | 2 |
| Raw pointers for optional values | No standard optional until C++17 | Option<T>, which is the same size as a pointer for reference types | 7 |
| Rule of three, five, zero | Copy and move semantics are yours to define | Clone when duplication means something, Drop when destruction does, and nothing otherwise | 5 |
| Header and implementation split | The compilation model demands it | Modules and pub; there is no header | 15 |
| Templates for compile-time polymorphism | Zero-cost abstraction | Generics, with bounds checked at definition rather than at instantiation | 10 |
std::shared_ptr for shared ownership | Reference counting is the general answer | Rc or Arc, deliberately, after considering single ownership | 12 |
| Iterator invalidation rules memorized | The standard documents them per container | The compiler enforces them; the rules do not need memorizing | 3 |
C++ programmers usually find the model familiar and the enforcement unfamiliar. The most useful adjustment is the second row: the instinct to avoid copies is imported from a language where a copy could be arbitrarily expensive, and it produces borrowed designs in Rust that would have been simpler owned.
The habits with no replacement
Three things you may be looking for do not exist, and looking for a substitute wastes more time than accepting the absence.
There is no inheritance. Not a hidden version, not a crate that provides it. Composition and traits cover what it was doing, and chapter 11 works through the translation.
There is no reflection. No runtime type inspection, no dynamic class loading,
no annotation scanning. What frameworks in other languages do with reflection,
Rust does with derive macros at compile time — which is why serde needs
#[derive(Serialize)] on your type rather than discovering its fields at
runtime.
There are no exceptions. panic! is not one, catch_unwind is not a
substitute, and building control flow on either is a way of writing a different
language badly.
Appendix B: Reading Compiler Errors as Design Feedback
The argument running through this book is that the Rust compiler mostly asks design questions. This appendix makes that concrete: the errors you will meet most often, what each one is actually asking, and the menu of answers.
Two habits are worth building before the list. First, read the whole message —
the note and help lines below the error usually name the fix, and the first line
alone is the least useful part. Second, run rustc --explain E0502 (or any
code), which prints a page-long explanation with examples.
Ownership
E0382 — use of moved value
What it says: a value was moved on one line and used on a later one.
What it is asking: who owns this now? You handed responsibility to someone else and then carried on as though you had not.
The answers: if the other side only needed to read it, change the signature to
&T and stop moving it. If both sides need it at different times, restructure so
they take turns. If both genuinely need independent values, clone, and be able to
say why. If both need to refer to the same logical thing, give it an identifier
and let one place own the storage. If the value must be owned by a long-lived
structure and referred to from several places, that is the chapter 12 case.
E0507 — cannot move out of borrowed content
What it says: you tried to take a field or an element out through a & or
&mut.
What it is asking: the caller still owns this and is entitled to find it whole. What should be left in the hole?
The answers: clone the field if you need an owned copy. Use
std::mem::take or std::mem::replace to swap something in, which is the right
answer for state machines. Change the method to take self by value if it is
consuming the value anyway. Or return a reference instead of an owned value, if
the caller only needs to look.
E0505 — cannot move out of a value because it is borrowed
What it is asking: something is still looking at this. The move would leave a reference pointing at nothing.
The answer: end the borrow first, usually by moving the line that uses it above the move, or by extracting what you need into an owned value.
Borrowing
E0502 — cannot borrow as mutable because it is also borrowed as immutable
What it says: a read and a write overlap.
What it is asking: do these two accesses really need to happen at the same time?
The answers: most often they do not, and reading the value you need into a local before the mutation ends the borrow. If the accesses are to different fields, use the fields directly rather than going through methods that borrow the whole struct. If they are to different fields through methods, consider splitting the struct. If they must interleave over the same data, the design needs indices or interior mutability, in that order of preference.
E0499 — cannot borrow as mutable more than once
What it is asking: which of these two writers should be the writer?
The answers: the same menu as E0502. Additionally, if you are inside a loop holding a mutable borrow across iterations, check whether the borrow can be re-taken each time instead.
E0596 — cannot borrow as mutable, not declared as mutable
What it is asking: did you mean to change this?
The answer: add mut to the binding, or to the parameter type, and if adding
it feels wrong, that is information — a value nobody declared mutable is usually
one nobody intended to change.
Lifetimes
E0597 — borrowed value does not live long enough
What it says: a reference outlives what it points at.
What it is asking: which of these two should live longer?
The answers: declare the referent in an outer scope, so it outlives the reference. Or make the holder own its data instead of borrowing, which is the right answer whenever the holder is stored rather than temporary. Or restructure so the borrow is used where the data lives rather than carried out of that scope.
E0515 — cannot return reference to local variable
What it is asking: you built this inside the function and are trying to hand out a view of it, but it dies when the function returns.
The answers: return the owned value. Or take a destination parameter to write into. Or, if the thing being referenced came from an argument, return a reference derived from that argument instead and let elision or an explicit lifetime say so.
E0106 — missing lifetime specifier
What it is asking: which input does the returned reference borrow from?
The answers: decide, then annotate. If the answer is “neither, it is something
I constructed”, return an owned value. Resist the temptation to try 'a in
various positions until it builds; an over-strict signature compiles and
constrains every caller forever.
E0373 — closure may outlive the current function
What it is asking: this closure borrows a local and is going somewhere the local will not follow, usually another thread.
The answers: add move to make the closure own its captures. If it must
borrow, use std::thread::scope, which guarantees the threads end before the
scope does. If it captures something that must be shared, Arc it.
Types and traits
E0308 — mismatched types
What it is asking: usually nothing deep, and occasionally something important.
The shallow cases are a missing &, an extra *, a String where a &str was
wanted, or a u32 where a usize was. The one worth pausing on is a mismatch
between two newtypes, or between a wrapped and an unwrapped value, because that
is the type system catching the mix-up it was introduced to catch.
Check the expected and found types carefully when generics are involved; the message names them in full, and the difference is often one type parameter deep.
E0277 — the trait bound is not satisfied
What it says: a type does not have a capability something required.
What it is asking: should this type gain the capability, or should the requirement be relaxed?
The answers: implement or derive the trait, if it makes sense for the type.
Convert to a type that has it. Or relax the bound, if the function does not
really need what it asked for. When the trait is one you have never heard of —
FromIterator, TryInto, Pattern — the help text usually names an
implementation that exists and a conversion that gets you to it.
A special case worth recognizing: the trait Sized is not implemented means you
are trying to use a value whose size is unknown at compile time, typically a
dyn Trait or a str without a pointer in front of it. The answer is a Box,
an Rc, or a reference.
E0599 — no method named X found
What it is asking: the method might be in a trait that is not in scope.
The answers: check whether you need a use for the trait, which is the most
common cause and is why crates ship a prelude. Check whether you are calling a
method that exists on T while holding a &T that needs dereferencing, or vice
versa. And check whether the method exists only under a bound your type does not
satisfy, which the error will usually say.
E0038 — the trait cannot be made into an object
What it says: you wrote dyn Trait for a trait that is not object safe.
What it is asking: this trait has a method that cannot go in a vtable, usually a
generic method or one returning Self.
The answers: move the offending method behind where Self: Sized, which keeps
it available on concrete types and excludes it from the object. Return
Box<Self> instead of Self. Split the trait into an object-safe part and a
rest. Or use generics instead of trait objects, if a heterogeneous collection was
not what you needed.
The general method
When an error does not match anything above, the loop that works is the same one chapter 1 described.
Read the message, all of it, including the notes. Then say out loud what the compiler believes: it thinks this reference will outlive the data, or it thinks two things want to write to this. Then ask whether it is right. It usually is, and the case where it is right is a bug you have not found yet rather than an inconvenience.
If it is right, the question is which of the design answers above applies. If you believe it is wrong, the useful move is to narrow the example until you have a dozen lines that still fail, at which point either the misunderstanding becomes obvious or you have a minimal reproduction worth asking about.
What does not work is trying syntax until the error goes away. Annotations added
by guesswork produce signatures that constrain callers for reasons nobody
remembers, and clone added by guesswork produces programs that are slower and
no clearer. The error was a question. It is worth answering.