Rust by Example: Lifetimes Basics
Grasp the concept of Lifetimes, Rust's mechanism for ensuring references are valid. This example explains how the borrow checker uses lifetimes to prevent dangling pointers and ensure memory safety.
Code
fn main() {
// --- Valid Reference ---
let r;
{
let x = 5;
r = &x;
// r refers to x, and x is valid here
println!("r: {}", r);
}
// x is dropped here
// println!("r: {}", r); // ERROR: 'x' does not live long enough
// --- Lifetimes in Functions ---
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
// &i32 // a reference
// &'a i32 // a reference with an explicit lifetime
// &'a mut i32 // a mutable reference with an explicit lifetime
// We must annotate lifetimes here because Rust can't tell
// if the returned reference refers to x or y.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}Explanation
Lifetimes are the mechanism Rust uses to ensure that references are valid. A lifetime is simply the scope for which a reference is valid. The Borrow Checker validates these scopes at compile time to prevent Dangling Pointers (references that point to invalid data) and Use-After-Free errors.
Most of the time, lifetimes are implicit and inferred by the compiler. However, when a function returns a reference, the compiler needs to know how the lifetime of the return value relates to the lifetimes of the input arguments. We use explicit annotations (like 'a) to describe these relationships. Note that annotations do not change how long a value lives; they only help the compiler verify that the code is safe.
In the longest function, the annotation <'a> tells the compiler: "The returned reference will live at least as long as the shorter of the two input lifetimes." This ensures that the caller cannot use the result after one of the inputs has become invalid.
Code Breakdown
println!("r: {}", r);. This works because x is still in scope. If we moved this line outside the inner block (after line 16), it would fail because x would have been dropped.fn longest<'a>(...). The <'a> syntax declares a generic lifetime parameter. It doesn't change how long values live; it just connects the lifetimes of the parameters and return value for the compiler's analysis.-> &'a str. This return type annotation guarantees that the returned reference is valid for at least lifetime 'a, which is the intersection (overlap) of the lifetimes of the inputs.
