Chapter 3 — Shared and Mutable Borrowing
Goal
Use &T and &mut T to access a value without taking ownership, and predict when those accesses conflict.
Prediction
fn main() {
let mut task = String::from("write");
let view = &task;
task.push_str(" tests");
println!("{view}");
}
What happens?
A. It prints write tests; references always observe changes.
B. It prints write; references contain snapshots.
C. Compilation fails because mutation conflicts with the later use of view.
D. Compilation fails because push_str consumes the string.
Reveal: read the evidence
Answer: C.
error[E0502]: cannot borrow `task` as mutable because it is also borrowed as immutable
|
3 | let view = &task;
| ----- immutable borrow occurs here
4 | task.push_str(" tests");
| ^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{view}");
| ---- immutable borrow later used here
push_str needs mutable access to task. The compiler sees shared access still needed afterward, so the accesses overlap.
Build the model
Borrowing grants temporary access while leaving ownership where it is.
fn byte_count(text: &String) -> usize {
text.len()
}
fn main() {
let task = String::from("write");
let count = byte_count(&task);
println!("{task}: {count}");
}
&task creates a shared reference. byte_count may read the String, but cannot replace or mutate it. The owner remains task, so it is usable after the call.
Mutable borrowing grants temporary exclusive access:
fn finish(text: &mut String) {
text.push_str(": done");
}
fn main() {
let mut task = String::from("write");
finish(&mut task);
println!("{task}");
}
Working model: at a given moment you may have either many shared references or one mutable reference, not both. Shared means “read without exclusive control.” Mutable means “exclusive access, including permission to write.”
This rule prevents two broad classes of bugs: observing a collection while an operation relocates its storage, and unsynchronized read/write or write/write access to the same value. For example, appending to a String may allocate a larger buffer. A reference into the old buffer must not remain usable across that append.
The owner is also restricted while its value is borrowed. task.push_str(...) behaves as a mutable borrow of task, so it cannot overlap the active shared borrow in the prediction.
Precise model: Rust checks places and uses, not merely variable names. References may be reborrowed, and the compiler can shorten a borrow to its last use. Types with interior mutability can permit mutation through shared references by moving checks to runtime or using synchronization, but ordinary &T does not permit mutation. Start with the ordinary rule; reach for interior-mutability types only when the design truly requires them.
Simplest fix
Finish reading before mutation:
fn main() {
let mut task = String::from("write");
let view = &task;
println!("before: {view}");
task.push_str(" tests");
println!("after: {task}");
}
Because view is not used after the first println!, its borrow ends there. The mutation is then exclusive.
Tradeoff alternative: clone the snapshot
fn main() {
let mut task = String::from("write");
let before = task.clone();
task.push_str(" tests");
println!("{before} -> {task}");
}
Cloning is appropriate when you need an owned historical snapshot. It costs allocation and copying; shortening the borrow costs nothing and should be the default when the old view is not independently needed.
Mini challenge
Will this compile?
fn main() {
let mut total = 0;
let left = &mut total;
let right = &mut total;
*left += 1;
*right += 1;
}
A. Yes; both references came from a mutable binding.
B. Yes; integers are Copy.
C. No; two mutable borrows are active at once.
D. No; references cannot modify integers.
Answer
C. left is used after right is created, so their exclusive borrows overlap. A simple sequential version works:
let mut total = 0;
*(&mut total) += 1;
*(&mut total) += 1;
Usually write the clearer total += 1 twice; explicit references add nothing here.