1.11.1. Review
In the beginner tutorial, we mentioned that every reference in Rust has a lifetime. A lifetime is the scope in which the reference remains valid, and in most cases it is implicit and inferred by the compiler.
When you take a reference to a variable, the lifetime begins. When the variable is moved or goes out of scope, the lifetime ends. In other words, for a reference, a lifetime is the name of the code region in which it must remain valid.
Lifetimes usually overlap with scopes, but not always.
1.11.2. Borrow Checker
Whenever a reference with some lifetime 'a is used, the borrow checker checks whether 'a is still alive. The process is:
- Trace the path back to where
'abegan — that is, where the reference was obtained - From there, check whether there are conflicts along that path
- Ensure that the reference points to a value that can be accessed safely
This example uses the rand crate. Add the following dependency to Cargo.toml:
[dependencies]
rand = "0.8"
Enter fullscreen mode Exit fullscreen mode
Consider this example:
use rand::random;
fn main() {
let mut x = Box::new(42);
let r = &x;
if random::<f32>() > 0.5 {
*x = 84;
} else {
println!("{}", r);
}
}
Enter fullscreen mode Exit fullscreen mode
xis of typeBox<i32>Declaring
ras a reference toxmeans the reference’s lifetime begins on that line (line 5)On line 7, the value of
xis modified through dereferencing. That requires a mutable reference tox. At this point, the borrow checker looks for a mutable reference toxand checks whether its use conflicts with anything else. In this example there is no conflict, so the code is validYou may ask: line 7 is inside the scope of
r. Since*xneeds a mutable reference tox, shouldn’t having both the immutable referencerand the mutable reference*xin the same scope violate the borrowing rules and produce an error?
In fact, Rust is smart enough to know that if theifbranch is taken, theelsebranch cannot be taken.ris never used in theifbranch at all, so using the mutable reference*xin theifbranch is fine. In other words, the lifetime ofrdoes not extend into theifbranch. This is an example of how lifetimes do not always exactly match scopes.
Let’s look at another example:
fn main() {
let mut x = Box::new(42);
let mut z = &x;
for i in 0..100 {
println!("{}", z);
x = Box::new(i);
z = &x;
}
println!("{}", z);
}
Enter fullscreen mode Exit fullscreen mode
-
xis of typeBox<i32> -
zis a reference tox, so the lifetime begins on this line (line 4) - On line 6,
zis printed inside the loop. Usingznaturally triggers a borrow-checker check. There is no problem here, so the borrow checker does not report an error - On line 7,
xis reassigned - On line 8,
zis reassigned. Rust treats the newly assigned reference as a different reference, so line 8 effectively starts a new lifetime, and the original lifetime ends at line 7 - Each subsequent loop iteration starts a new lifetime at
z = &x;. Therefore the borrow checker does not report an error
Features of the Borrow Checker
The borrow checker is conservative: if it is not sure whether a borrow is valid, it rejects that borrow.
Sometimes the borrow checker needs help understanding why a borrow is valid, which is one of the reasons Unsafe Rust exists.
1.11.3. Generic Lifetimes
Sometimes we need to store references inside our own types. Then we need to annotate those references with lifetimes so that the borrow checker can verify their validity. One example is returning a reference from a method where the returned reference lives longer than self.
Rust lets you make a type generic over one or more lifetimes.
Two Reminders
If a type implements the
Droptrait, then dropping the type counts as using the lifetimes or types that the type is generic over. If the type does not implementDrop, then dropping it does not count as using the lifetime, and the references inside the type can be ignored.
For example, when an instance of some type is about to be dropped, the borrow checker checks whether it is still legal to use the lifetimes that the type is generic over, because the code in yourdropfunction might use those references.A type can be generic over multiple lifetimes, but usually there is no need to make the type signature more complex. You should use multiple lifetime parameters only when the type contains multiple references, and the returned reference should be tied only to one of those lifetimes.
Look at this example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Enter fullscreen mode Exit fullscreen mode
'a denotes a lifetime called a. x, y, and the return type all share this lifetime a, which means that x, y, and the return value all have the same lifetime.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.