7. Borrowing: shared XOR mutable
Moving values everywhere would be impractical. Usually you want to let code use a value without taking ownership. That is a borrow, written as a reference.
7.1 The rule
At any moment, a value can have either:
- any number of shared references
&T(read-only), or - exactly one mutable reference
&mut T(read-write),
and no reference may outlive the value it points to.
fn total_len(words: &[String]) -> usize { // borrows; the caller keeps ownership
words.iter().map(|w| w.len()).sum()
}
fn shout(words: &mut Vec<String>) { // exclusive borrow; may modify
for w in words.iter_mut() {
*w = w.to_uppercase();
}
words.push("!".to_string());
}
fn main() {
let mut words = vec!["hello".to_string(), "world".to_string()];
let n = total_len(&words); // shared borrow, ends when the call returns
shout(&mut words); // mutable borrow, ends when the call returns
println!("{n} {words:?}"); // owner still valid: ["HELLO", "WORLD", "!"]
}
7.2 Why the rule exists: iterator invalidation
// DOES NOT COMPILE
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow into v's heap buffer
v.push(4); // needs &mut v, and push may REALLOCATE the buffer
println!("{first}"); // first would point into freed memory
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
| let first = &v[0];
| - immutable borrow occurs here
| v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
| println!("{first}");
| ------- immutable borrow later used here
This is the same bug as Go's append aliasing trap and C's realloc-then-dangling-pointer bug. In Go it silently corrupts data. In C it is UB. In Rust it does not compile.
"Shared XOR mutable" rules out a whole category of bugs at once:
| Bug | Why it cannot happen |
|---|---|
| Iterator invalidation | you cannot mutate a collection while a borrow of it is alive |
| Use-after-free via a reference | a reference cannot outlive its owner |
| Data races | a data race needs two threads with access, at least one writing; that is shared AND mutable at once (Ch 28) |
| Aliasing surprises for the optimizer | &mut T is guaranteed unique, so LLVM gets noalias information C only gets from restrict |
7.3 Borrows last until their last use (non-lexical lifetimes)
fn main() {
let mut scores = vec![10, 20, 30];
let max = scores.iter().max().copied(); // `.copied()` turns Option<&i32> into Option<i32>
// The shared borrow from iter() ended at the end of the line above,
// because `max` holds a copy, not a reference.
scores.push(40);
println!("{max:?} {scores:?}");
let first = &scores[0];
println!("{first}"); // last use of `first`: the borrow ends here
scores.push(50); // so this is fine
}
Most of the friction with the borrow checker goes away once you remember that a borrow ends at its last use, not at the end of the block.
7.4 Dereferencing and auto-ref
fn bump(n: &mut i32) {
*n += 1; // `*` dereferences: write through the reference
}
fn main() {
let mut count = 0;
bump(&mut count);
let s = String::from("abc");
let r = &s;
println!("{} {}", r.len(), (*r).len()); // method calls auto-dereference: r.len() just works
println!("{count}");
}
The . operator adds &, &mut, or * as needed to make a method call
type-check. That is why you rarely write * outside assignments.
7.5 The four ways to fix a borrow-checker error
When you hit E0499, E0502, or E0505, one of these almost always works:
- Shorten the borrow. Copy or clone the small value you need
(
let id = user.id;) before the mutation. - Reorder. Finish reading before you start writing.
- Split the borrow. Borrow disjoint fields
(
let (a, b) = (&mut s.left, &mut s.right);) or slices (v.split_at_mut(mid)). - Restructure ownership. Use indices instead of references (Ch 27), an entry API (Ch 11), or move the data instead of borrowing it.
Wrapping everything in Rc<RefCell<T>> also works, but it moves the checks
to runtime. Treat it as a last resort (Ch 22, 68).