Most Rust code compiles once you understand ownership and borrowing, but functions that return references often hit "please specify lifetimes" errors. Here we cover why lifetime annotations exist, the minimal syntax needed to solve common issues, and how the compiler tracks borrowed references.
This is one of the most abstract chapters in the Rust series. So instead of trying to memorize every rule at once, focus on one idea first: a lifetime is a label that describes the relationship between references.
What Is a Lifetime?
- Definition: A named label that helps the compiler reason about how long a reference stays valid.
- Purpose: Prevent dangling references that point to freed data.
- Notation: Short names like
'a,'bprefixed with an apostrophe.
The key insight: 'a does not measure wall-clock time. It simply ties references together and says "these all have to live at least as long as each other."
A useful metaphor is a badge label, not a stopwatch. A lifetime does not add more time; it marks which references are allowed into the same safe region.
Why Lifetime Annotations Show Up
At the simplest level, the problem looks like this:
- A function takes references as input.
- The function returns a reference.
- The compiler cannot tell which input reference the output is tied to.
That is when lifetime annotations become necessary.
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
- This function fails to compile because the compiler cannot prove whether the returned reference came from
aorb. - Without extra clues, it cannot guarantee the return value will live long enough.
- As code grows more complex—say, storing references in structs—the compiler needs explicit relationships.
So lifetime annotations are not about making references live longer. They are about telling the compiler which input reference the output depends on.
Adding Explicit Lifetime Parameters
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
<'a>declares that the function uses a lifetime parameter named'a.&'a strmeans "a string slice that lives for'a."- Because the return type also uses
'a, callers know the output cannot outlive the shorter of the two inputs.
At first, it helps to read the signature line by line:
a: &'a str-> inputais valid for'ab: &'a str-> inputbis also valid for'a-> &'a str-> the returned reference is valid only inside that same relationship
If that part makes sense, you already understand most beginner lifetime syntax.
Using Lifetimes in Structs
struct Highlight<'a> {
snippet: &'a str,
}
impl<'a> Highlight<'a> {
fn show(&self) {
println!("{}", self.snippet);
}
}
- Structs that store references must declare lifetime parameters.
Highlight<'a>promises that it only borrows data for'a, so an instance cannot outlive the data it points to.- If the struct owned a
String, no lifetime annotation would be necessary.
Common Lifetime Patterns in Signatures
| Pattern | Meaning | Example |
|---|---|---|
fn foo<'a>(x: &'a str) -> &'a str |
Output lives as long as input | Return whichever slice is longer |
fn bar<'a, 'b>(x: &'a str, y: &'b str) -> &'a str |
Inputs each keep their own lifetime | Return only one of the inputs |
impl<'a> Struct<'a> |
Struct fields hold references | Parsers, lexers, etc. |
Lifetime Elision Rules
Rust applies three default rules so you rarely have to annotate lifetimes manually:
- Each reference parameter gets its own lifetime parameter.
- If there's only one input reference, its lifetime is assigned to the output.
- For methods with
&selfor&mut self, the receiver's lifetime flows to the output.
When a function grows more complicated than those rules, add explicit annotations. If the compiler says it's ambiguous, write the lifetimes down.
At the beginner level, this rule of thumb works well:
- One input reference: elision often works.
- Two or more input references: the return relationship can become ambiguous.
- References stored in structs: explicit lifetimes are common.
The 'static Lifetime
- Represents data that lives for the entire program run.
- String literals like
"hello"are'static. - Accepting
&'static str(fn foo(x: &'static str)) severely limits callers, so slapping'staticon everything rarely solves real problems.
Reading Borrow Checker Errors
fn main() {
let result;
{
let s = String::from("rust");
result = s.as_str();
}
println!("{}", result);
}
- This fails because
sdrops beforeresultis used. - Lifetime annotations cannot extend
s's lifetime; they only describe relationships. If the data dies early, no annotation can resurrect it.
That is the most important boundary to remember: lifetimes explain safety, but they do not magically keep data alive.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.
💬 댓글
이 글에 대한 의견을 남겨주세요