Did you know that they make a movie about Rust?
It's called "The Story of 'a lifetime".
You've already used ownership in several forms. You moved Strings, borrowed
slices, passed &mut references into functions, and handed values to Vec,
HashMap, Option, Result, and your own structs. Now we can connect those
operations and see how Rust manages memory safely without a garbage collector.
Languages with manual memory management (C, C++) hand you the power to free memory yourself, and with it the power to free it twice, free it too early, or forget to free it at all. Languages with a garbage collector take that power back and spend runtime and memory tracking what's still alive. Rust takes a third path: before the program runs, the compiler checks where owned values are dropped and whether references outlive what they point at.
Three rules summarize the model you've already been using:
&,
or one &mut, never both).Rules 1 and 2 give each value one place where cleanup normally happens, which rules out double-frees in safe Rust. The aliasing part of rule 3 rules out data races because you can't write through one reference while reading through another. Lifetime checks supply the other half: a reference can't remain usable after its owner has been dropped.
For an owned String, dropping the value frees its buffer automatically. If a
reference would still be used after that drop, safe Rust rejects the code at
compile time.
The borrow checker also tracks how long a reference stays valid. That's what a lifetime describes.
A reference borrows a value, so it must not outlive that value. If it could, you'd have a reference pointing at memory that's already been cleaned up, which is a use-after-free bug.
Picture a function that tries to return a reference to its own local string:
fn dangling() -> &String {
let s = String::from("temporary");
&s // ERROR: `s` is dropped at the end of this function
}
s is owned by dangling, so it's dropped the moment the function returns. A
reference to it would dangle, so the compiler rejects the code. When reading
this error, I'd start by asking who owns s and when it gets dropped. A borrow
has to stay valid for as long as you use it, and the compiler checks that span
(the reference's lifetime) against the owner's.
Lifetimes can have a name.
By convention, you name them 'a, 'b, and so on, though more descriptive names are allowed too.
fn dangling<'a>() -> &'a String {
let s = String::from("temporary");
&s // Still an error: naming a lifetime doesn't keep `s` alive
}
In this example, <'a> declares a lifetime parameter, and &'a String uses it
for the returned reference.
This is the same attempt to return a reference to a local value, and it fails
for the same ownership reason. Strictly speaking, the first signature also
has a missing lifetime: there is no input reference whose lifetime the compiler
can use for the return value. Writing 'a addresses that missing annotation,
but it doesn't extend the life of s. To return this string, return an owned
String instead of a &String reference.
Most of the time the compiler works lifetimes out on its own and you never write one. You'll usually need annotations when a struct holds a reference or a function returns one of several borrowed inputs. The syntax can take some getting used to, but keep the same question in mind: which owner does this reference depend on, and will that owner still be alive?
When the compiler reports a lifetime error, trace the borrowed value, its owner, and the last use of the reference.
In the Option<T> chapter,
find_user_by_id returned a borrowed username. Now the caller needs to keep that
name after the user records are dropped.
Run the supplied code first: three tests pass, and
test_username_survives_records stops at todo!().
test_username_survives_records. Before
uncommenting it, predict whether it will compile. Which value owns the text,
and when is that value dropped?todo!() and the /* and */, then run the tests. Read the
compiler error before editing the function.find_user_by_id so the caller can use
the username after the records are dropped. Keep the borrowed input parameter
and all tests unchanged. The records must still be dropped at the end of
their block.The lookup must still return the first matching username, or None for a
missing ID. The caller must still be able to use its records after the call. You
don't need lifetime annotations for this task.
All four tests should pass when you're done. To set the exercise aside, restore
the last test's comment markers and todo!() rather than removing the test.
When is the borrowed return from the Option<T> chapter a better choice? What
extra work does your implementation do to keep a separate username?
/// Finds the first user with this ID, or returns `None`.
fn find_user_by_id(users: &[(u32, String)], id: u32) -> Option<String> {
users
.iter()
.find(|(uid, _)| *uid == id)
.map(|(_, name)| name.clone())
}
// Cloning the matching username leaves the caller's records unchanged. The
// returned String has its own buffer, so dropping the records won't free it.
// Returning &str avoids the copy if the records stay alive while we use the
// name.
#[test]
fn test_find_user_by_id() {
let mut users = vec![(1, String::from("alice")), (2, String::from("bob"))];
assert_eq!(find_user_by_id(&users, 2).as_deref(), Some("bob"));
assert_eq!(find_user_by_id(&users, 99).as_deref(), None);
users.push((3, String::from("charlie")));
assert_eq!(find_user_by_id(&users, 3).as_deref(), Some("charlie"));
}
#[test]
fn test_empty_records() {
assert_eq!(find_user_by_id(&[], 1).as_deref(), None);
}
#[test]
fn test_first_match() {
let users = [
(7, String::from("Férris 🦀")),
(7, String::from("impostor")),
];
assert_eq!(find_user_by_id(&users, 7).as_deref(), Some("Férris 🦀"));
}
#[test]
fn test_username_survives_records() {
let username = {
let users = [(42, String::from("Férris 🦀"))];
find_user_by_id(&users, 42)
};
assert_eq!(username.as_deref(), Some("Férris 🦀"));
}
Ownership is the model underneath the moves, borrows, and references you've been
using throughout the course. In the username exercise, returning an owned
String let the caller keep the name after the records were dropped.
Copy types duplicate instead. You met this with
String (moves) and i32 (copies) back in the
moves chapter.&T, &mut T) lets you use a value without owning it, under the
aliasing rule: many shared borrows or one mutable borrow, never both. That
rule is what turns data races into compile errors.C++ uses RAII and destructors for deterministic cleanup, while garbage-collected languages track which values are still alive at runtime. Rust adds compile-time ownership and borrowing rules, so safe Rust turns use-after-free, double-free, and data races into compile errors.
Extra practice to explore at your own pace. These chapters do not count toward course progress.