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?