Lesson 15
Lifetimes
Understand how Rust uses lifetimes to ensure references are always valid.
Why Lifetimes?
Every reference in Rust has a lifetime -- the scope for which that reference is valid. Most of the time, lifetimes are inferred automatically, just like types. But when a function returns a reference, Rust sometimes needs your help to figure out how long that reference should live. That's where lifetime annotations come in.
Lifetime Annotations
Lifetime annotations don't change how long values live -- they describe the relationship between lifetimes of references. The syntax uses an apostrophe: 'a. When you write fn longest<'a>(x: &'a str, y: &'a str) -> &'a str, you're telling Rust: "the returned reference will live at least as long as the shorter of x and y."
Lifetime Elision
Rust has elision rules that let you skip lifetime annotations in common cases. If a function takes one reference and returns one reference, Rust assumes they have the same lifetime. That's why most functions you've written so far didn't need explicit lifetimes. The compiler will tell you when it needs help.
The Borrow Checker
The borrow checker is Rust's compile-time guardian. It tracks every reference to ensure it never outlives the data it points to. If you try to use a reference after the data is dropped, the compiler catches it. Lifetimes are the annotations that help the borrow checker do its job when things get complex.
Try it
// This function returns a reference -- but to which input?
// The lifetime 'a tells Rust: the result lives as long as
// the shorter of the two input lifetimes.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() {
x
} else {
y
}
}
// A struct that holds a reference needs a lifetime annotation
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn first_word(&self) -> &str {
// Elision rule: return lifetime = &self lifetime
self.text.split_whitespace().next().unwrap_or("")
}
fn announce(&self, announcement: &str) -> &str {
// Elision: multiple inputs, but &self gets priority
println!(" Attention: {}", announcement);
self.text
}
}
// One input reference -> no annotation needed (elision rule 1)
fn first_three(s: &str) -> &str {
if s.len() >= 3 {
&s[..3]
} else {
s
}
}
fn main() {
println!("--- Longest String ---");
let result;
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// result is valid here because both strings are in scope
println!(" Longest: {}", result);
}
println!("\n--- Struct with Lifetime ---");
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt {
text: &novel[..15],
};
println!(" Excerpt: {:?}", excerpt.text);
println!(" First word: {:?}", excerpt.first_word());
let text = excerpt.announce("important message");
println!(" Text: {:?}", text);
println!("\n--- Lifetime Elision ---");
let hello = "hello world";
println!(" First three of '{}': '{}'", hello, first_three(hello));
println!(" First three of 'hi': '{}'", first_three("hi"));
// Static lifetime: string literals live for the entire program
println!("\n--- Static Lifetime ---");
let s: &'static str = "I live forever!";
println!(" {}", s);
}Challenge
Try removing the lifetime annotations from the longest() function and read the compiler error. Then add them back. This is the best way to understand why lifetimes exist!
Previous: Generics
Next: Closures & Iterators