Lifetimes
Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime - the scope for which that reference is valid. Most of the time, lifetimes are inferred, but sometimes you need to annotate them explicitly.
What Are Lifetimes?
A lifetime is the scope during which a reference is valid. Consider this:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
// println!("{}", r); // ERROR: x doesn't live long enough
} // ---------+
The reference r has lifetime 'a, but it refers to x which only has lifetime 'b. Since 'b is shorter than 'a, the code won't compile.
Lifetime Annotation Syntax
Lifetime annotations describe relationships between lifetimes:
&i32 // a reference
&'a i32 // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'a
When You Need Lifetime Annotations
The compiler needs help when:
- A function returns a reference
- A struct holds references
- Multiple references have ambiguous relationships
Function Signatures
// This won't compile - Rust doesn't know which input's lifetime to use
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// Solution: annotate with lifetime 'a
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 = String::from("short");
let result = longest(&string1, &string2);
println!("The longest string is: {}", result);
}
The 'a annotation means: "the returned reference will be valid for the smaller of the two input lifetimes."
Lifetime Elision Rules
Rust has rules for inferring lifetimes so you don't always need annotations:
Rule 1: Each Input Gets Its Own Lifetime
// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &str { ... }
Rule 2: One Input Lifetime = Output Lifetime
// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
Rule 3: &self Lifetime = Output Lifetime
impl MyStruct {
// Written:
fn get_name(&self) -> &str { ... }
// Compiler infers:
fn get_name<'a>(&'a self) -> &'a str { ... }
}
Structs with References
When a struct holds references, you must annotate lifetimes:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}
The annotation 'a means: "an instance of ImportantExcerpt can't outlive the reference it holds."
Methods with Lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// Lifetime elision: &self lifetime is used for return
fn level(&self) -> i32 {
3
}
// Return type uses 'a from struct
fn announce_and_return_part(&self, announcement: &str) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = ImportantExcerpt {
part: novel.split('.').next().unwrap(),
};
println!("Level: {}", excerpt.level());
println!("Part: {}", excerpt.announce_and_return_part("Here it comes!"));
}
The Static Lifetime
'static means the reference lives for the entire program duration:
fn main() {
// String literals have 'static lifetime
let s: &'static str = "I live forever!";
println!("{}", s);
}
Use 'static sparingly - it's usually a sign you should reconsider your design.
Multiple Lifetime Parameters
Sometimes you need multiple lifetime parameters:
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 }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!");
let ann = String::from("Comparing strings");
let result = longest_with_announcement(&s1, &s2, &ann);
println!("Longest: {}", result);
}
Lifetime Bounds
You can specify that a generic type must live at least as long as a lifetime:
fn print_ref<'a, T>(t: &'a T)
where
T: std::fmt::Display + 'a,
{
println!("{}", t);
}
fn main() {
let x = 5;
print_ref(&x);
}
Common Lifetime Patterns
Pattern 1: Input/Output Relationship
fn first_word<'a>(s: &'a str) -> &'a str {
match s.find(' ') {
Some(pos) => &s[..pos],
None => s,
}
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word);
}
Pattern 2: Struct Holding a Reference
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Parser<'a> {
Parser { input, position: 0 }
}
fn remaining(&self) -> &'a str {
&self.input[self.position..]
}
}
fn main() {
let text = String::from("hello world");
let parser = Parser::new(&text);
println!("Remaining: {}", parser.remaining());
}
Pattern 3: Returning References from Methods
struct Container {
data: Vec<String>,
}
impl Container {
fn get(&self, index: usize) -> Option<&String> {
self.data.get(index)
}
fn first(&self) -> Option<&String> {
self.data.first()
}
}
fn main() {
let container = Container {
data: vec![String::from("a"), String::from("b")],
};
if let Some(first) = container.first() {
println!("First: {}", first);
}
}
Practice Exercise
// A struct that borrows a string slice
struct Highlight<'a> {
text: &'a str,
start: usize,
end: usize,
}
impl<'a> Highlight<'a> {
fn new(text: &'a str, start: usize, end: usize) -> Highlight<'a> {
Highlight { text, start, end }
}
fn highlighted_portion(&self) -> &'a str {
&self.text[self.start..self.end]
}
fn full_text(&self) -> &'a str {
self.text
}
}
fn main() {
let document = String::from("Rust is a systems programming language");
let highlight = Highlight::new(&document, 0, 4);
println!("Highlighted: '{}'", highlight.highlighted_portion());
println!("Full text: '{}'", highlight.full_text());
}
Key Takeaways
- Lifetimes ensure references are always valid
- Most lifetimes are inferred by the compiler
- Use
'asyntax when the compiler needs help - Lifetime annotations describe relationships, they don't change how long things live
- Structs holding references need lifetime parameters
'staticmeans "lives for the entire program"- Lifetime elision rules reduce annotation boilerplate
Lifetimes are one of Rust's most powerful features for memory safety!