Rust: Rust Lifetimes: Annotating the Validity of References
Last updated: 2026-08-26
Lifetime is the mechanism Rust uses to ensure that "references never hang"—it assigns an "expiration date" to each reference.
You're Actually Already Using Lifetimes—All the code in Lessons 9 and 10 has lifetimes; the compiler just "guessed" them for you. This lesson teaches you when you need to manually specify lifetimes.
1. What You'll Learn
- Why Is a Lifecycle Needed? — To Prevent dangling references
- Lifecycle annotation syntax:
'a,'b - Lifetimes in Function Signatures—Lifetimes of Parameters and Return Values
- Lifetime Omission Rules—When Can the Compiler Automatically Inference?
- Lifecycle annotations in structures
'staticLifecycle—Permanent References
2. Conceptual Diagrams
flowchart LR
subgraph "Function Signature fn longest<'a>"
X["x: &'a str"] --> RET["Return Value: &'a str"]
Y["y: &'a str"] --> RET
end
NOTE["'a = the shorter of x and y's lifetimes"] -.-> RET
3. A Story About Chasing a Mouse
(1) Frustration: The mouse ran away, but I’m still watching
Tom is a biologist who is using a tracker to record the location of mice:
The tracker locks onto the mouse A → Mouse A ran into the cave → The tracker is still pointing toward the entrance.
- The tracker (reference) points to Mouse A (data)
- Mouse A ran away (data was destroyed)
- The tracer is still pointing to that location (dangling reference—dangerous!)
"It would be great if the tracker had a label that said, 'Valid until Mouse A comes out of its hole'..."
(2) Solutions for the Rust Lifecycle
// Life Cycle Annotation 'a: Tell the compiler "the validity period of the return value does not exceed that of parameter x's validity period"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("Long Strings");
let result;
{
let string2 = String::from("short");
result = longest(&string1, &string2); // ✅ string2 and string1 are both still alive
println!("The longer one is: {}", result);
} // string2 Destroyed here
// println!("{}", result); // ❌ If you print here, result references string2 which is destroyed
} // string1 Destroyed here
Lifetimes are like "expiration date" labels attached to trackers. The compiler uses these labels to determine whether a reference is still within its lifetime. In this example, the lifetime of
resultcannot exceed that ofstring2.
4. How the Lifecycle Works
(1) Every reference has a lifecycle
fn main() {
let x: i32 = 10; // x Its life cycle begins here
let r: &i32 = &x; // r Its life cycle begins here
println!("r: {}", r); // Usage r
} // r whose lifecycle ends first, then x ends
(2) Why is it necessary to annotate lifecycles?
When a function returns a reference, the compiler needs to know which parameter the reference comes from:
graph TB
A[Functions Return References] --> B{Which parameter does the return value point to?}
B --> C[Pointer Parameters1 → The Lifecycle of Return Values ≤ Parameters1]
B --> D[Pointer Parameters2 → The Lifecycle of Return Values ≤ Parameters2]
B --> E[Point to one of the two → Must be labeled: Find the intersection]
C --> F[No annotation required (the compiler can infer)]
D --> F
E --> G[Manual annotation is required 'a]
| Scenario | Example | Does it require annotation? |
|---|---|---|
| Single Input Reference | fn first(x: &str) -> &str |
Not required (omission rule) |
| Multiple input references | fn longest(x: &str, y: &str) -> &str |
Required (the compiler doesn't know which one to choose) |
| Does not return a reference | fn len(x: &str) -> usize |
Not required |
| Structures containing references | struct S<'a> { r: &'a i32 } |
Required |
(3) Quick Reference for Life Cycle Annotation Locations
| Location | Syntax | Description |
|---|---|---|
| Function Definition | fn foo<'a>(x: &'a str) -> &'a str |
Parameters and Return Values |
| Structure Definition | struct S<'a> { r: &'a str } |
A structure cannot outlive its reference |
| impl block | impl<'a> S<'a> |
Declaring the Lifecycle When Implementing a Method |
| trait constraint | where T: 'a |
Constraint types cannot contain short-lived references |
| Static annotation | &'static str |
Valid throughout the entire program's execution |
5. Lifecycle Annotation
(1) Syntax
// 'a is the name of the lifecycle parameter (using 'a, 'b, 'c)
// Pronunciation: Regarding the lifecycle 'a, both x and y need to be alive for at least 'a
fn function<'a>(x: &'a str, y: &'a str) -> &'a str {
// The lifecycle of return values = the shorter of x and y
}
(2) Lifecycle Omission Rules
The compiler can automatically infer lifetimes (without you having to write them) in the following three cases:
| Rule | Meaning |
|---|---|
| Each input reference has its own lifecycle | fn foo(x: &str) automatically assigns a lifecycle to x |
| Only one input reference | The lifetime of the output reference = the lifetime of the input reference |
| Multiple inputs, one of which is &self or &mut self | The lifetime of the output reference is &self |
// No annotation required: there is only one input reference
fn first_word(s: &str) -> &str { &s[..] }
// Needs annotation: two input references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
6. Lifecycle Examples
▶ Example 1: Why Do We Need a Lifecycle? (Difficulty ⭐⭐⭐)
Output:
The longer one is: <result>
<result>
// ============================================
// What happens if there are no lifecycle annotations?
// ============================================
// This function has two input references, the return value could be any one of these
// ❌ If not annotated, a compilation error will occur: expected lifetime parameter
// fn longest_wrong(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// ✅ Correct Version: Lifecycle 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("hello");
let result;
{
let s2 = String::from("hi");
result = longest(&s1, &s2); // result lifecycle = s2 lifecycle (the shorter one)
println!("The longer one is: {}", result);
} // s2 destroyed, result invalid
// println!("{}", result); // ❌ The compiler will prevent: result's lifecycle has ended
}
Output:
result: <result>
Remains valid even after leaving the inner layer: <result>
The lifetime
'ais set to the shorter ofxandy. If the lifetime of the return value were set to the longer one, it might still be in use after the shorter one has been destroyed—which is exactly what the compiler is trying to prevent.
▶ Example 2: Parameters for Different Life Cycles (Difficulty ⭐⭐⭐)
Output:
result: <result>
Remains valid even after leaving the inner layer: <result>
// ============================================
// The two parameters have different lifecycles.
// ============================================
// Two Lifecycles: 'a corresponds to x, 'b corresponds to y
// The return value depends only on x, so use 'a
fn choose_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x // Return only x, so the lifecycle only needs 'a
}
fn main() {
let x = String::from("Live to be a hundred years old");
let result;
{
let y = String::from("Short-lived ghost");
result = choose_first(&x, &y); // ✅ result Depends solely on x
println!("result: {}", result);
} // y destroyed, but result does not depend on y, so no problem
println!("Remains valid even after leaving the inner layer: {}", result); // ✅ x Still Alive
}
Output:
result: Live to be a hundred years old
Remains valid even after leaving the inner layer: Live to be a hundred years old
Output:
Announcement: <announcement>
When the return value depends solely on a particular parameter, you only need to annotate that parameter’s lifetime. Even if the
choose_firstparameter inyis short-lived, it does not affect the result—because the return value does not use any data fromyat all.
▶ Example 3: Lifecycle in Structures (Difficulty ⭐⭐⭐⭐)
Output:
Announcement: <announcement>
Excerpt: <excerpt.content>
Length: <excerpt.length()>
Announcement of Results: <excerpt.announce("Welcome to this article")>
// ============================================
// Storing references in a structure -- the lifecycle must be specified.
// ============================================
struct Excerpt<'a> {
content: &'a str, // Structures Borrow External Strings
}
impl<'a> Excerpt<'a> {
fn length(&self) -> usize {
self.content.len()
}
fn announce(&self, announcement: &str) -> &str {
println!("Announcement: {}", announcement);
self.content // Returning a Reference to the Interior of a Structure
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Can't find the period");
let excerpt = Excerpt {
content: &first_sentence,
};
println!("Excerpt: {}", excerpt.content);
println!("Length: {}", excerpt.length());
println!("Announcement of Results: {}", excerpt.announce("Welcome to this article"));
} // excerpt must be destroyed before novel (content depends on novel)
Output:
Excerpt: Call me Ishmael
Length: 15
Announcement of Results: Call me Ishmael
When a structure contains references, you must specify a lifetime parameter
<'a>on the structure name. This tells the compiler that the lifetime of a structure instance cannot exceed the lifetime of the data it references.
▶ Example 4: 'static' Lifecycle (Difficulty ⭐⭐⭐)
Output:
I exist throughout the entire lifecycle of a program
<std::ptr::from_ref(val)>
// ============================================
// 'static: the reference remains valid throughout the program's execution.
// ============================================
fn main() {
// A string literal is 'static
let s: &'static str = "I exist throughout the entire lifecycle of a program";
println!("{}", s);
// 'static Constraints: T must not contain any non-static references
fn print_static<T: 'static>(val: &T) {
println!("{:?}", std::ptr::from_ref(val));
}
let num: i32 = 42;
print_static(&num); // ✅ i32 excludes references, complies with 'static
// let msg = String::from("temp");
// let r = &msg;
// print_static(&r); // ❌ r references non-'static msg
}
Output:
I exist throughout the entire lifecycle of a program
0x...(Memory Address)
Output:
=== Text Analysis ===
'staticis the longest lifetime in Rust—data exists from the moment the program starts until it ends. String literals have a'staticlifetime. Please note:'staticshould be interpreted as "this reference is valid forever," rather than "this data survives until the end."
▶ Example 5: Comprehensive Exercise—Text Analyzer (Difficulty ⭐⭐⭐)
Output:
=== Text Analysis ===
<analyzer.summary()>
First 3 words: <top3>
Includes 'Rust' the word: <rust_mentions>
// ============================================
// Life Cycle in Practice: Zero-Copy Text Analysis
// ============================================
struct TextAnalyzer<'a> {
text: &'a str,
}
impl<'a> TextAnalyzer<'a> {
fn new(text: &'a str) -> Self {
TextAnalyzer { text }
}
fn word_count(&self) -> usize {
self.text.split_whitespace().count()
}
fn longest_word(&self) -> &'a str {
self.text
.split_whitespace()
.max_by_key(|w| w.len())
.unwrap_or("")
}
fn first_n_words(&self, n: usize) -> Vec<&'a str> {
self.text.split_whitespace().take(n).collect()
}
fn line_count(&self) -> usize {
self.text.lines().count()
}
fn char_count(&self) -> usize {
self.text.chars().count()
}
fn summary(&self) -> String {
format!(
"Character: {}, Words: {}, Number of lines: {}, Longest Word: '{}'",
self.char_count(),
self.word_count(),
self.line_count(),
self.longest_word()
)
}
}
fn highlight_word<'a>(text: &'a str, word: &str) -> Vec<&'a str> {
text.split_whitespace()
.filter(|w| w.contains(word))
.collect()
}
fn main() {
let article = "Rust is a systems programming language \
that runs blazingly fast and prevents segfaults. \
Rust guarantees memory safety and thread safety.";
let analyzer = TextAnalyzer::new(article);
println!("=== Text Analysis ===");
println!("{}", analyzer.summary());
let top3 = analyzer.first_n_words(3);
println!("\nFirst 3 words: {:?}", top3);
let rust_mentions = highlight_word(article, "Rust");
println!("Includes 'Rust' the word: {:?}", rust_mentions);
let paragraph = "First line.\nSecond line.\nThird line.";
let p_analyzer = TextAnalyzer::new(paragraph);
println!("\nParagraph Analysis: {}", p_analyzer.summary());
}
Output:
=== Text Analysis ===
Character: 124, Words: 18, Number of lines: 1, Longest Word: 'blazingly'
First 3 words: ["Rust", "is", "a"]
Includes 'Rust' the word: ["Rust", "Rust"]
Paragraph Analysis: Character: 39, Words: 6, Number of lines: 3, Longest Word: 'Second'
TextAnalyzerHolds a reference to the original text throughout its lifecycle<'a>—all analysis methods are zero-copy.longest_wordThe returned&'a strpoints to word slices in the original text and does not result in any memory allocation.
❓ FAQ
📖 Summary
- Lifetimes are a compile-time mechanism Rust uses to ensure that references remain valid.
- Use the
'asyntax to link the validity periods of the input and output references - Life Cycle Omission Rules eliminate the need for manual annotation in over 80% of scenarios
- When storing references in a struct, you must specify a lifetime parameter.
'statichas the longest lifecycle (the entire duration of the program's execution)- No runtime overhead throughout its lifecycle—it disappears once it's compiled.
📝 Exercises
- Difficulty ⭐: Read a piece of existing Rust code (such as the example from Lesson 9) and identify which functions avoid manual annotation thanks to the lifetime elision rule.
- Difficulty ⭐⭐⭐: Write a function
fn shortest<'a>(x: &'a str, y: &'a str) -> &'a strthat returns the shorter of two strings. Inmain, create variables with different lifetimes to call it. - Difficulty ⭐⭐⭐⭐: Define a struct
Book<'a>that contains a fieldtitleof type&'a str. Write aimplblock that implements a methodfn first_word(&self) -> &strreturning the first word of the title. Demonstrate its use inmain.