Lifetimes are perhaps the hardest thing to understand when first approaching Rust. Today I’m going to create a lifetime error and demonstrate two strategies for fixing it. If you’re very new to rust, you may wish also to read the (beautifully titled) post Rust Lifetimes for the Unitialised first, as well as Strategies for Returning References in Rust.
To get started, we’re going to consider two Ruby implementations that are equivalent:
At the end of both Ruby snippets,
my_collection is full of values like “value number 1”. Not a terribly interesting program, but it will help us illustrate lifetimes in Rust. Below is an attempt to write the same program in Rust, but it won’t compile. Today we’re going to talk about why it won’t compile, and discuss two changes that would make it compile. Here’s the Rust program:
If we try to build the above program, we get the following issue:
Compiling playground v0.0.1 (file:///playground) error[E0597]: `value` does not live long enough --> src/main.rs:19:31 | 19 | my_collection.insert(&value); | ^^^^^ borrowed value does not live long enough 20 | }); | - `value` dropped here while still borrowed 21 | } | - borrowed value needs to live until here error: aborting due to previous error error: Could not compile `playground`. To learn more, run the command again with --verbose.
This seems like a very simple program, and it’s at first discouraging to see errors like this when learning Rust. But let’s dive into this error.
Rust tells us that the “borrowed value does not live long enough.” What this means is that
my_collection, which is owned by the
main() function, cannot have a reference to
value which is owned by the anonymous function passed to
for_each() on line 17, because
value goes out of scope on line 20, and
my_collection has to be valid to the end of
In Rust terms, “lifetime a” begins at line 16, when
my_collection is created.
my_collection stores a collection of borrowed strings of lifetime a. That’s what
Vec<&'a str> means in line 2: “A vector of borrowed strings of lifetime a”. The value created by
&value in line 19 has a different lifetime: it lives from line 19 to line 20. Call it “lifetime b”. Because lifetime b ends before lifetime a, we can’t use a value with lifetime b where the compiler expects a value of lifetime a. We can almost think of this as type checking: you can’t put an
&'b str in a vector of
&'a str unless b outlives a.
But if the strings don’t live long enough in Rust, how do the Ruby programs work?
The reason the Ruby program in example 1 above is able to work is that Ruby has a runtime garbage collector, which will notice that
value needs to live as long as
my_collection has a reference to it. Rather than having lifetimes, Ruby has a lifeguard that keeps track of how long different values need to be around and keeps them alive that long. This language feature prevents programmers from having to worry about lifetimes, but it also has a significant runtime cost. The same thing is true in most garbage-collected programming languages.
To avoid this cost, Rust doesn’t have a garbage collector. At runtime,
&value is little more than a pointer, and nothing is telling it to live as long as
my_collection, so it won’t. Fortunately, the Rust compiler is smart enough to detect that the reference doesn’t live long enough and complain at compile time, rather than letting you dereference a no-longer-valid pointer at runtime.
Now that we have a basic notion of Rust lifetimes, we can understand two basic ways of solving the borrow checker error that we had above.
The first solution to this problem is to put the strings somewhere that does live long enough:
What this does is give your borrowed elements a home. Now, when you make a new string, you give ownership of it to the vector
values. Now objects who have references (who “borrow”) these values may do so as long as they go out of scope after the vector does.
There are two small gotchas with this approach.
The first is that the vector must own the strings. That is, it must have the type
Vec<&str>. (If you try to add values to a
Vec<&str>, you’ll encounter the same problem we were originally trying to solve:
vector will be asking to borrow something that goes out of scope before it does.)
The second gotcha I only caught because of an amazing compiler error message (I mean it – the rust compiler is super helpful):
note: values in a scope are dropped in the opposite order they are created
What that means is that, in this example, you have to declare
my_collection; you’ll see the above error message if you switch lines 16 and 17 in the gist above.
That covers the first solution to the lifetime issue. In short, you have to put the values somewhere that they do live long enough.
The second solution is for the collections to take ownership of the values. Note that you can employ this solution only if you are able to change the type signature of the method that’s complaining about the lifetime.
The solution here is not trying to use borrows at all. Because
my_collection is a collection of strings, and not of borrowed strings, they will live as long as it does.
The lesson here is that we can’t arbitrarily return values from a narrower scope. In garbage collected languages, it’s perfectly normal to build up a value in a local variable and then return it to the calling scope. In Rust, we can do this to, but we have to return ownership of the local variable. If we try to return a reference (in Rust terminology, a “borrow”) of a variable from a smaller scope into a larger scope, Rust will remind us that we’re breaking the rules about lifetimes.
Till next time, happy learning!