Borrowing & References 🤝
Last lesson, giving a value to a function meant losing it. Ridiculous, right? Meet the fix: lending. This is where Rust starts feeling good.
Lending instead of giving: &
Put an ampersand in front of a value and you create a reference: a way to look at a value without owning it. Think of a library book 📚: you can read it, but the library still owns it, and you have to give it back.
fn count_letters(text: &String) -> usize {
text.len()
} // the borrow ends here: nothing is freed, we never owned it
fn main() {
let poem = String::from("crabs write free verse");
let n = count_letters(&poem); // lend it: &poem
println!("{poem} has {n} letters"); // ✅ poem is still ours!
}
Compare with Lesson 6's devour: no ownership lost, no clunky
hand-it-back dance. The & appears in two places: the function
says "I accept borrowed Strings" (&String), and the caller says
"here, borrow mine" (&poem). This is called borrowing.
Borrowing with edit rights: &mut
A plain & reference is read-only. To let someone modify
your value without taking it, lend it mutably:
fn add_signature(letter: &mut String) {
letter.push_str(" - sincerely, Rusty 🦀");
}
fn main() {
let mut letter = String::from("Dear ocean,");
add_signature(&mut letter); // lend with edit rights
println!("{letter}"); // changed, still ours
}
(The variable itself must be mut too; you can't lend edit rights
you don't have.)
The two rules of borrowing 📜
- 🟢 any number of read-only references (
&T), or - 🔴 exactly one mutable reference (
&mut T), never both.
In short: many readers or one writer.
Why so strict? Imagine ten people silently reading a shared document while one person rewrites it: chaos, confusion, readers seeing half-edited sentences. Every data race and a whole family of crashes come from exactly that: reading and writing the same data at once. Most languages let it happen and hope. Rust makes it unrepresentable:
fn main() {
let mut score = String::from("10 points");
let r1 = &score;
let r2 = &score; // ✅ two readers, fine
println!("{r1} {r2}");
let w = &mut score; // ✅ readers are done, one writer OK
w.push_str("!!!");
println!("{w}");
}
But overlap a reader and a writer, and the compiler steps in:
let mut score = String::from("10 points");
let reader = &score;
let writer = &mut score; // ❌ can't write while someone's reading
println!("{reader}");
error[E0502]: cannot borrow `score` as mutable because it is
also borrowed as immutable
|
2 | let reader = &score;
| ------ immutable borrow occurs here
3 | let writer = &mut score;
| ^^^^^^^^^^ mutable borrow occurs here
4 | println!("{reader}");
| -------- immutable borrow later used here
println!("{reader}") above the &mut
line and the example compiles. When the borrow checker complains, ask:
"can I finish one job before starting the next?" The answer is almost always yes.
Slices: borrowing a piece 🍰
You can also borrow just a portion of something. This finally explains
&str from Lesson 3; it's a borrowed view into text:
fn main() {
let song = String::from("under the sea");
let word: &str = &song[6..9]; // borrow characters 6,7,8
println!("{word}"); // "the"
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // borrow a window: [20, 30, 40]
println!("{middle:?}");
}
&str, not &String
A &str can view a whole String, a piece of one, or a
"literal", so fn shout(text: &str) accepts all three.
It's the friendlier doorway, and Rust converts &String to
&str automatically. Idiom to remember: own a
String, borrow a &str.
- Forgetting the
&at the call site:count_letters(poem)moves the value;count_letters(&poem)lends it. - Trying
&muton a variable that isn'tmut. - Two
&mutborrows alive at once; finish one job first.
Fix it with a borrow
This is Lesson 6's broken cake program. Fix it by changing devour
to borrow instead of take. You'll change exactly two characters.
fn devour(snack: String) {
println!("nom nom: {snack}");
}
fn main() {
let cake = String::from("carrot cake");
devour(cake);
println!("{cake} is still here!");
}
Reveal solution
fn devour(snack: &String) { // 1: accept a loan
println!("nom nom: {snack}");
}
fn main() {
let cake = String::from("carrot cake");
devour(&cake); // 2: lend, don't give
println!("{cake} is still here!");
}
(Even more idiomatic: snack: &str; see the note above.)
The decorator
Write fn decorate(s: &mut String) that wraps the text in
✨ sparkles (hint: push_str adds to the end;
insert_str(0, "✨") adds to the front). Call it on a String of your
choice and print the result.
Reveal solution
fn decorate(s: &mut String) {
s.insert_str(0, "✨ ");
s.push_str(" ✨");
}
fn main() {
let mut title = String::from("The Rusty School");
decorate(&mut title);
println!("{title}"); // ✨ The Rusty School ✨
}
Borrow-checker whisperer
This won't compile. Rearrange the lines (don't delete any!) so it does.
fn main() {
let mut inventory = String::from("rope");
let peek = &inventory;
inventory.push_str(", torch");
println!("peeked: {peek}");
println!("final: {inventory}");
}
Reveal solution
fn main() {
let mut inventory = String::from("rope");
let peek = &inventory;
println!("peeked: {peek}"); // finish reading FIRST
inventory.push_str(", torch"); // then write
println!("final: {inventory}");
}
The read-borrow peek ends at its last use, freeing
inventory to be modified. Many readers or one writer,
just not overlapping.