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.
A string literal is already a &str, and a borrowed String automatically becomes one at the call site.
That means the same parameter accepts both without making the caller convert anything.
Reach for &str by default when you only need to read text.
The body only needs to call .len() on the slice.
Pay more attention to the signature and the test: after the call, the caller can still use s because the function only borrowed it.
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.- Rust calls the automatic conversion from
&Stringto&stra "deref coercion." It is why the test can pass&sdirectly, with no.as_str()call.
/// 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.
The parameter is &mut String, not &mut str.
Appending text may require the owned String to grow its buffer, while a string slice has a fixed length.
The function does not need to return the String.
It changes the value through the reference, and the caller sees that change after the call returns.
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 verifies the basic borrowing syntax. These experiments focus on reading ownership errors and identifying the rule each one violates.
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. Once you can do that, you can change the code for a reason instead of guessing.
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 part of the lesson. Once you can explain one in a sentence, you can decide what ownership or borrow needs to change instead of guessing.