In case it helps develop your mental model, I'll add my own explanation and then highlight some differences from what you may have read elsewhere.
I'll approach this by explaining the code with my mental model as soon as possible, and then try to explain the mental model in more detail afterwards.
(I tried to make this shorter but failed, and at least this way the perhaps the benefits will be apparent up front, and you can stop reading if your eyes glaze over.)
Here are the core ideas needed for the example:
- Creating a reference to a variable makes it be borrowed
- Using a reference keeps some set of borrows active
- Going out of scope and other uses of variables conflict with being borrowed
And we'll also need to know the meaning of this signature:
fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
The meaning is: "Uses of the return value keep both *str1 and *str2 borrowed."
If we accept this as the starting point, we can gloss over thinking about Rust lifetimes for the moment. (We'll discuss how to get the meaning via Rust lifetimes later.)
Let's look at the first example (which does not compile):
{
let longest; // Say the type of this is `&'0 _`.
let string1 = String::new();
let str1 = &string1; // And this is `&'1 _`.
{
let string2 = String::new();
let str2 = &string2; // And this is `&'2 _`.
longest = longer(str1, str2); // Line 7
} // Line 8
println!("{str1}"); // Line 9
println!("{longest}"); // Line 10
} // Line 11
The expression &string1 created a borrow of string1, and we want to figure out where that borrow remains active, and similarly for string2. Then we'll see if there are any uses of string1 and string2 that conflict with being borrowed.
&string1 was assigned to str1, so uses of str1 keep string1 borrowed, and similarly for str2/string2. So for example, string1 must still be borrowed through at least line 9, as there is a use of str1 on line 9.
Key to the example is knowing what longest keeps borrowed. In terms of the example, applying the meaning we supplied above means that uses of longest keep both string1 and string2 borrowed.
That means that string1 and string2 stay borrowed through line 10. In particular, string2 is still borrowed on line 8. But on line 8, string2 goes out of scope. Going out of scope conflicts with being borrowed, and this is the source of the borrow check error.
For the second example, we move the use of longest to some place before string2 goes out of scope.
{
let longest; // Say the type of this is `&'0 _`.
let string1 = String::new();
let str1 = &string1; // And this is `&'1 _`.
{
let string2 = String::new();
let str2 = &string2; // And this is `&'2 _`.
longest = longer(str1, str2); // Line 7
println!("{longest}"); // Line 8
} // Line 9
println!("{str1}"); // Line 10
} // Line 11
The only thing that keeps string2 borrowed are the explicit uses of str2 on line 7 and of longest on line 8. So string2 need not remain borrowed on line 9, and the conflict from going out of scope goes away.
Explicit uses of str1 and longest keep string1 borrowed through line 10. But string1 does not go out of scope on line 9, and it need not be borrowed on line 11 where it does go out of scope, so there's no conflict there either.
longest does go out of scope on line 11 (and after string1). But references going out of scope does not keep their referents borrowed -- the compiler understands that there is no way for the reference to "observe" its referent when it goes out of scope. It is almost entirely a no-op.
You can think of longest becoming uninitialized when it goes out of scope. If longest itself was borrowed -- if you had a &longest you were trying to keep around -- this may cause a borrow check error. (That's why I said "almost entirely a no-op".)
We had no such nested references in our examples. If there aren't nested references around, most examples that try to illustrate borrow checking by changing where references go out of scope are misleading. You can move the declaration of str2 to the top of main and it won't change the results of either example in any practical way, for instance.
Now let's try to add detail and incorporate Rust lifetimes more explicitly.
The core idea is that the borrow checker
- Figures out where in the control flow variables are borrowed, and how
- Then checks every use of every place to see if it conflicts with any borrows
Just like the other replies, we need to distinguish between the lexical or liveness scope of variables -- it's "lifetime" -- and Rust lifetimes (those 'a things). Rust lifetimes approximate the duration of a borrow -- where in the control flow some variable is borrowed.
The main connection between the two concepts is that it is a borrow conflict for a variable to be moved, destructed, or to go out of scope when it is borrowed. That is, going out of scope is a use checked against any potential borrows. If all borrows of a variable end before it goes out of scope, there is no conflict.
Roughly speaking, uses of a Rust lifetime -- uses of values whose type contains a '_ lifetime -- keep borrows alive. In practice this means that uses of a reference keep the referent borrowed, as in the examples.
There is some nuance around when a reference goes out of scope: That is a use of the reference (it conflicts with having a borrow of the reference itself -- like a &&something). But it is not a use of the lifetime -- it does not keep the referent borrowed.
The lifetime in the type of a reference also does not have to correspond to the liveness scope of the reference. Otherwise we couldn't have local variables of type &'static str, for example! And longest wouldn't compile either -- both references hold borrows longer than the function body, but we don't return both references.
Function signatures and other lifetime annotations ('b: 'a) convey how uses of lifetimes keep each other alive -- or in practical terms, which borrows the use of a value keeps alive.
As we said before, this signature:
fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
Means "uses of the return value keep both *str1 and *str2 borrowed" in practical terms.
Note that when *str1 and *str2 drop doesn't matter here -- we're just conveying when they remain borrowed. Analysis at the call site will determine if there's a conflict with going out of scope or not.
In the examples, it also doesn't mean "'0, '1, and '2 must all be the same". Why not?
When you pass in your arguments, you're passing in copies of the values. And the types of those copies can have different lifetimes than the original thanks to subtyping, or variance, or however you want to call it. In practical terms, we say the outer lifetime of references can shrink. Or more technically:
&'a str can coerce to &'b str if 'a: 'b ("'a outlives 'b")
'a: 'b also means uses of 'b keep 'a active
In the example we can think of the call and assignment to longest as meaning
- We're calling
longest::<'0>
- Which must mean
'1: '0 and '2: '0 (so our arguments can coerce)
- Which means uses of
'0 keep '1 and '2 active
- Which means uses of
longest keep string1 and string2 borrowed
Considering this variation of the function adds some indirection, but we end up with the same conclusion.
fn longer<'a, 'b>(str1: &'a str, str2: &'b str) -> &'a str
where 'b: 'a
{
str2
}
- We're calling
longest<'0, 'b> where 'b: '0
- Which must mean
'1: '0 and '2: 'b: '0
- Which means uses of
'0 keep '1 and 'b and '2 active
- Which means uses of
longest keep string1 and string2 borrowed
Technically this variation is more general because you can pass in types with different lifetimes. But in practical uses it won't make a difference -- without forcing things by using annotations, there will be nothing in the borrow checking analysis requiring 'b to be any longer than 'a.
(And generally speaking, callers don't want to borrow longer than necessary in order to call such a function.)
I find it unfortunate that the book describes calling longest thusly:
The 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 the 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.
This implies that the compiler has assigned all the input lifetimes in the calling function ahead of time somehow, and then picks one of those at the call site based on the inputs to fn longest, and therefore the lifetime in longest is determined by the lifetimes in the arguments to fn longest.
But in my mental model, things work exactly the other way around. The uses of the return value (longest) determine where its lifetime must be active (what the lifetime is), and that partially determines what the lifetimes in the inputs to fn longest must be (a superset of the lifetime in longest). Which is why I keep saying:
// Uses of the return value keep both `*str1` and `*str2` borrowed
fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
It is true that 'a has to end before either referent goes out of scope, or you're get a borrow checker error. But the borrow checker does not look at where the referents go out of scope in an attempt to choose some lifetime. It determines the lifetime, and then sees if going out of scope has a conflict.
I suspect that the book's explanation has roots in pre-NLL Rust (i.e. over 8 years ago), when there was a tight connection between how long references borrowed their referents and lexical scopes. (There are sadly many parts of the Nomicon which have also not been updated since those days.)