Borrowing
In one sentence
Section titled “In one sentence”A reference borrows a value without owning it: many shared &T readers or one exclusive &mut T writer, never both, checked by the compiler.
The Java you know
Section titled “The Java you know”Java has one kind of reference. Any code that has a reference can read the object, change it, or give the reference to another thread. The rules about who may change what are in your head, in comments, or in a lock.
The classic result is a ConcurrentModificationException. The loop reads the list through an iterator, and the body changes the same list:
Java
static void removeCancelled(List<Order> orders) { for (Order order : orders) { if (order.cancelled()) { orders.remove(order); // ConcurrentModificationException at run time } }}Rust
for (i, order) in orders.iter().enumerate() { // shared borrow of `orders`... if order.cancelled { orders.remove(i); // ...so a mutable borrow is not allowed: E0502 }}In Java, this compiles and fails at run time (if you are lucky, the first time). In Rust, the same code does not compile. Error E0502 says: you cannot borrow orders as mutable, because it is also borrowed as immutable. The loop holds the shared borrow, and remove needs an exclusive one.
The Rust you will see
Section titled “The Rust you will see”Rust has two kinds of reference:
| Type | Name | How many at a time | Can change the value |
|---|---|---|---|
&T |
shared reference, shared borrow | any number | no |
&mut T |
mutable reference, exclusive borrow | exactly one, and no &T at the same time |
yes |
There is no exact Java equivalent. The nearest idea: &T is a read-only view, and &mut T is a read-write view plus an exclusive lock. The difference is that the compiler checks the rules, and there is no cost at run time.
The fix for the loop above is the standard library method that does the iteration and the removal together. It is the same fix as in Java:
pub fn remove_cancelled(orders: &mut Vec<Order>) { orders.retain(|order| !order.cancelled);}How to read it
Section titled “How to read it”A borrow lasts from where it is made to where it is last used, not to the end of the block. This is called non-lexical lifetimes (NLL). It explains why code that looks like it breaks the rules compiles:
let first = &orders[0]; // shared borrowlet count = orders.len(); // another shared borrow: fineprintln!("first is {}, {count} in total", first.id);// `first` is not used after this line, so its borrow ends here
let last = orders.last_mut().expect("at least one order"); // mutable borrowlast.cancelled = true; // fine: no shared borrow is still in use- Line 1:
&orders[0]makes a shared borrow oforders. The namefirstholds it. - Line 2: Another shared borrow. Any number of shared borrows can exist together.
- Line 3: The last use of
first. Its borrow ends here, not at the end of the block. - Line 6:
last_mut()makes an exclusive borrow. This is allowed, because no shared borrow is still in use. - Line 7: Change through the exclusive borrow. If line 3 came after this line, the code would not compile.
When you read Rust, look for the & and &mut:
- In a parameter,
&Tmeans “I read it”, and&mut Tmeans “I change it”. The caller keeps ownership in both cases. - On a method,
&selfreads the object,&mut selfchanges it, andselfconsumes it. - At a call site,
f(&x)andf(&mut x)show you what the function may do tox.
Why this removes a class of bugs
Section titled “Why this removes a class of bugs”The rule “one writer or many readers” is the rule that prevents data races. A data race needs two threads that access the same memory at the same time, where at least one of them writes. Rust’s borrow rules make that impossible in safe code, also across threads. This is why Rust can say “fearless concurrency”. See Threads and shared state.
The same rule also prevents single-threaded bugs: iterator invalidation (the loop above), a reference into a Vec that becomes invalid when the Vec grows, and “who changed my object” surprises.
Auto-deref and reborrowing, at a reading level
Section titled “Auto-deref and reborrowing, at a reading level”You do not often see * in Rust code. The . operator follows references for you, so order.id works for an Order, a &Order and a &mut Order. You see * mainly in arithmetic and comparisons, where you need the value itself:
let id = order.id; // no `(*order).id`: field access dereferences for youlet cents: &u64 = &order.total_cents;let doubled = *cents * 2; // arithmetic needs the value: `*` dereferencesA &mut T that you pass to a function is “reborrowed” for the call. The function gets a temporary exclusive borrow, and the original &mut is usable again after the call. This is why you can call two &mut self methods one after the other on the same value. You do not need to understand the details to review code. Read it as “lend it for the duration of the call”.
What good looks like
Section titled “What good looks like”- Parameters take the most general borrowed form.
&strinstead of&String,&[T]instead of&Vec<T>,&Tinstead of&Box<T>. Every owned form converts to these, so callers have more freedom. See S06:&String,&Vec<T>,&Box<T>parameters. &selffor methods that only read.&mut selfonly when the method changes the object. A&mut selfgetter blocks all other access while it is in use, for no reason.- Short borrows. Take a reference, use it, and let it end. Do not keep references in long-lived variables.
- The borrow rules are followed, not avoided. When the compiler rejects a borrow, the good fix changes the structure: borrow later, split the work into two loops, or use a method like
retain. A clone is sometimes correct, but it is not the default fix.
Red flags in AI output
Section titled “Red flags in AI output”.clone()to make a borrow error go away. The most common AI fix for E0502 and E0499. Ask: what is the conflict, and does the copy hide a real bug? See S01:clone()to satisfy the borrow checker.&mut selfwhere&selfis sufficient. It compiles, but it stops callers from holding any other reference while they call the method. Agents addmutwhen they are not sure.&Vec<T>,&Stringand&Box<T>parameters. A direct translation ofList<T>andStringparameters. See S06:&String,&Vec<T>,&Box<T>parameters.- Indexes instead of references to avoid the borrow checker, for example storing
usizepositions in another struct and looking them up later. Sometimes this is correct (an arena), but often it hides a design that fights the rules. RefCellorRc<RefCell<T>>added only to get past a compile error. It moves the borrow check to run time, where a mistake is a panic. See Smart pointers.
- S01Severity: Medium
clone()to satisfy the borrow checkerA copy of the data is made only to make a borrow error go away. - S05Severity: LowOwned parameters (
String,Vec<T>) where a borrow is sufficientThe function takes ownership of data that it only reads. - S06Severity: Low
&String,&Vec<T>,&Box<T>parametersA reference to the container where a slice (&str,&[T]) or&Tis more flexible.
Better vs worse
Section titled “Better vs worse”The task: compute a cart total, and count the expensive prices.
Worse
totaltakes&mut selfbut only reads, andcount_expensivetakes&Vec<u64>.impl super::Cart {pub fn total(&mut self) -> u64 {self.prices.iter().sum()}}pub fn count_expensive(prices: &Vec<u64>, limit: u64) -> usize {prices.iter().filter(|&&p| p > limit).count()}Run in Playground (opens the Rust Playground in a new tab) Better
&selffor a read-only method, and a slice parameter that accepts any list of prices.impl super::Cart {pub fn total_cents(&self) -> u64 {self.prices.iter().sum()}}pub fn count_expensive(prices: &[u64], limit: u64) -> usize {prices.iter().filter(|&&p| p > limit).count()}Run in Playground (opens the Rust Playground in a new tab)
Both versions compile and give the same result. The “worse” version has two costs. A caller that holds &cart cannot call total(), so the caller must restructure its code or clone the cart. And count_expensive(&prices[..10], limit) does not compile with the &Vec version, so a caller with part of a list must copy it into a new Vec.
&mut self is correct when the method changes the object, or caches a result in it. If a “read” method needs &mut self to update a cache, ask if the cache belongs in the type.
Check yourself
Section titled “Check yourself”A loop iterates over
ordersand callsorders.push(...)in its body. What happens?Show the answer
It does not compile (E0502 or E0499). The loop holds a borrow of
ordersfor its whole duration. Collect the new orders in a secondVecand append them after the loop, or use a method that does both steps.A method
fn total(&mut self) -> u64only adds up numbers. Why is this a problem?Show the answer
It needs an exclusive borrow, so no other reference to the object can be in use when you call it. Code that holds
&cartcannot call it at all. Change it to&self.Code takes
let first = &v[0];, then callsv.push(1);, then usesfirst. Why does this not compile, and what bug does that prevent?Show the answer
pushneeds&mut vwhile the shared borrowfirstis still in use. If theVecgrows, it can move its elements to a new buffer, andfirstwould point to freed memory. In Java the garbage collector prevents this, but the same pattern causesConcurrentModificationExceptionwith iterators.When is it correct to clone to solve a borrow error?
Show the answer
When you need an independent copy anyway: the copy goes to another thread, into a struct that outlives the original, or the original changes and you need the old value. If the copy is only read and then thrown away, borrow instead.