Lifetimes
Master the borrow checker's way of preventing use-after-free errors.
§Lifetime Basics
Lifetimes track how long references are valid:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
This won't compile. The compiler doesn't know how long the returned reference lives:
error[E0106]: missing lifetime specifier
§Explicit Lifetime Annotations
Use 'a to denote a lifetime:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
This says: "The returned reference lives as long as both input references."
Using the Function
let s1 = String::from("Hello");
let s2 = String::from("World");
let result = longest(&s1, &s2);
println!("{}", result); // s1 and s2 still valid
This fails because the returned reference outlives the inputs:
let s1 = String::from("Hello");
let result;
{
let s2 = String::from("World");
result = longest(&s1, &s2); // ERROR: s2 doesn't live long enough
}
println!("{}", result);
"Lifetimes don't change runtime behavior. They're annotations that help the compiler verify safety."
§Multiple Lifetimes
fn first_string<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x // returns reference with lifetime 'a
}
§Lifetime in Structs
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_word = novel.split(' ').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_word,
};
println!("Excerpt: {}", excerpt.part);
}
The struct can't outlive the reference it holds:
let excerpt;
{
let s = String::from("Hello");
excerpt = ImportantExcerpt { part: &s };
} // s is dropped here
println!("{}", excerpt.part); // ERROR: s no longer valid
§Lifetime in Impl Blocks
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
§Lifetime Elision
Rust has rules that allow you to omit lifetime annotations:
Rule 1: Each parameter gets its own lifetime
fn first(x: &str) -> &str {
// Expanded to: fn first<'a>(x: &'a str) -> &'a str
x
}
Rule 2: If one input lifetime, assign to output
fn longest(x: &str, y: &str) -> &str {
// Ambiguous - can't apply rule
x
}
This still requires explicit lifetimes because there are two inputs.
Rule 3: If &self, its lifetime is assigned to output
struct MyStruct;
impl MyStruct {
fn get_ref(&self) -> &String {
// The output lifetime is &self's lifetime
}
}
§Lifetime Bounds
Use lifetime bounds with trait objects:
struct Context<'a> {
message: &'a str,
}
trait Logger {
fn log(&self);
}
impl<'a> Logger for Context<'a> {
fn log(&self) {
println!("{}", self.message);
}
}
§Static Lifetime
'static means the reference lives for the entire program:
let s: &'static str = "Hello"; // string literals are 'static
const GREETING: &'static str = "Welcome";
Functions returning 'static:
fn get_greeting() -> &'static str {
"Hello" // string literals are 'static
}
§Higher-Ranked Trait Bounds
fn call_twice<F>(f: F)
where
F: for<'a> Fn(&'a str),
{
f("Hello");
f("World");
}
call_twice(|s| println!("{}", s));
The for<'a> means: "This function works for any lifetime you choose."
§Variance
Lifetimes are covariant:
fn assign_refs<'a, 'b: 'a>(r: &'a i32, s: &'b i32) {
let _: &'a i32 = s; // OK: 'b lives at least as long as 'a
}
§Common Patterns
Owned Data is Easiest
fn process(s: String) -> String {
// No lifetime issues with owned data
s
}
Returning References Requires Care
fn process(s: &str) -> &str {
// Must be explicit about which input's lifetime
s
}
Returning Owned Data from References
fn process(s: &str) -> String {
// No lifetime needed - owned data
s.to_string()
}
§Practice
- Write a function that takes two string slices and returns the longer one
- Add explicit lifetime annotations to the function
- Create a struct holding a reference and verify lifetime constraints
- Implement a method on a struct with references
- Understand why you can't return a local reference
- Compare code with owned vs borrowed data
- Use the
'staticlifetime in a function
"Lifetimes aren't about complexity; they're about clarity. They document relationships between references in your code."