6.6 Lifetimes: Ensuring References Remain Valid
Rust lifetimes, those '_
things, are not directly about the liveness scope of values or variables, nor are they about when a value gets destructed. Instead, they are primarily about the duration of borrows. They are a compile-time concept, a type-level property that gets discarded after borrow checking completes and is not present during runtime. Variables themselves do not inherently have “Rust lifetimes” (those '_
things); rather, these lifetimes parameterize types, especially references.
Every reference in Rust has a lifetime, but the compiler can often infer them without explicit annotation through a set of rules called lifetime elision rules. You only need to write lifetime annotations when the compiler’s inference rules are insufficient to guarantee safety, typically in function or struct definitions involving references where the relationships between input and output reference lifetimes are ambiguous.
6.6.1 Explicit Lifetime Annotation Syntax
When you need to be explicit, lifetime annotations use the following syntax:
- Names: Lifetime names start with an apostrophe (
'
) followed by a short, lowercase name (conventionally starting from'a
, e.g.,'a
,'b
,'input
). The name'static
has a special, reserved meaning (see below). - Declaration: Generic lifetime parameters are declared in angle brackets after a function name (e.g.,
fn my_func<'a, 'b>
) or struct/enum name (e.g.,struct MyStruct<'a>
). - Usage: The lifetime name is placed after the
&
(or&mut
) in a reference type (e.g.,x: &'a str
,y: &'b mut i32
).
Lifetime annotations do not change how long any values live. Instead, they describe the relationships between the validity scopes (lifetimes) of different references, allowing the borrow checker to verify that references are used safely. They act as constraints for the compiler’s analysis.
Example: Function with Lifetimes
Consider a function that returns the longer of two string slices. Because the returned reference borrows from one of the inputs, the compiler needs explicit annotations to know how the lifetime of the output relates to the lifetimes of the inputs.
// `<'a>` declares a generic lifetime parameter `'a`. // `x: &'a str` and `y: &'a str` constrain both input slices to live at least // as long as `'a`. // `-> &'a str` declares that the ret. slice is also bound by this same 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 is long"); let result; { let string2 = String::from("xyz"); // The compiler enforces that 'a is, at most, the shorter borrow duration // of string1 and string2 relevant to this call. result = longest(&string1, &string2); println!("The longest string is '{}'", result); // Works here, result is valid. } // println!("The longest string is '{}'", result); // Compile-time error! // The borrow duration associated with `result` (which might point to `string2`'s // data) has ended because `string2` went out of scope. Using `result` here // would risk accessing freed memory. }
The annotation 'a
connects the lifetimes: the returned reference is guaranteed to be valid only as long as both input references (x
and y
) are valid. If the function tried to return a reference to data created inside the function (like the dangle
example earlier), the compiler would reject it because that data’s lifetime would be shorter than the required lifetime 'a
.
Important Clarification: When a function’s return type is annotated with a lifetime from an input (e.g., -> &'a str
), it signifies that uses of the returned value keep the corresponding input borrow(s) active. It’s not that the lifetime of the return value is decided at the call site based on the inputs; rather, the borrow checker verifies that the returned reference’s actual use duration respects the longest possible duration implied by its inputs.
The 'static
Lifetime
The special lifetime 'static
indicates that a reference is valid for the entire duration of the program. String literals (&'static str
) have this lifetime because their data is embedded in the program’s binary. References to global constants or leaked Box
es can also have the 'static
lifetime.
Mastering lifetimes, particularly understanding elision rules and when annotations are needed, is key to leveraging Rust’s compile-time safety guarantees effectively. We’ll encounter more complex lifetime scenarios later.