I once heard a Rust joke I liked so much that I borrowed it for this chapter.
In the moves chapter, take_ownership took a String
and returned it. That works, but passing ownership back and forth gets tedious
fast when the function only needs to read the value.
Instead, a better approach is to let the function "borrow" the value.
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.
For the same value, the borrowing rule is:
While references are in use, you can have either any number of shared
&references, or one exclusive&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. For a
String, this prevents a write from reallocating the buffer while another
reference still points into it. Across threads, the same restriction also helps
prevent data races.
Borrowing can take a few tries to get used to, especially when you have to work out where a borrow ends. When the compiler rejects your code, I suggest starting with this rule: who needs to read the value, and who needs to change it?
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 read and grow its
owned String 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." A caller with aStringcan pass&swithout calling.as_str(); the tests cover both an ownedStringand a string literal.
/// 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 mut s = String::from("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");
s.push_str(", Neo");
assert_eq!(s, "The Matrix has you, Neo");
assert_eq!(borrow_string(&s), 23);
}
#[test]
fn test_borrow_string_literal() {
assert_eq!(borrow_string("Ferris"), 6);
}
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.
At the call site (see the test), you need both &mut s and a binding declared
with 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.
/// 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");
}
These tests pass as written. Uncomment the lines in one experiment at a time to see what the compiler rejects. Before you do, predict which line will cause an error and why. Then run the tests and compare the error with your prediction.
experiment_use_after_move, uncomment the assertion. Fix it without
cloning the string or changing take_ownership.experiment_two_mutable_borrows, uncomment both lines. Fix the error by
reordering the existing statements. Keep both references and both pushes; the
final string should be Ferris!!.experiment_mix_shared_and_mutable, uncomment the mutation. Reorder the
statements, keeping both the print through shared and the mutation. The
print should show Ferris; the final string should be
Ferris - now with extra crab.For the borrowing experiments, don't clone the string, remove an operation, or add a nested scope. Mark the last use of each reference and predict whether your new order will compile before running it. Add an assertion for each final string. Once the tests pass, explain why the borrows no longer conflict even though the reference bindings are still in scope.
Keep each fix before moving on. To skip an experiment, comment out the lines you uncommented so its test passes again. Even if you run only one test, all tests in the file still have to compile.
/// 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);
// `s` moved into the function; `result` now owns the returned string.
assert_eq!(result, "Rust - owned by Rust!");
}
#[test]
fn experiment_two_mutable_borrows() {
let mut s = String::from("Ferris");
let r1 = &mut s;
r1.push('!');
// The borrow through `r1` ends at its last use, before we create `r2`.
let r2 = &mut s;
r2.push('!');
assert_eq!(s, "Ferris!!");
}
#[test]
fn experiment_mix_shared_and_mutable() {
let mut s = String::from("Ferris");
let shared = &s;
println!("{shared}");
// Nothing uses `shared` after this point, so we can borrow `s` mutably.
mutate_string(&mut s);
assert_eq!(s, "Ferris - now with extra crab");
}
You borrowed a String read-only as &str, mutated one through &mut String,
and practiced repairing ownership and borrowing errors.
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.- For the same value, active borrows allow either shared
&Taccess or exclusive&mut Taccess, never both. This restriction also helps prevent data races across threads.- For these local references, a borrow ends at the reference's last use, not necessarily at the closing brace. Using
r1before creatingr2lets their exclusive borrows take turns. Printingsharedbefore mutatingsends the shared borrow before the mutable one starts. Using either earlier reference again after the mutation can bring the conflict back.- 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.
Extra practice to explore at your own pace. These chapters do not count toward course progress.