Lifetimes allow us to ensure that references are valid as long as we need them to be.
Every reference in rust has a lifetime, which is the scope for which that reference is valid. Most of the time, lifetimes are implicit and inferred, just like how must of the time types are inferred.
When the lifetimes of references could be related in different ways we must annotate the lifetimes to inform the compiler of what we actually want. The primary goal of lifetimes is the prevention of dangling references, which cause a program to reference data other than the data it's intended to reference.
missing lifetime specifier
This function will not compile. Rust cannot tell at compile time whether the
reference in the return type refers to x or y because the if statement could
return either.
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
We also don't know the concrete lifetimes of the references that will be passed
in, so we can't look at the scopes to determine whether the reference we return
will always be valid. The borrow checker can't determine this either, because it
doesn't know how the lifetimes of x and y relate to the lifetime of the
return value.
To fix this error we'll add generic lifetime parameters that define the relationship between the references so the borrow checker can perform its analysis.
annotations
Lifetime annotations don't change how long any of the references life. Rather, they describe the relationships of the lifetimes of multiple references to each other.
Just as functions can accept any type when the signature specifies a generic type parameter, functions can accept references with any lifetime by specifying a generic lifetime parameter.
The names of lifetime parameters must start with an apostrophe ' and are
usually all lowercase and short. Most people use 'a for the first lifetime
parameter.
To fix the earlier function we need to express the following constraint: the returned reference will be valid as long as both the parameters are valid. This is the relationship between the lifetimes of the parameters and the return value.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
This function signature now tells rust that for some lifetime 'a, the function
takes two parameters, both of which are string slices that live at least as long
as lifetime 'a. The function signature also tells rust that the string slice
returned from the function will live at least as long as lifetime 'a. In
practice, it means that he lifetime of the reference returned by the longest
function is the same as the smaller of the lifetimes of the values referred to
by the function arguments. These relationships are what we want rust to use when
analyzing this code.
Note that the longest function doesn't need to know exactly how long x and y will live, only that some scope can be substituted for 'a that will satisfy this signature.
In this example we try to use result after string2 has gone out of scope,
but since our function declares that the lifetime of both arguments must be
around at least as long as the result.
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}");
}
If the print was instead inside the inner block there would be no issues:
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}");
}
}
dangling references
Lifetime annotations do not change the lifetime our types, they just annotate it. This code fails to compile:
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
Despite trying to provide a lifetime annotation this will still fail to compile because result goes out of scope when our function ends and returning a pointer to it would create a dangling pointer.
We could simply return the owned type rather than using references and lifetimes:
fn longest(x: &str, y: &str) -> String {
if x.len() > y.len() {
String::from(x)
} else {
String::from(y)
}
}
static lifetimes
The 'static lifetime is a special lifetime annotation which denotes that the
affected reference can live for the entire duration of the program. All string
literals have the 'static lifetime, which we can annotate as follows:
let s: &'static str = "I have a static lifetime.";
You might see suggestions to use the 'static lifetime in error messages. But
before specifying 'static as the lifetime for a reference, think about whether
the reference you have actually lives the entire lifetime of your program or
not, and whether you want it to. Most of the time, an error message suggesting
the 'static lifetime results from attempting to create a dangling reference or a
mismatch of the available lifetimes. In such cases, the solution is to fix those
problems, not to specify the 'static lifetime.