Lifetimes
Lifetimes ensure that references are valid for as long as they're needed. They're Rust's way of preventing dangling pointers and use-after-free errors at compile time. While often inferred, understanding lifetimes is crucial for advanced Rust programming.
What are Lifetimes?
A lifetime is the scope for which a reference is valid. Every reference in Rust has a lifetime, though it's often implicit and inferred by the compiler.
fn main() {
let r; // Lifetime 'a starts
{
let x = 5; // Lifetime 'b starts
r = &x; // Error: x doesn't live long enough
} // Lifetime 'b ends, x is dropped
println!("r: {}", r); // Error: r references dropped value
} // Lifetime 'a ends
The problem is that r
holds a reference to x
, but x
is dropped before r
tries to use it.
Basic Lifetime Syntax
Lifetime annotations don't change how long references live - they describe the relationships between lifetimes.
Lifetime Annotation Syntax
&i32 // A reference
&'a i32 // A reference with an explicit lifetime
&'a mut i32 // A mutable reference with an explicit lifetime
Lifetime names:
- Start with an apostrophe:
'a
,'b
,'lifetime
- Are usually short:
'a
,'b
,'c
- Must be declared before use
Functions with Lifetimes
When a function takes references as parameters or returns references, you often need lifetime annotations:
Problem: Ambiguous Lifetimes
// This won't compile - ambiguous lifetimes
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
The compiler doesn't know whether the returned reference should live as long as x
or y
.
Solution: Explicit 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 is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
This means:
- Both
x
andy
must live at least as long as lifetime'a
- The returned reference will be valid for lifetime
'a
- The actual lifetime is the smaller of the two input lifetimes
Lifetime Elision Rules
The compiler can often infer lifetimes using three rules:
Rule 1: Each Parameter Gets Its Own Lifetime
// Written by programmer:
fn first_word(s: &str) -> &str
// Inferred by compiler:
fn first_word<'a>(s: &'a str) -> &str
Rule 2: Single Input Lifetime is Assigned to All Outputs
// Written by programmer:
fn first_word(s: &str) -> &str
// Inferred by compiler:
fn first_word<'a>(s: &'a str) -> &'a str
Rule 3: Multiple Parameters with &self
or &mut self
The lifetime of self
is assigned to all output lifetime parameters:
impl<'a> ImportantExcerpt<'a> {
// Written by programmer:
fn announce_and_return_part(&self, announcement: &str) -> &str
// Inferred by compiler:
fn announce_and_return_part(&self, announcement: &str) -> &str // self's lifetime
}
Examples of Lifetime Annotations
Different Lifetime Patterns
// Same lifetime for all parameters and return
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Return lifetime tied to first parameter only
fn first<'a>(x: &'a str, y: &str) -> &'a str {
x
}
// Multiple lifetime parameters
fn compare<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where 'b: 'a // 'b must outlive 'a
{
if x.len() > y.len() { x } else { x } // Can only return x
}
// No relationship between input and output lifetimes
fn make_string() -> &'static str {
"This is a static string"
}
Functions That Don't Compile
// Error: Can't return reference to local variable
fn dangle<'a>() -> &'a str {
let s = String::from("hello");
&s // s is dropped at end of function
}
// Error: Can't determine which input lifetime to use
fn longest_broken<'a, 'b>(x: &'a str, y: &'b str) -> &str {
if x.len() > y.len() { x } else { y }
}
Lifetimes in Structs
Structs can hold references, but need 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 please: {}", announcement);
self.part // Returns reference with same lifetime as self
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
println!("Important excerpt: {}", i.part);
}
Multiple Lifetimes in Structs
struct MultiRef<'a, 'b> {
x: &'a str,
y: &'b str,
}
fn main() {
let string1 = String::from("long string");
let string2 = String::from("short");
let multi = MultiRef {
x: &string1,
y: &string2,
};
println!("x: {}, y: {}", multi.x, multi.y);
}
Advanced Lifetime Patterns
Lifetime Bounds
Specify that one lifetime must outlive another:
fn longest_with_bound<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b must outlive 'a
{
if x.len() > y.len() {
x
} else {
y // This is allowed because 'b: 'a
}
}
Higher-Ranked Trait Bounds (HRTB)
For closures that work with any lifetime:
fn apply_to_all<F>(strings: &[String], f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
for s in strings {
println!("{}", f(s));
}
}
fn main() {
let strings = vec![
String::from("hello"),
String::from("world"),
];
apply_to_all(&strings, |s| {
if s.len() > 4 { s } else { "short" }
});
}
Lifetime Subtyping
Longer lifetimes can be used where shorter ones are expected:
fn choose_first<'a>(first: &'a str, _second: &str) -> &'a str {
first
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = choose_first(&string1, &string2);
} // string2 dropped here
println!("The result is {}", result); // Works: result has lifetime of string1
}
Static Lifetime
The 'static
lifetime denotes references that live for the entire program duration:
// String literals have 'static lifetime
let s: &'static str = "I have a static lifetime.";
// Static variables
static HELLO_WORLD: &str = "Hello, world!";
fn return_static() -> &'static str {
"This string is stored in the program binary"
}
// Box::leak can create 'static references
fn leak_example() -> &'static str {
let string = String::from("leaked");
Box::leak(string.into_boxed_str())
}
When to Use 'static
// Good: String literals are naturally 'static
const MESSAGE: &'static str = "Error occurred";
// Often unnecessary: lifetime elision works
fn get_message() -> &'static str {
"Hello"
}
// Could be simplified to:
fn get_message_simple() -> &str {
"Hello"
}
Common Lifetime Patterns
Returning References to Input
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string);
println!("First word: {}", word);
}
Borrowing from Multiple Sources
struct Container<'a> {
data: &'a str,
}
impl<'a> Container<'a> {
fn new(data: &'a str) -> Self {
Container { data }
}
fn get_data(&self) -> &str {
self.data
}
// Method that borrows from self and parameter
fn compare(&self, other: &str) -> &str {
if self.data.len() > other.len() {
self.data
} else {
// Can't return other because lifetimes don't match
"equal or shorter"
}
}
}
Lifetime Bounds in Generic Functions
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
}
}
Debugging Lifetime Issues
Common Error: Temporary Value Dropped
fn main() {
let result;
{
let temp = String::from("temporary");
result = temp.as_str(); // Error: temp doesn't live long enough
}
println!("{}", result);
}
// Solution: Move the string instead
fn main() {
let result;
{
let temp = String::from("temporary");
result = temp; // Move the String
}
println!("{}", result);
}
Common Error: Conflicting Lifetimes
// Problem
fn problematic<'a>(x: &'a str, y: &'a str) -> (&'a str, &'a str) {
if x.len() > y.len() {
(x, y)
} else {
(y, x)
}
}
// Solution: Use different lifetimes if they don't need to be the same
fn better<'a, 'b>(x: &'a str, y: &'b str) -> (&'a str, &'b str) {
(x, y)
}
Lifetime Elision in Practice
Cases Where Annotations Are Required
// Multiple inputs, unclear which lifetime to use for output
fn needs_annotation<'a>(x: &'a str, y: &str) -> &'a str {
x
}
// Multiple inputs, returning both
fn needs_annotation2<'a>(x: &'a str, y: &'a str) -> (&'a str, &'a str) {
(x, y)
}
Cases Where Annotations Are Optional
// Single input: lifetime is clear
fn no_annotation_needed(s: &str) -> &str {
&s[1..]
}
// Methods: self's lifetime is used
impl<'a> Container<'a> {
fn get(&self) -> &str {
self.data
}
}
Best Practices
1. Start Without Lifetime Annotations
Let the compiler tell you when they're needed:
// Start with this
fn first_word(s: &str) -> &str {
// Implementation
}
// Add annotations only if compiler requires them
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// Implementation
}
2. Use Descriptive Lifetime Names for Complex Cases
// For simple cases, 'a is fine
fn simple<'a>(x: &'a str) -> &'a str { x }
// For complex cases, use descriptive names
fn complex<'input, 'output>(
data: &'input str,
cache: &mut HashMap<&'input str, &'output str>
) -> &'output str {
// Implementation
}
3. Prefer Owned Types When Lifetimes Get Complex
// Instead of complex lifetime relationships
fn complex_lifetimes<'a, 'b>(x: &'a str, y: &'b str) -> SomeStruct<'a, 'b> {
// Complex implementation
}
// Consider using owned types
fn simpler(x: String, y: String) -> SomeOwnedStruct {
// Simpler implementation
}
4. Use Lifetime Bounds Sparingly
// Only when necessary
fn with_bound<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a,
{
if x.len() > y.len() { x } else { y }
}
Lifetimes ensure memory safety by preventing dangling references at compile time. While they can seem complex, the compiler's error messages and lifetime elision rules make them manageable in most cases. Focus on understanding the relationships between references rather than memorizing syntax rules.