How it works
The explanation, with examples you can run.
Shared or mutable
fn total_len(words: &[String]) -> usize { words.iter().map(|w| w.len()).sum() // shared borrow: read only} fn add_bang(s: &mut String) { s.push('!'); // mutable borrow: may change it} fn main() { let mut words = vec![String::from("hello"), String::from("world")]; let first = &words[0]; // shared borrow starts println!("{first}"); // ...and ends here, after its last use add_bang(&mut words[1]); // so a mutable borrow is fine now println!("{} {}", total_len(&words), words[1]);}It prints hello and 11 world!. &[String] is a slice: a borrowed view of part or all of a vector or array, made of a pointer and a length. Taking &[T] or &str instead of &Vec<T> or &String lets callers pass any contiguous data.
What the rule prevents
Move the println!("{first}") after a words.push(...) and the compiler stops you:
error[E0502]: cannot borrow `words` as mutable because it is also borrowed as immutable |3 | let first = &words[0]; | ----- immutable borrow occurs here4 | words.push(String::from("world")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereThis is a real bug in other languages: push may reallocate the buffer, and first would then point at freed memory. In C++ it is undefined behaviour; in Java the equivalent iterator throws ConcurrentModificationException at run time. Rust rejects it before the program runs. The same rule, applied across threads, is what rules out data races.
Borrows end at their last use
The checker works on the control-flow graph, not on braces: a borrow is live from where it is created to where it is last used. This is non-lexical lifetimes (NLL), introduced with Rust 2018; since Rust 1.63 it is the only borrow checker, in every edition. It is why the first example compiles: first is no longer used when add_bang borrows mutably.
Practical patterns
- Shorten borrows. Copy out what you need (
let n = v.len();) before mutating. - Borrow disjoint parts. The checker understands separate struct fields, so
&mut s.aand&mut s.bcan coexist. For slices,split_at_mutgives two non-overlapping mutable halves. - Use indices or keys instead of long-lived references into a collection you keep changing.
- Pass references by default. A function that only reads should take
&T; takeTonly when it needs to keep the value.
References are checked statically and cost nothing at run time: a &T is just a pointer. When the static rules are too strict, for example a value with several owners, the smart pointers Rc, RefCell and friends move some of the checking to run time.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.