From https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
Dangling references
Lifetime ensure that references are valid as long as we need them to be. Lifetime prevents dangling references.
Every reference has a reference, which is the scope for which that reference is valid.
Most of the time, lifetimes are inferred and implicit.
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
The code above won’t compile. The reason is clearly explained by the compiler
error[E0597]: `x` does not live long enough
|
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
Borrow checker
Rust complier has a borrow checker compares scopes to determine whether all borrows (references) are valid.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
Notice println
uses r
, but r
would’ve been dropped by that time.
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
Here, r
lives long enough for println
to use it.
Lifetimes in functions
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
The above code produces this error:
error[E0106]: missing lifetime specifier
|
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 {
| ++++ ++ ++ ++
The return type needs a generic lifetime parameter on it because Rust can’t tell whether the reference being returned refers to x
or y
. Actually, we don’t know either.
We need generic lifetime parameters that define the relationship between the references so the borrow checker can perform its analysis.
Generic lifetime describe the relationships of the lifetimes of multiple references to each other without affecting the lifetimes.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
We want the signature in the example below to express the following constraint: The returned reference will be valid as long as both the parameters are valid.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Because we’ve annotated the returned reference with the same lifetime parameter 'a
, the returned reference will also be valid for the length of the smaller of the lifetimes of x
and y
.
We’re specifying that the borrow checker should reject any values that don’t adhere to these constraints
Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.
Complies:
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}");
}
Doesn’t compile:
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}");
Error:
error[E0597]: `string2` does not live long enough
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| -------- borrow later used here
Lifetimes in structs
We can define structs to hold references, but in that case we would need to add a lifetime annotation on every reference in the struct’s definition.
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 annotation means an instance of ImportantExcerpt
can’t outlive the reference it holds in its part
field.
Lifetime elision rules
Lifetime elision allows us to omit lifetime annotations when the compiler is able to infer it after applying these 3 rules:
- Compiler assigns a lifetime parameter to each parameter that’s a reference.
- If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters.
- If there are multiple input lifetime parameters, but one of them is
&self
or&mut self
, the lifetime ofself
is assigned to all output lifetime.
fn first_word(s: &str) -> &str {
// Equivalent by first rule
fn first_word<'a>(s: &'a str) -> &str {
// Equivalent by second rule
fn first_word<'a>(s: &'a str) -> &'a str {
Requires explicit lifetime annotations:
fn longest(x: &str, y: &str) -> &str {
// Equivalent by first rule
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
You can see that the second rule doesn’t apply because there is more than one input lifetime. Neither does the third, because it’s not a method.
Even with all 3 rules applied, it is still not clear what the output lifetime is. Thus, we need to annotate the lifetimes explicitly, similar the example in Lifetimes in functions.