If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
10.5.1 What Is a Lifetime
Every reference in Rust has its own lifetime. The purpose of a lifetime is to keep a reference valid; in other words, it is the scope during which a reference remains valid.
In most cases, lifetimes are implicit and can be inferred. If the lifetimes of references may be related in different ways, you must annotate lifetimes manually.
Lifetimes are probably the most distinctive feature of Rust compared with other languages, so they are very hard to learn.
10.5.2 Why Lifetimes Exist
The main purpose of lifetimes is to avoid dangling references. This concept was already discussed in 4.4. Reference and Borrowing, and I’ll repeat the earlier explanation here:
When using pointers, it is very easy to trigger an error called a Dangling Pointer. It is defined as follows: a pointer references an address in memory, but that memory may already have been freed and reallocated for someone else to use. If you reference some data, the Rust compiler guarantees that the data will not go out of scope before the reference does. This is how Rust ensures that dangling references never appear.
Take this example:
fn main() {
let r;
{ // small braces
let x = 5;
r = &x;
}
println!("{}", r);
}
- In this example,
ris declared first but not initialized. The purpose is to letrexist in the scope outside the small braces (as shown by the comment position). Of course, Rust has noNullvalue, sorcannot be used before it is initialized. - Inside the small braces, the variable
xis declared and assigned the value5. The next line assigns a reference toxtor. - After that small braces scope ends,
ris printed outside it.
This code is invalid because when r is printed, x has already gone out of scope and been destroyed. So the value of r—that is, the memory address referenced by x—now points to memory that has already been freed, and the data it points to is no longer x. That creates a dangling reference, so the compiler reports an error.
Output:
error[E0597]: `x` does not live long enough
--> src/main.rs:5:7
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here
The error says that the borrowed value does not live long enough. That is because when the inner-braces scope ends, x goes out of scope, but r has a larger scope and can continue to be used. To ensure program safety, any operation based on r cannot run correctly at that point.
Rust checks whether code is valid through the borrow checker.
10.5.3 The Borrow Checker
The borrow checker compares scopes to determine whether all borrows are valid. In the example above, the borrow checker sees that r is a reference to x, but r lives longer than x, so it reports an error.
How do we solve this problem? Easy: make x live at least as long as r.
fn main() {
let x = 5;
let r = &x;
println!("{}", r);
}
In this case, x lives from line 2 to line 5, and r lives from line 3 to line 5. So x’s lifetime fully covers r’s lifetime, and the program does not report an error.
10.5.4 Generic Lifetimes in Functions
Take this example:
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
}
}
string1is aString, whilestring2is a string slice&str. These two values are passed into thelongestfunction (string1first needs to be converted to&str), and the returned value is printed.The logic of
longestis to compare the two input parameters and return the longer one.
Output:
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 {
| ++++ ++ ++ ++
The error says that a lifetime annotation is missing, more specifically that the return type is missing a lifetime parameter. As the help text says, the function’s return type contains a borrowed value, but the function signature does not say whether that borrowed value comes from x or from y. Consider introducing a named lifetime parameter.
Look at this function again:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
Clearly, the return value of this function is either x or y, but which one it is cannot be known in advance. The specific lifetimes of the two input parameters x and y are also unknown here, if we look at the function on its own. So, unlike the earlier example, we cannot compare scopes to determine whether the returned reference will remain valid. The borrow checker cannot do that either, because it does not know whether the lifetime of the return type is tied to x or to y.
In fact, even if the return value is fixed, writing it this way still causes an error:
fn longest(x: &str, y: &str) -> &str {
x
}
Output:
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 {
| ++++ ++ ++ ++
The compiler still cannot tell, because the function signature does not express where the borrowed value in the return type comes from.
So this has nothing to do with the logic inside the function body; it is entirely about the function signature. How should we change it? We can follow the suggestion in the error message:
= 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 {
| ++++ ++ ++ ++
Since it tells us to add a generic lifetime parameter, we will add one:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
'a represents a lifetime named a. x, y, and the return type all use lifetime a, which means the lifetimes of x, y, and the return value are the same.
The phrase “the same” is not entirely precise, because the actual lifetimes of the x and y values in main differ a little. We will talk about that in the next article.
Now let’s look at the full code:
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
}
}
Output:
The longest string is abcd
Top comments (0)