Rust lifetimes explained
What lifetimes are in Rust, why the borrow checker needs them, how to read and write 'a annotations, the elision rules, and structs that hold references.
A lifetime is the stretch of code during which a reference is valid. Every reference has one. Most of the time the compiler works lifetimes out by itself and you never write them; you only add lifetime annotations when a function or struct connects several references and the compiler can't tell how they relate.
The problem lifetimes solve
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{r}");
}
error[E0597]: `x` does not live long enough
x is dropped at the inner }, but r is used after that. The borrow checker compares the two regions and rejects the program. In C this would compile and read freed memory.
Annotations describe relationships
Within one function the compiler can see everything. Across a function call it only sees the signature. Consider:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
'a (read "lifetime a") is a lifetime parameter, declared in <> like a generic type. The signature says: "the returned reference is valid for as long as both a and b are". Callers are then checked against that promise:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let outer = String::from("a long string");
let result;
{
let inner = String::from("short");
result = longest(&outer, &inner);
}
println!("{result}");
}
error[E0597]: `inner` does not live long enough
Annotations never make anything live longer. They are a contract that lets the compiler check each function on its own, and each call on its own.
Lifetime elision
You rarely write lifetimes because of three elision rules:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it's used for all outputs:
fn first_word(s: &str) -> &strneeds nothing. - In methods, if there's
&selfor&mut self, outputs getself's lifetime:fn name(&self) -> &strneeds nothing.
When the rules can't decide (two reference inputs, a reference output, no self), you get E0106 "missing lifetime specifier" and must annotate.
Structs that hold references
A struct with a reference field needs a lifetime parameter, meaning "a Parser can't outlive the text it reads":
struct Parser<'a> {
input: &'a str,
pos: usize,
}
This is common for short-lived views (parsers, iterators, tokenizers) and avoids copying. For long-lived data (config, records, anything stored in a collection or sent to a thread), make the struct own its data (String, Vec<T>) instead.
'static
'static is the lifetime of the whole program. String literals are &'static str. A bound T: 'static (required by thread::spawn) means "contains no short-lived references", and owned types like String satisfy it.
Practical advice
- If you're fighting lifetimes in a struct, try owning the data first. A
Stringfield costs an allocation and saves a lot of complexity. - Return owned values from functions that create new data. Only return references that point into the inputs.
- Read lifetime errors as timelines: rustc labels where the value is created, borrowed, dropped and later used.
'_is the "anonymous lifetime":impl Parser<'_>or-> Chars<'_>says "a lifetime is here, inferred as usual".
Example
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
// The returned word borrows from the input text, not from the parser.
fn next_word(&mut self) -> Option<&'a str> {
let rest = &self.input[self.pos..];
let rest_trimmed = rest.trim_start();
if rest_trimmed.is_empty() {
return None;
}
let start = self.pos + (rest.len() - rest_trimmed.len());
let end = rest_trimmed.find(' ').map(|i| start + i).unwrap_or(self.input.len());
self.pos = end;
Some(&self.input[start..end])
}
}
fn main() {
let outer = String::from("a long string");
{
let inner = String::from("short");
let result = longest(&outer, &inner);
println!("longest: {result}");
}
let text = String::from(" move north then attack ");
let mut words = Vec::new();
{
let mut p = Parser::new(&text);
while let Some(w) = p.next_word() {
words.push(w);
}
} // the parser is gone, but the words still borrow `text`
println!("{words:?}");
}
Output:
longest: a long string
["move", "north", "then", "attack"]