Lifetimes
Prerequisites: Common standard traits
Every reference in Rust has a lifetime: the region of code during which it is valid. The borrow checker uses lifetimes to guarantee that no reference outlives the data it points to, so dangling pointers are impossible.
Most of the time lifetimes are inferred and you never write them. You need to annotate them when the compiler can't work out how the lifetimes of a function's inputs relate to its output, or when a struct stores a reference.
Lifetime annotations don't change how long anything lives. They describe relationships ("the returned reference is valid as long as both arguments are") so the compiler can check every caller against them.
Syntax
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 'a is a lifetime parameter
if x.len() >= y.len() { x } else { y }
}
struct Excerpt<'a> { // a struct holding a reference
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn part(&self) -> &str { // elided: tied to &self
self.part
}
}
Lifetime names start with an apostrophe and are usually short: 'a, 'b.
<'a> declares the parameter, just like <T> declares a type parameter.
Why longest needs an annotation
longest returns either x or y, and the compiler can't know which one at
compile time. Without annotations it has no idea how long the result is
allowed to be used. 'a says: the result is valid for the shorter of the
two input lifetimes. Callers are then checked against that promise.
Example
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
// The result depends only on `text`, so only `text` shares the lifetime.
fn first_sentence<'a>(text: &'a str, _separator_hint: &str) -> &'a str {
text.split('.').next().unwrap_or(text)
}
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(part: &'a str) -> Self {
Excerpt { part }
}
fn word_count(&self) -> usize {
self.part.split_whitespace().count()
}
}
fn main() {
let a = String::from("a fairly long string");
let result;
{
let b = String::from("short");
result = longest(&a, &b);
println!("longest: {result}"); // fine: both `a` and `b` are still alive here
}
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt::new(first_sentence(&novel, "."));
println!("excerpt: '{}' ({} words)", excerpt.part, excerpt.word_count());
let greeting: &'static str = "string literals live for the whole program";
println!("{greeting}");
}
Expected output
longest: a fairly long string
excerpt: 'Call me Ishmael' (3 words)
string literals live for the whole program
Excerpt<'a> means "an Excerpt cannot outlive the string it borrows from".
If novel were dropped while excerpt was still in use, the program would
not compile.
Lifetime elision
To save typing, the compiler applies three elision rules to function signatures. If they fully determine the output lifetime, you don't write any:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is used for every output
reference:
fn first_word(s: &str) -> &strneeds no annotation. - If one of the parameters is
&selfor&mut self, its lifetime is used for the outputs. That is why methods rarely need annotations.
longest has two reference inputs and no self, so none of the rules decide
the output — hence the annotation.
'static is a special lifetime meaning "valid for the entire program". All
string literals are &'static str. Don't reach for 'static to silence a
lifetime error; it usually means an owned value (String) is what you need.
Common mistakes
-
Returning a reference from a function with two reference parameters and no annotation:
error[E0106]: missing lifetime specifier. The compiler suggests adding'a. -
Using the result after one of the inputs is gone:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() >= y.len() { x } else { y } } fn main() { let a = String::from("long string is long"); let result; { let b = String::from("xyz"); result = longest(a.as_str(), b.as_str()); } println!("{result}"); }This fails with
error[E0597]: `b` does not live long enough:resultmight refer tob, which is dropped at the closing brace. -
Returning a reference to a local variable. No annotation can fix that; the value is dropped when the function returns. Return the owned value (
String,Vec<T>) instead. -
Putting references in structs by default. Structs that own their data (
Stringrather than&str) are much easier to use; add lifetimes to a struct only when borrowing is a deliberate choice.
Practice
- Write
fn longer_slice<'a>(a: &'a [i32], b: &'a [i32]) -> &'a [i32]that returns the slice with more elements (aon a tie). - Write a struct
Highlight<'a>holdingtext: &'a str, with a methodfirst_word(&self) -> &strthat returns the text up to the first space.
Print the longer of [1, 2, 3] and [4, 5], and the first word of a
Highlight built from "borrow checker rules".
Show solution
fn longer_slice<'a>(a: &'a [i32], b: &'a [i32]) -> &'a [i32] {
if a.len() >= b.len() { a } else { b }
}
struct Highlight<'a> {
text: &'a str,
}
impl<'a> Highlight<'a> {
// Elision rule 3: the returned &str is tied to &self.
fn first_word(&self) -> &str {
self.text.split(' ').next().unwrap_or(self.text)
}
}
fn main() {
let x = [1, 2, 3];
let y = [4, 5];
println!("{:?}", longer_slice(&x, &y));
let sentence = String::from("borrow checker rules");
let h = Highlight { text: &sentence };
println!("{}", h.first_word());
}
[1, 2, 3]
borrow