Why are Rust developers so frugal? They prefer to borrow.
The functions chapter had a nuisance baked in.
Pass a String to a function and the move rules say you've handed it over.
Want to use it afterwards? Too bad, it's gone.
Returning it back out works but gets tedious fast, especially when the function only needed to read the value.
Borrowing is the fix.
A borrow lets a function use a value without taking ownership of it: &value for a shared, read-only borrow, and &mut value for an exclusive, writable one.
fn length(s: &String) -> usize { s.len() } // borrows, doesn't take
let s = String::from("rust");
let n = length(&s); // lend s out for the call
println!("{s}"); // s still owns the data
The caller keeps ownership the whole time. The function gets temporary access and gives it back when it returns.
Borrows come with a single rule, and the borrow checker enforces it everywhere:
At any moment you can have either any number of shared
&references, or exactly one&mutreference. Never both at once.
A shared reference promises the data won't change while you're looking at it. A mutable reference promises nobody else is looking while you write to it. Allow both at once and you'd have someone reading a value halfway through someone else's change, which is the classic data race. Rust rules that out at compile time instead of trusting you to get the locking right.
When the compiler rejects your code with a borrowing error, this rule is the first thing to check.
Most of the time a function only needs to look at a value, not own it.
That's a shared borrow, written &T in the signature.
The caller keeps ownership; the callee gets temporary read-only access.
This function takes &str rather than &String.
&str is the universal "borrowed string slice" type: a string literal is already a &'static str, and &String automatically coerces to &str, so &str parameters accept both without forcing the caller to convert.
Reach for &str by default when you're just reading.
The body is a one-liner: call .len() on the slice.
The point of the exercise is the signature: notice that after the call, the caller's s is still usable in the test below.
Useful from the standard library
str::lenis the byte length of the slice. The chapter on strings covers why that's not the same as a character count.- The "deref coercion" from
&Stringto&stris what lets the test pass&sdirectly. No.as_str()needed.
/// Borrows a string reference without taking ownership.
/// The original string remains valid after this function returns.
fn borrow_string(s: &str) -> usize {
s.len()
}
#[test]
fn test_borrow_string() {
let s = "The Matrix has you";
let len = borrow_string(s);
// s is still valid here because we only borrowed it
assert_eq!(len, 18);
assert_eq!(s, "The Matrix has you"); // Still here!
}
Sometimes you want to modify a value in place without taking ownership of it.
That's a mutable borrow: &mut T.
The caller still owns the value, but the callee gets exclusive write access for the duration of the call.
Two things to notice in the signature:
&mut String, not &mut str.
We need the owned String because growing it (with push_str) may reallocate; a bare string slice has a fixed length.On the call site (see the test): the caller has to write &mut s explicitly, and s itself has to have been declared let mut s = ....
Mutability is opt-in at every layer.
Useful from the standard library
String::push_strworks on&mut Stringexactly the same way as on an ownedString. The compiler reaches through the reference for you.String::pushis the single-charversion, in case you want to append one character at a time.
/// Takes a mutable reference to modify the string in place.
/// The &mut allows us to change the string's contents.
fn mutate_string(s: &mut String) {
s.push_str(" - now with extra crab");
}
#[test]
fn test_mutate_string() {
let mut s = String::from("Ferris");
mutate_string(&mut s);
assert_eq!(s, "Ferris - now with extra crab");
}
Passing the previous tests is the easy part of this chapter.
Ownership only really clicks once you've seen the canonical errors with your own eyes, so the messages feel familiar later (the iterators chapter, the ? operator chapter, ...) instead of like a brick wall.
Each test below is paired with a commented-out line. Uncomment one at a time, run the tests, read the error carefully, then comment it out again before moving on. The errors are the lesson here. The tests themselves don't assert anything interesting.
The three errors you'll trigger correspond to the three rules of the borrow checker:
Re-read each compiler message until you can explain in one sentence why the compiler is complaining. That's the muscle this chapter is building.
Useful from the standard library
Clone::clonemakes an explicit deep copy when you genuinely need two owners. A good "escape hatch" once you've understood why a borrow won't compile, but not the first thing to reach for.- The compiler errors themselves are the documentation here. Each one is a paragraph you'd otherwise have to read in a book.
/// Takes ownership of a String and modifies it.
fn take_ownership(s: String) -> String {
let mut s = s;
s.push_str(" - owned by Rust!");
s
}
/// Takes a mutable reference to modify the string in place.
fn mutate_string(s: &mut String) {
s.push_str(" - now with extra crab");
}
#[test]
fn experiment_use_after_move() {
let s = String::from("Rust");
let _result = take_ownership(s);
// Uncomment the next line. Expect: "borrow of moved value: `s`".
// assert_eq!(s, "Rust");
}
#[test]
fn experiment_two_mutable_borrows() {
let mut s = String::from("Ferris");
let r1 = &mut s;
// Uncomment the next two lines together. Expect:
// "cannot borrow `s` as mutable more than once at a time".
// let r2 = &mut s;
// r2.push('!');
r1.push('!');
}
#[test]
fn experiment_mix_shared_and_mutable() {
let mut s = String::from("Ferris");
let shared = &s;
// Uncomment the next line. Expect:
// "cannot borrow `s` as mutable because it is also borrowed as immutable".
// mutate_string(&mut s);
println!("{shared}");
}
You borrowed a String read-only as &str, mutated one through &mut String, and triggered the borrow checker's three canonical errors on purpose.
What we learned
- A borrow lets you read or modify a value without taking ownership.
&Tis a shared, read-only borrow;&mut Tis an exclusive, writable one.- The borrow-checker rule: at any moment, either any number of
&Tborrows or exactly one&mut T, never both. That's what rules out data races at compile time.- Mutability is opt-in at every layer: the binding (
let mut x), the parameter (&mut T), and the call site (&mut x).- Default to
&strover&String(and&[T]over&Vec<T>) for read-only parameters. Slice types accept more callers thanks to deref coercion.- The compiler errors are the lesson. Once you can say in one sentence why the compiler is complaining, you've built the muscle this chapter is for.