07. References and Borrowing (&T, &mut T, the borrow rules)
Borrowing lets you use a value without taking ownership — `&` for an immutable reference, `&mut` for a mutable one. The core rule is that at any moment you have either (N shared references) OR (1 exclusive mutable reference), never mixed. This is what blocks data races at compile time.
What you'll learn
- 1Create references with `&` and `&mut`, and dereference them
- 2State the borrow rule (N shared or 1 exclusive)
- 3Take references as function arguments to avoid move
- 4Read and fix borrow-checker errors
- 5Understand NLL (Non-Lexical Lifetimes) and reference scoping
Overview
After lesson 06's move, passing a value to a function means losing it. Returning to keep using it is ugly. Borrowing fixes this — and on the same axis it **prevents data races at compile time**.
Core Concepts
1) Two kinds of references
- **&T** — shared (immutable) reference. Read-only. Multiple allowed
- **&mut T** — exclusive (mutable) reference. Read-write. Only one at a time
2) The borrow rule (one-liner)
**At any moment for a given value, you have (N shared references) OR (1 exclusive reference) — never both.**
3) Why? — preventing data races
If one thread is reading while another writes, you have a race. Rust prevents this **even in single-threaded code**, and the same rule scales to multi-threaded contexts. One exclusive mutator at a time = no races.
4) NLL — Non-Lexical Lifetimes
A reference is valid up to its **last use**, not the end of the enclosing scope. This makes shared → exclusive → shared sequences just work naturally.
| Combination | Allowed? |
|---|---|
| multiple &x | ✓ |
| single &mut x | ✓ |
| &x + &x | ✓ |
| &x + &mut x | ✗ |
| &mut x + &mut x | ✗ |
Hands-on Examples
Pass by reference — no move:
fn print_len(s: &String) {
println!("len={}", s.len());
}
fn main() {
let s = String::from("hello");
print_len(&s); // lent
println!("{}", s); // OK — s still alive
}Mutate through &mut:
fn add_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // hello world
}Two simultaneous mutable borrows are a compile error:
let mut x = 5;
let r1 = &mut x;
let r2 = &mut x; // error[E0499]: cannot borrow `x` as mutable more than once
println!("{} {}", r1, r2);NLL — after last use, a new exclusive borrow is fine:
let mut x = 5;
let r1 = &x;
let r2 = &x;
println!("{} {}", r1, r2); // r1, r2 last used here
let r3 = &mut x; // OK — the shared ones are done
*r3 += 1;
println!("{}", r3);Common Mistakes
Q. Do I have to write `&mut` on both sides of the call?
A. Yes — the caller writes `add_world(&mut s)` explicitly. Reading the call site tells you immediately where mutation happens.
Q. Do references auto-deref for method calls?
A. Yes — `s.len()` works on a reference because the compiler inserts deref automatically. For raw dereference use `*r`.
Q. The borrow checker feels too strict. What now?
A. Read the error message — Rust's compiler nearly always suggests a fix. Usually a small restructuring (tightening scopes, avoiding shared+exclusive at the same time) unblocks you.
Recap
- Borrow = lend without transferring ownership: `&`, `&mut`
- (N shared) OR (1 exclusive) at any time — never mixed
- Data-race prevention is enforced even single-threaded
- NLL lets you re-borrow after the previous reference's last use
Try It Yourself
- Write a length-printing function in both reference and owning forms; observe the caller-side difference
- Write `fn add_one(v: &mut Vec<i32>)` that adds 1 to every element
- Create two `&mut` to the same variable on purpose to reproduce the error, then copy the message
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗