Welcome to module 3, Visualizing lifetimes to understand borrow checker errors. In the previous module,
we looked at lifetimes of values and references in four examples that compiled because all of the lifetimes were valid.
In this module, we're going to look at five examples that don't compile because they would create invalid references.
The examples are returning a reference to a value created within an inner scope, returning a reference to a value created within a function, referencing a value that is moved,
creating a struct that refers to itself, and storing references in a HashMap.
For each example, we'll draw the lifetimes to understand why the references would be invalid, and we'll talk about how to fix the problems.
Let's start with returning a reference to a value that's created within an inner scope.
This first example is similar to examples 1 and 2 from the previous module.
We create a vector and bind it to the variable list,
and then we create a reference to the first two items in the list.
The difference is that rather than creating the list in the outer scope of main,
we're creating the list in an inner scope and attempting to return the reference out of that scope to the first_two variable.
The lifetime of the list value starts where list is created and ends at the end of the inner scope
where list gets cleaned up. As written,
we're trying to use the reference to list,
starting from the creation of the first_two variable through the end of main.
The lifetime of the reference is longer than the lifetime of the value it's referencing,
which isn't allowed.
Let's look at the compiler error we get.
It says `list` does not live long enough,
then it points to the reference to the first two items of the list that we try to return from the inner scope, and says that the borrowed value does not live long enough.
At the end of the inner scope,
it says list has dropped
while still borrowed,
and it says the borrowed value needs to live until the end of main.
The compiler is drawing the same conclusions that we drew with the lines, just explaining it in a slightly different way.
Fixing this problem depends on what we're ultimately trying to do.
We could move all uses of the reference into the inner scope rather than returning the reference from the inner scope,
which would make the reference's lifetime shorter than the values.
Or, we can move the list creation out of the inner scope,
which makes the value's lifetime longer than the references.
Next,
let's see what happens when we try to return a reference out of a function.
This example is similar to one we looked at in the Borrowing module in unit
2.
But this time, we're going to look at it in the context of lifetimes. Rather than returning a reference from an inner scope,
as we did in the first example,
we're instead returning a reference out of a function that points to a value created in that function.
The list value's lifetime starts in the return_first_two function where it's created and ends at the end of the function where it goes out of scope.
This code is trying to create a reference to list,
return it from the function, and use it in main,
from where the first_two variable is created, until the end of main.
That would make the reference's lifetime longer than the value it's referring to,
which, again, would be invalid.
Looking at the compiler error for this code,
it says there's a missing lifetime
specifier and points to a spot where it expected a lifetime parameter.
The help text explains that there isn't a value for the borrowed parameter to be borrowed from because there isn't a value passed in.
The compiler can tell we must be trying to borrow a value we create in the function,
which isn't valid.
It then goes on to suggest giving the return type a 'static lifetime.
We'll be talking about lifetime
specifiers, lifetime parameters, and the 'static lifetime in future videos,
along with whether that suggestion would fix this problem or not.
A fix not suggested by the compiler,
is to move the owned list out of the function,
so the return type is a vector of i32 rather than a slice.
Then, we would create the reference in main instead,
which again makes the lifetime of the reference shorter than that of the value.
For the next example,
let's try creating a reference to a value that gets moved.
In this code, we'll create a vector and bind it to the variable
list_a.
Then, we'll transfer ownership to the variable list_b.
After moving the vector out of list_a, we'll attempt to take a reference to the last two elements in list_a and
print them out.
The lifetime of list_a ends when we transfer ownership to list_b because the value is moved.
Its lifetime in that memory location is over.
The lifetime of the reference in first_two is entirely disjointed from the lifetime of list_a.
So, this example isn't valid because a reference's
lifetime must be completely contained within the lifetime of the value being referenced.
The compiler error tells us we can't use a value after we've moved it.
If we swap the lines that move the vector and take a reference so that we try to hold on to and use a reference after we've moved its value,
this would also be invalid.
The lifetime of the reference overlaps that of the value now,
but it still isn't completely contained within the value's lifetime.
The compiler now says list_a can't be moved because it's borrowed.
A fix that works in Rust 1.24.1 is to move the vector into list_b
last, and create and use the reference in an inner scope that ends before we transfer ownership. In Rust 1.31 and up, when using the Rust 2018 edition,
the borrow checker has learned to see when you're done with references, so the inner scope is no longer needed.
Next, let's take a look at why the compiler won't allow a struct to hold a reference that points to a part of itself.
In the second example,
when we tried to return a reference from a function,
a solution you may have been considering is returning both the vector and the reference together in one type.
Let's try that now. We'll define a struct named ListAndRef that stores both a vector and a slice that references the vector's
first_two items. In the return_list_and_first_two function, we'll create a vector and bind it to the variable list_to_use.
Then, we'll create an instance of the ListAndRef struct with the list_to_use vector in the list field of the struct in a slice
referencing the first two elements of list_to_use in the first_two field.
Then, we'll return the instance.
There are multiple reasons that this is a problem.
First of all,
the struct owns the data in the list field.
So, when we assign list_to_use, ownership is moved to the struct. We're then not allowed to create a slice referencing list_to_use because that would be a borrow after a move.
Even if we could create the slice reference,
we'd have another problem.
Ownership is moved when a value is returned from a function.
The fields in memory, before the end of the function, would hold the list and the reference to the list. Moving a value copies values on the stack.
When the struct instance is moved to a different location,
the reference would still point to the old location of the list field.
That location would now be invalid.
If Rust automatically fixed up references like this on a move,
moving would no longer be the quick operation it is today.
Just imagine a struct with lots of self references. If you have a situation where you need to have a struct hold a reference to itself,
check out the rental or owning-ref crates,
which use unsafe code but expose safe wrappers to manage self-referential structs.
Finally,
let's look at some problems that can come up with HashMaps and references.
This program will count the number of times words are used in text provided by user input.
We create a HashMap to store the words and the number of times we've seen them.
Then, we create a String to hold
the user input,
and we read a line of input.
Next,
we split the user input on whitespace and count the words.
If the word isn't in the HashMap yet,
we insert it with a 0 value,
then add 1 to the value.
Finally,
we print out the HashMap.
The lifetime of the counts HashMap starts where we create a new HashMap and ends at the end of main, annotated here in blue.
The lifetime of the input string starts where we create the new String and ends at the end of main just before counts is dropped,
annotated here in orange.
The split_whitespace method returns slices that reference the string owned by the input variable.
When we insert the word slices into the HashMap,
those references then need to live as long as the HashMap does,
which would mean that they would outlive the input string,
so this isn't allowed.
This is really subtle, and actually works with Rust 2018 because the Rust borrow checker learned some new tricks.
So, let's make it more obvious and make it not work in any Rust version by adding an inner scope.
Now it's easier to see that input gets cleaned up at the end of the inner scope,
but we need it to be valid to print out the counts in the inner scope.
Rust 1.24 explains this
as 'input' does not live long enough.
In this particular example,
we can fix the problem by swapping the order in which we declare counts and input.
If we declare input first,
it is dropped after counts is. Then, the lifetimes of the references don't need to last longer than input lives
and this code is valid.
However,
what if we want to accept multiple lines of input in a loop and keep a running count of word frequencies?
In this case,
the input string goes out of scope at the end of each loop iteration,
but we would need the word slices to be valid until the end of main,
where counts is valid.
We can't fix this program in the same way we did the variation without the loop by moving the input declaration above the declaration of counts.
Instead,
the fix is to copy each word in order to store owned strings rather than string slices in the HashMap.
This gets rid of all of the reference problems.
In the future, when you get error
messages like the ones we saw in this module,
such as borrowed value
does not live long enough,
remember that it means Rust
can't prove that all of your references will always be valid.
Try looking at where the lifetimes of the values and the references are
to
understand what code needs to be rearranged. In this unit,
we looked at the concrete lifetimes in five examples with references that would be invalid.
We saw that we can't return a reference out of the scope
its value is created in or out of the function its value was created in.
We saw that the lifetime of a value ends when the value is moved, so references can't outlive a move either.
We discussed how self-referential structs aren't something safe
Rust allows because the references would become invalid.
And we covered problems that might arise when storing references in a HashMap,
when the HashMap outlives the values referenced.
The experience you've gained with concrete lifetimes will be useful to understand generic lifetimes, the next module's topic.