Lifetimes
A lifetime describes the scope during which a reference is valid.
- Every reference in Rust has a lifetime
- Most of the time, the compiler infers it
- Sometimes, you must explicitly tell the compiler how lifetimes relate
Lifetimes do not change how long values live They only describe relationships between references
Why Lifetimes Exist
Without lifetimes, this would be allowed:
let r: &i32;
{
let x = 5;
r = &x;
}
println!("{}", r); // ❌ use-after-free
Rust prevents this by tracking lifetimes at compile time.
Lifetime Basics (Implicit Lifetimes)
fn print(s: &String) {
println!("{}", s);
}
You didn’t write lifetimes, but Rust sees:
fn print<'a>(s: &'a String)
'a= lifetime of the reference- The reference must be valid for the duration of the function call
When Lifetimes Become Necessary
The Problem Case
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
❌ Compiler error: missing lifetime specifier
Why?
- Rust doesn’t know whether the return value refers to
xory - It needs to know how long the returned reference is valid
Explicit Lifetime Annotations (Basic)
Correct Version
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
What this means
'ais a generic lifetime parameter- The returned reference is valid as long as both
xandyare valid - The actual lifetime will be the shorter of the two at runtime
Usage
let s1 = String::from("short");
let s2 = String::from("much longer");
let result = longest(&s1, &s2);
println!("{}", result);
Safe, guaranteed.
Important Rule (This Is Key)
A function cannot return a reference to a value it owns
fn bad() -> &String {
let s = String::from("hello");
&s // ❌ illegal
}
Why?
sis dropped at the end of the function- The reference would dangle
Lifetimes don’t “extend” data—they only describe reality.
Lifetime Elision Rules (Compiler Magic)
Rust has rules that let you skip lifetime annotations in common cases.
Rule 1: One input reference → output uses same lifetime
fn first(s: &str) -> &str {
s
}
Expanded form:
fn first<'a>(s: &'a str) -> &'a str
Rule 2: &self in methods gets its own lifetime
impl String {
fn len_ref(&self) -> &usize {
// hypothetical example
}
}
Rust knows the output is tied to self.
Lifetimes with Structs (Intermediate)
Struct Holding References
struct ImportantExcerpt<'a> {
part: &'a str,
}
ImportantExcerpt cannot outlive the string it references
Usage
let novel = String::from("Call me Ishmael...");
let excerpt = ImportantExcerpt {
part: &novel[0..4],
};
println!("{}", excerpt.part);
If novel is dropped first → compile error.
Lifetimes in impl Blocks
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn part(&self) -> &str {
self.part
}
}
&selfuses the same lifetime'a- Returned reference is guaranteed valid
Multiple Lifetimes (Intermediate)
fn mix<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
This is valid because:
- Return value only depends on
x y’s lifetime is irrelevant to the output
But this would NOT compile:
fn bad<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
y // ❌ lifetime mismatch
}
Lifetimes + Mutable References
fn replace<'a>(s: &'a mut String) -> &'a mut String {
s.push_str("!");
s
}
- Only one mutable reference exists
- Lifetime ensures no other borrows overlap
Lifetime Bounds (Advanced)
'a: 'b (Outlives Relationship)
fn example<'a, 'b>(x: &'a str, y: &'b str)
where
'a: 'b,
{
// 'a lives at least as long as 'b
}
Used when one reference must outlive another.
Lifetimes with Traits (Advanced)
use std::fmt::Display;
fn announce<'a, T>(x: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
x
}
Combines:
- Lifetime parameters
- Generic type parameters
- Trait bounds
'static Lifetime (Special Case)
'static = valid for the entire program
let s: &'static str = "I live forever";
Why?
- String literals are embedded in the binary
Common Uses
fn takes_static(s: &'static str) {
println!("{}", s);
}
'static does NOT mean “no lifetime issues”
It means “this reference never becomes invalid”
Advanced: Lifetime vs Ownership (Key Insight)
This does NOT work:
fn make_ref<'a>() -> &'a String {
let s = String::from("hello");
&s
}
Why?
- Lifetimes don’t create ownership
- You can’t return references to dropped values
Fix it by returning ownership:
fn make_string() -> String {
String::from("hello")
}
Mental Model That Actually Works
- Ownership: who owns the data?
- Borrowing: who can access it?
- Lifetimes: how long is that access valid?
Think of lifetimes as labels, not timers.
Why Lifetimes Matter
Lifetimes guarantee:
- No dangling references
- No use-after-free
- Thread-safe borrowing
- Zero runtime cost
And all of this is enforced at compile time.