Lifetimes in Rust
Lifetimes are Rust's way of ensuring references are valid for as long as they're used. Every reference has a lifetime, which is the scope for which that reference is valid.
Understanding Lifetimes
The Borrow Checker
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
// println!("{}", r); // Error! |
} // ---------+
Lifetime Annotation Syntax
Lifetime annotations don't change how long references live - they describe the relationships between lifetimes.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
Function Lifetimes
When Lifetimes Are Needed
// This won't compile without lifetime annotations
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// With lifetime annotations
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 string");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("Longest: {}", result);
}
Different Lifetimes
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// Multiple lifetimes
fn longest_with_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("Announcement: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
Lifetime Elision Rules
The compiler uses three rules to infer lifetimes:
- Each input reference gets its own lifetime
- If there's one input lifetime, it's assigned to all outputs
- If there's
&selfor&mut self, its lifetime is assigned to outputs
// Original
fn first_word(s: &str) -> &str {
// ...
}
// After rule 1
fn first_word<'a>(s: &'a str) -> &str {
// ...
}
// After rule 2
fn first_word<'a>(s: &'a str) -> &'a str {
// ...
}
Struct Lifetimes
Structs can hold references with lifetime annotations:
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: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}
Static Lifetime
The 'static lifetime means the reference can live for the entire program:
fn main() {
let s: &'static str = "I have a static lifetime.";
// String literals have 'static lifetime
let literal = "Hello, world!";
// This function requires 'static
fn needs_static(s: &'static str) {
println!("Static string: {}", s);
}
needs_static(literal);
}
Advanced Lifetime Patterns
Lifetime Bounds
use std::fmt::Display;
fn longest_with_display<'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
}
}
Multiple Lifetime Parameters
struct Context<'s> {
text: &'s str,
}
struct Parser<'c, 's> {
context: &'c Context<'s>,
}
impl<'c, 's> Parser<'c, 's> {
fn parse(&self) -> Result<(), &'s str> {
Err(&self.context.text[1..])
}
}
fn parse_context(context: Context) -> Result<(), &str> {
Parser { context: &context }.parse()
}
Lifetime Subtyping
struct Context<'a> {
text: &'a str,
}
// 'a: 'b means 'a lives at least as long as 'b
impl<'a: 'b, 'b> Context<'a> {
fn get_ref(&'b self) -> &'a str {
self.text
}
}
Common Lifetime Scenarios
Returning References from Functions
// Can't return reference to local variable
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // Error: s goes out of scope
// }
// Return owned value instead
fn no_dangle() -> String {
let s = String::from("hello");
s
}
// Or accept reference and return part of it
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
Mutable References
fn change_and_return<'a>(s: &'a mut String) -> &'a mut String {
s.push_str(" world");
s
}
fn main() {
let mut s = String::from("hello");
let s_ref = change_and_return(&mut s);
println!("{}", s_ref);
}
Practical Examples
String Manipulation
struct StringSplitter<'a> {
remainder: &'a str,
delimiter: &'a str,
}
impl<'a> StringSplitter<'a> {
fn new(string: &'a str, delimiter: &'a str) -> Self {
StringSplitter {
remainder: string,
delimiter,
}
}
}
impl<'a> Iterator for StringSplitter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if let Some(index) = self.remainder.find(self.delimiter) {
let part = &self.remainder[..index];
self.remainder = &self.remainder[index + self.delimiter.len()..];
Some(part)
} else if !self.remainder.is_empty() {
let part = self.remainder;
self.remainder = "";
Some(part)
} else {
None
}
}
}
fn main() {
let text = "a,b,c,d";
let splitter = StringSplitter::new(text, ",");
for part in splitter {
println!("Part: {}", part);
}
}
Cache with Lifetime
use std::collections::HashMap;
struct Cache<'a> {
data: HashMap<&'a str, &'a str>,
}
impl<'a> Cache<'a> {
fn new() -> Self {
Cache {
data: HashMap::new(),
}
}
fn insert(&mut self, key: &'a str, value: &'a str) {
self.data.insert(key, value);
}
fn get(&self, key: &'a str) -> Option<&&'a str> {
self.data.get(key)
}
}
fn main() {
let key = String::from("name");
let value = String::from("Alice");
let mut cache = Cache::new();
cache.insert(&key, &value);
if let Some(v) = cache.get(&key) {
println!("Found: {}", v);
}
}
Best Practices
- Let the compiler infer lifetimes when possible
- Use lifetime elision - don't annotate unnecessarily
- Name lifetimes meaningfully when multiple are involved
- Consider ownership instead of complex lifetimes
- Use
'staticsparingly - it's often not what you want - Think about lifetime relationships not absolute durations
- Simplify by returning owned values when lifetimes get complex
- Document lifetime requirements in complex APIs
Common Errors and Solutions
Lifetime Mismatch
// Error: lifetime mismatch
// fn invalid<'a>() -> &'a str {
// let s = String::from("hello");
// &s // s doesn't live long enough
// }
// Solution: return owned value
fn valid() -> String {
String::from("hello")
}
Multiple Possible Lifetimes
// Error: compiler can't determine lifetime
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// Solution: explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}