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?