Lifetimes are Rust's way of ensuring references are valid. They're a crucial concept that enables memory safety without garbage collection, preventing dangling references and use-after-free bugs at compile-time.
What Are Lifetimes?
Lifetimes describe the scope for which a reference is valid. Every reference in Rust has a lifetime, which is determined by how long the referenced data remains valid.
The Problem Lifetimes Solve
Without lifetimes, this code would be dangerous:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{}", r); // x is already dropped!
}Rust's lifetime checker prevents this at compile-time.
Lifetime Annotations
Lifetimes are annotated with an apostrophe followed by a name.
Basic Syntax
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}The 'a lifetime parameter ensures the returned reference is valid as long as both inputs.
Lifetime Names
Conventional lifetime names are short:
// 'a - common default
// 'b - second lifetime
// 'input - descriptive name
fn func<'a, 'b>(x: &'a str, y: &'b str) -> &'a strLifetime Elision
Rust has lifetime elision rules that reduce annotation burden.
Rule 1: Input Lifetimes
Each parameter gets its own lifetime:
// Written as:
fn foo(x: &str, y: &str)
// Elided from:
fn foo<'a, 'b>(x: &'a str, y: &'b str)Rule 2: Single Input Lifetime
If there's one input lifetime, it's assigned to all outputs:
// Written as:
fn parse(s: &str) -> Result<&str, Error>
// Elided from:
fn parse<'a>(s: &'a str) -> Result<&'a str, Error>Rule 3: Methods
If there are multiple input lifetimes but one is &self or &mut self, that lifetime is assigned to all outputs:
impl MyStruct {
fn get(&self, s: &str) -> &str
// elided from:
fn get<'a, 'b>(&'a self, s: &'b str) -> &'a str
}Structs with Lifetimes
Structs can hold references with explicit lifetimes.
Basic Example
struct ImportantExcerpt<'a> {
part: &'a str,
}
let novel = String::from("Call me Ishmael...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};The struct can't outlive the reference it holds.
Multiple Lifetimes
Structs can have multiple lifetime parameters:
struct Context<'s, 't> {
code: &'s str,
explanation: &'t str,
}Lifetime Subtyping
Longer lifetimes can substitute for shorter ones.
Covariance
fn choose_first<'a: 'b, 'b>(first: &'a str, second: &'b str) -> &'b str {
first
}The 'a: 'b syntax means "lifetime a outlives lifetime b".
In Practice
fn mutate<'a, 'b>(x: &'a mut str, y: &'b str)
where
'a: 'b,
{
x.make_ascii_uppercase();
println!("{}", y);
}The 'static Lifetime
'static is a special lifetime that lasts for the entire program duration.
String Literals
let s: &'static str = "I have a static lifetime.";String literals are stored in the binary and available everywhere.
Static Variables
static HELLO_WORLD: &str = "Hello, world!";When to Use 'static
Use 'static for constants and global data:
const MAX_SIZE: usize = 100;
static ERROR_MESSAGES: &[&str] = &[
"File not found",
"Permission denied",
];Avoid Overusing 'static
Don't make everything 'static:
// Don't do this:
fn process(s: &'static str) { }
// Do this instead:
fn process(s: &str) { }Methods with Lifetimes
Methods can have lifetime parameters.
Basic Example
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}Multiple Lifetime Parameters
impl<'a> ImportantExcerpt<'a> {
fn compare<'b>(&self, other: &'b str) -> bool
where
'a: 'b,
{
self.part == other
}
}Lifetime Bounds
Lifetimes can have bounds like generics.
T: 'a Syntax
fn ref<'a, T>(x: &'a T) -> &'a T
where
T: 'a,
{
x
}This means "T contains references with lifetime 'a or longer."
In Structs
struct Ref<'a, T: 'a> {
value: &'a T,
}Generic Types with Lifetimes
Lifetimes work with generic types.
Example
struct Holder<'a, T> {
value: &'a T,
}
impl<'a, T> Holder<'a, T> {
fn new(value: &'a T) -> Self {
Holder { value }
}
}With Bounds
fn print_ref<'a, T>(t: &'a T)
where
T: Display + 'a,
{
println!("{}", t);
}Combining Lifetimes and Traits
Lifetimes interact with trait bounds.
Example
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}Advanced Lifetime Patterns
The Borrow Checker's Understanding
Rust understands non-lexical lifetimes (NLL):
let mut v = vec![1, 2, 3];
let r = &v[0];
v.push(4); // Error with old borrow checker
println!("{}", r); // But r is no longer used!With NLL, Rust sees that r isn't used after v.push.
Multiple Mutable References (with Splits)
let mut v = vec![1, 2, 3, 4, 5];
let (left, right) = v.split_at_mut(2);
left[0] = 10;
right[0] = 20;This works because the slices are guaranteed not to overlap.
Lifetime Variance
Lifetimes have variance relationships.
Covariant
fn covariance<'a, 'b>(x: &'a str) -> &'b str
where
'a: 'b,
{
x
}Longer lifetime can substitute for shorter.
Invariant
Mutable references are invariant:
fn invariant<'a>(x: &'a mut str) -> &'a mut str {
x
}Common Lifetime Errors
Dangling Reference
fn dangle() -> &String { // Error!
let s = String::from("hello");
&s
}Fix: Return owned String.
Lifetime Mismatch
fn returns_str() -> &str {
let s = String::from("hello");
&s[..] // Error!
}Fix: Use 'static or return owned value.
Missing Lifetime
fn parse(s: &str) -> Result<&str, Error>
// Error: missing lifetime specifierFix: Add lifetime annotation or rely on elision.
Debugging Lifetime Issues
The compiler provides excellent error messages.
Reading Error Messages
"borrowed value does not live long enough" "returned value references local data" "lifetime may not live long enough"
Common Solutions
- 1Clone the data
- 2Restructure code
- 3Add explicit lifetime annotations
- 4Use owned types instead of references
Practical Examples
Parsing
fn parse_command<'a>(input: &'a str) -> Command<'a> {
let mut parts = input.split_whitespace();
Command {
name: parts.next().unwrap(),
args: parts.collect(),
}
}Windowing
struct Window<'a, T> {
items: &'a [T],
start: usize,
end: usize,
}
impl<'a, T> Window<'a, T> {
fn new(slice: &'a [T]) -> Self {
Window {
items: slice,
start: 0,
end: slice.len(),
}
}
}Caching
struct Cache<'a> {
key: String,
value: &'a str,
}
impl<'a> Cache<'a> {
fn get(&self) -> &'a str {
self.value
}
}Lifetime and Closures
Closures capture with lifetimes:
fn apply<'a, F>(f: F, s: &'a str) -> &'a str
where
F: Fn(&'a str) -> &'a str,
{
f(s)
}Best Practices
- 1Let the compiler guide you
- 2Prefer explicit lifetimes when elision isn't clear
- 3Keep lifetimes as short as possible
- 4Use 'static sparingly
- 5Consider returning owned types if lifetimes get complex
- 6Document lifetime requirements in docs
Performance Implications
Lifetimes have zero runtime cost:
// All these are the same at runtime
let s = String::from("hello");
let r: &str = &s;
let r: &'static str = "hello";Lifetimes are compile-time only.
Conclusion
Lifetimes are Rust's mechanism for ensuring references are always valid. They prevent use-after-free and dangling reference bugs at compile-time, enabling safe systems programming without a garbage collector.
Master lifetimes, and you'll write Rust code that's both safe and flexible. The compiler will guide you toward correct code, catching entire classes of bugs before they can cause problems.
Practice with lifetimes until they become intuitive. They're a key part of what makes Rust special, enabling memory safety without sacrificing performance.