Why are Rust developers so frugal? They prefer to borrow.
Ownership is the rule that makes Rust feel different from other languages. The short version:
That's it. The compiler enforces these rules, which is why Rust catches use-after-free, double-free, and data races at compile time, with no garbage collector at runtime.
Assigning or passing a value transfers ownership ("moves" it). After a move, the original binding is no longer usable:
let s = String::from("hello");
let t = s; // ownership moves from s to t
// println!("{s}"); // ERROR: borrow of moved value
println!("{t}"); // fine
Types that are cheap to copy (integers, bools, chars, fixed-size arrays
of those) implement Copy and don't move. They're duplicated bit-for-bit:
let a = 5;
let b = a; // a is copied, not moved
println!("{a} {b}");
Most of the time you don't want to transfer ownership; you just want to
read or modify the value briefly. That's a borrow, written &value (or
&mut value for a mutable borrow).
fn length(s: &String) -> usize { s.len() } // borrow, doesn't take it
let s = String::from("rust");
let n = length(&s); // borrow s
println!("{s}"); // s still owns the data
The borrow checker enforces one rule that takes a moment to internalize:
At any time, you can have either any number of immutable references or exactly one mutable reference. Never both.
This is what prevents data races at compile time. If the compiler ever yells at you about borrowing, that rule is the first thing to look at.
When a function parameter has an owned type like String (no & in
front), calling the function moves the argument in. The caller's
binding is no longer usable afterwards. The value lives at the
callee now, and will be dropped when the callee finishes (unless it
hands ownership back via the return value, which is exactly what
happens here).
Note the signature: String in, String out. Implement the body by
mutating the parameter (s.push_str(...)) and then returning s.
Because you own s, you're free to mutate it without any &mut
dance: ownership implies the right to modify.
Useful from the standard library
String::push_strappends a&strto an ownedString. No allocation if there's spare capacity.- The parameter needs
mut s: Stringto callpush_stron it. Mutability is a property of the binding, not the type, so even an owned value has to be declaredmutbefore you can mutate it. Themutis local to the function and doesn't appear in the type.
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.
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.
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 (chapter 16, chapter 19, ...) 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.
You moved a String into a function and back out, borrowed one
read-only as &str, mutated one through &mut String, and made
the borrow checker complain on purpose to see its three canonical
errors.
What we learned
- Every value has exactly one owner. When the owner goes out of scope, the value is dropped. No garbage collector required.
- Assigning or passing a non-
Copyvalue transfers ownership. The old binding is no longer usable.Copytypes (integers, bools, chars, fixed-size arrays of those) are duplicated bit-for-bit instead.- Borrows let you read or modify a value without taking ownership.
&Tis a shared (read-only) borrow;&mut Tis exclusive.- The borrow-checker rule: at any time, either any number of
&Tborrows or exactly one&mut Tborrow, 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 s: Stringor&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.clone()is the explicit escape hatch when you really do need two owners. Reach for it after you understand why a borrow won't compile, not before.