Validating References with Lifetimes
Lifetimes ensure references remain valid for their required duration. Every reference has a lifetime—the scope where it’s valid. Most lifetimes are inferred, but explicit annotation is required when relationships between reference lifetimes are ambiguous.
Preventing Dangling References
Lifetimes prevent dangling references by ensuring referenced data outlives the reference:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
This fails because x
is destroyed before r
is used:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| --- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
The Borrow Checker
The borrow checker compares scopes to validate borrows:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
The reference r
(lifetime 'a
) outlives the referenced data x
(lifetime 'b
).
Fixed version:
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
Generic Lifetimes in Functions
Functions returning references need lifetime parameters when the compiler can’t determine the relationship between input and output lifetimes:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
Error:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Lifetime Annotation Syntax
Lifetime parameters start with an apostrophe and are typically lowercase:
&i32 // a reference
&'a i32 // a reference with explicit lifetime
&'a mut i32 // a mutable reference with explicit lifetime
Lifetime Annotations in Function Signatures
Declare lifetime parameters in angle brackets:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
This signature means:
- Both parameters live at least as long as lifetime
'a
- The returned reference will live at least as long as lifetime
'a
- The actual lifetime is the smaller of the two input lifetimes
Example usage:
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}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Invalid usage:
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Thinking in Terms of Lifetimes
Only specify lifetime parameters for references that affect the output. If a function always returns the first parameter, the second parameter doesn’t need a lifetime annotation:
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
Returning references to values created within the function creates dangling references:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
Lifetime Annotations in Struct Definitions
Structs holding references need lifetime annotations:
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
This ensures the struct instance can’t outlive the reference it holds.
Lifetime Elision
Three rules eliminate the need for explicit lifetime annotations in common cases:
- Input lifetimes: Each reference parameter gets its own lifetime
- Single input: If there’s exactly one input lifetime, it’s assigned to all outputs
- Methods: If one parameter is
&self
or&mut self
, its lifetime is assigned to all outputs
Examples:
// Rule 1: fn foo<'a>(x: &'a i32)
fn first_word(s: &str) -> &str {
// Rule 2: fn foo<'a>(x: &'a i32) -> &'a i32
fn first_word<'a>(s: &'a str) -> &'a str {
For multiple inputs without self
, explicit annotations are required:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { // Error: can't determine output lifetime
Lifetime Annotations in Method Definitions
Struct lifetime parameters must be declared after impl
:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
Third elision rule example:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
The Static Lifetime
The 'static
lifetime indicates data living for the entire program duration:
let s: &'static str = "I have a static lifetime.";
String literals are stored in the binary and have 'static
lifetime. Use 'static
only when data truly lives for the program’s entire duration.
Generics, Trait Bounds, and Lifetimes Together
Example combining all three concepts:
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {result}"); } use std::fmt::Display; fn longest_with_an_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 } }
This function:
- Is generic over type
T
(withDisplay
trait bound) - Has lifetime parameter
'a
for references - Ensures the returned reference lives as long as both inputs
Lifetimes provide compile-time memory safety guarantees without runtime overhead.