Rust, part 6: lifetimes without the fear
Part 6from the Rust series · 15 parts in all
Lifetimes have a reputation far beyond their difficulty, for a reason that is worth naming: most of the time they are invisible. You write them when the compiler cannot infer the relationship between references, which is exactly when a human reader needs them too. Part 6 is the small set of rules that covers nearly every case.
A lifetime is a claim about how long a reference stays valid
// The signature says: the result borrows from BOTH inputs, so it may not
// outlive either of them.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
No runtime thing happens here. 'a is a name for a region of the source, and the
annotations are the compiler's way of checking that nothing you return outlives the data it
points into. That is why the error message is about the caller the compiler could not
prove safe.
Three elision rules cover most annotations
You usually write nothing, because rustc applies these in order:
- Each elided input reference gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output references.
- If one of the inputs is
&selfor&mut self, its lifetime is assigned to all output references.
Rule 3 is why methods rarely need annotations and free functions with two references do. Structures that borrow are the other place it becomes explicit — and there the annotation is documentation:
// This parser does not own its input; it must not outlive it.
struct Parser<'i> {
input: &'i str,
pos: usize,
}
impl<'i> Parser<'i> {
fn rest(&self) -> &'i str { &self.input[self.pos..] }
}
&self borrows the parser for the call; &'i str returns
data that lives as long as the input did. Those are different lengths, and the difference is
the whole reason both appear in one line.
The static escape hatch, and its cost
When a value has no natural owner to borrow from, the alternatives are owning it
(String, Vec) or leaking it (Box::leak,
OnceLock) to get a 'static. Leaking is sometimes right for
configuration read once at start-up. As a habit it is wrong, because the point of lifetimes is
that the compiler is tracking something real — reach for 'static and you have
told it to stop.
The practical advice, after the theory: when a lifetime error appears, look at the signature first. The body is usually fine, and the compiler is objecting to a function that claims a relationship between its inputs and output that it cannot prove — which is a design question, not a syntax one. Next: the async model, which is where all of the above gets interesting again.