09. Lifetimes — explicit 'a and elision rules
A lifetime is a compile-time marker for how long a reference is valid. You write them as `'a`, but most of the time the compiler infers them through elision rules. This lesson covers why lifetimes exist, when to write them by hand, and the meaning of `'static`.
What you'll learn
- 1See why lifetimes prevent dangling references
- 2Annotate functions and structs with `'a`
- 3Recite the 3 lifetime elision rules
- 4Distinguish `'static` from other lifetimes
- 5Read and resolve lifetime-related compile errors
Overview
A reference whose target dies becomes a dangling reference. Rust prevents this at compile time by tracking lifetimes for every reference. Most code never spells them out — but functions that take references and return references sometimes need annotations.
Core Concepts
1) The problem lifetimes solve
fn main() {
let r;
{
let x = 5;
r = &x; // x is about to die...
} // x dropped
println!("{}", r); // error: `x` does not live long enough
}2) Explicit annotation 'a
When a function takes references and returns one, the compiler needs to relate them. `'a` is any name (commonly a, b, c) marking a shared lifetime.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}3) Elision — 3 rules
- Each reference parameter gets its own lifetime
- If there's one parameter, its lifetime is assigned to all output lifetimes
- If there's `&self` or `&mut self`, its lifetime is assigned to all output lifetimes
When inference succeeds via these rules, no annotation is needed — which is why everyday code rarely shows `'a`.
4) 'static — program-long
`&'static str` is valid for the program's entire run. String literals are the classic example.
Hands-on Examples
Two-input case that needs annotation:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(s1.as_str(), s2.as_str());
println!("longest = {}", result);
} // s2 dropped — result valid only inside the inner scope
}A struct holding a reference must declare a lifetime:
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let e = Excerpt { part: first_sentence };
println!("{}", e.part);
}Elision in action — no annotation needed:
// elidable — one input ref, rule 2 applies
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' { return &s[0..i]; }
}
s
}
// fully written: fn first_word<'a>(s: &'a str) -> &'a str { ... }Common Mistakes
Q. How far should I spell lifetimes out?
A. Only when the compiler asks. Errors point out where elision can't decide — then add annotations there. No need to pre-emptively scatter `'a` everywhere.
Q. Is `'static` a magic catch-all?
A. No. `'static` means "lives forever" — that's a **strong constraint**. Short-lived references can't satisfy it. Sprinkling `'static` on signatures reduces caller flexibility a lot.
Q. What if two references have different lifetimes?
A. Declare both with `<'a, 'b>` and optionally an outlives bound `'b: 'a`. Usually you can simplify by tying both to the shorter of the two — when lifetime algebra gets ugly, your design probably needs a rethink.
Recap
- Lifetimes mark how long references are valid for the compiler
- Elision auto-fills most signatures
- Annotations are needed when functions relate refs in to refs out
- `'static` = whole program, not a free pass
Try It Yourself
- Try `longest` with two different lifetimes `'a, 'b` and read the error
- Define `struct Book<'a> { title: &'a str, author: &'a str }` and use it
- Write code that creates a dangling reference on purpose, then copy the compiler's exact message
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗