Lifetimes in Rust by example
When you make a reservation at a restaurant, the restaurant provides you access to a table in the future in the form of a reservation.
You can do two things with that reservation:
- You can use the reservation, which will give you access to the dining table associated with the reservation
- You could not use the reservation, and the table will be free to be used by someone else
When you make a reservation at a restaurant, you are getting exclusive access to a table at the restaurant while you’re eating your meal. You’re not given ownership of the dining table, but you do have guaranteed access to it for some length of time.
In Rust, when you make a reference (“borrow a reference”) to some data, you’re getting access to that specific data. You’re not given ownership of the data, but you do have guaranteed access to it for a predetermined duration.
It’s that predetermined duration in which access to referenced data is guaranteed that we’re talking about here today. That is a reference’s lifetime.
What is a lifetime?
A reference’s lifetime represents how long that reference is valid.
Why do we need to care about lifetimes?
One of the things Rust is designed to do is actively avoid dangling references. A dangling reference occurs when a variable that points to a memory location continues to do so after that memory location has been deallocated. This happens when the memory is freed before the references to it are destroyed or reassigned, leading to a scenario where the reference points to a memory location that may no longer hold valid data.
A dangling reference could return null, or it could return some part of some other data from some other program that was stored there. When this happens, you experience an interesting issue known as a segmentation fault.
To avoid dangling references (and segmentation faults) and the issues that come with them, Rust needs to know that your references are valid for the entire time they are in scope. It’s like how the restaurant needs to ensure that no two parties have the same table reserved at the exact same time.
How do we use lifetimes?
Every reference is associated with a lifetime, so if you use are using references, you are using lifetimes.
In many cases, the Rust compiler can infer a reference’s lifetime based on rules that it follows called lifetime elision rules. These rules help the compiler understand your code in a way that allows you to elide the lifetime annotation in the code you’re compiling.
Here is a code snippet with lifetimes elided:
And here is that same code with lifetimes included:
As you can see, lifetimes in Rust are annotated with a tick mark ('
) followed
by an identifier, like 'a
or 'y'
. The identifier is just a label; you can
make it 'anything
if it 'helpsyou
except for 'static
which is a special
lifetime annotation used to denote a static lifetime.
In both of the get_reference
examples above, we have a function that takes a
reference to an i32
and returns a reference to an i32
with the same lifetime
as the one passed into the function. Essentially we’re saying if we’re going to
return a reference to some data, that reference must be available for at least
as long as it takes to return that reference from this function.
The explicit annotations in the second example make clear that the returned
reference has the same lifetime as the input reference, 'a
. The compiler can
generally infer the lifetime of references—especially in trivial examples like
this.
In other cases, the Rust compiler takes no chances and requires you to clearly articulate how long references are to alive for.
Let’s look at another example of lifetime annotations related to a function.
Below we have a function that takes two references but returns only one of those references. For this to happen correctly, Rust needs to know the lifetime of the returned reference to ensure it’s not outliving the data it refers to. Remember: the lifetime of the returned reference is kind of like the lifetime of a restaurant reservation.
This function is trivial enough that the Rust compiler can infer the lifetimes, but try to guess where lifetimes have been elided:
See the above snippet with explicit lifetime annotations
Rust can usually infer lifetimes based on your code. When this happens, you don’t have to explicitly declare lifetimes. This is called lifetime elision—you are eliding the lifetimes. Explicit lifetime annotations are required when Rust can’t figure things out on its own.
Lifetimes help Rust understand how different references relate to each other in terms of their scope.
Let’s explore a bit further into how you might use lifetimes in more complex situations.
struct lifetime annotations
Whenever you store references in a struct, you must annotate those references with lifetimes. This requirement prevents dangling references and ensures memory safety by verifying that the referenced data will not be cleaned up as long as the struct exists.
Let’s look at two examples.
First, imagine we have a restaurant reservation system where each reservation keeps a reference to the customer making it. The system needs to ensure that information about the customer isn’t removed or lost as long as the reservation exists.
Here’s how we might define a struct for this scenario:
In this struct, 'a
tells Rust that the customer_name
reference must not
outlive the lifetime 'a
. It’s like saying, “As long as this reservation is
active in the restaurant, the information about the customer must also be
available.”
Let’s take a break from the food references (I haven’t eaten yet), and instead, let’s imagine we have a file processing automation where each item in a queue keeps a reference to a particular file that needs to be processed. The system needs to ensure that information about the file isn’t removed or lost as long as the item exists in the queue.
Here’s how you might define a struct with lifetimes in Rust for this scenario:
In this struct, 'a
tells Rust that the file_reference
must not outlive the
lifetime 'a
. It’s like saying, “As long as this item is in the queue
for processing, the reference to the file must also be available.”
Lifetimes in Rust are fundamentally about ensuring data integrity and memory safety. By annotating lifetimes, you tell the Rust compiler how your references relate to each other, and in doing so, enable the compiler to guarantee the safety of your program.
A (slightly) more complex example
Now let’s look at something a bit more interesting. Let’s say we are building a session and config system where a user’s session contains their ID and a reference to some configuration data:
Both the Config
and Session
structs are annotated with a lifetime parameter
'a
, which tells us (and the Rust compiler) that the references they hold
(data
in Config
, and config
in Session
) must not outlive the struct
instances themselves. In other words, these annotations ensure that the
configuration data a session references cannot be deallocated while the session
is still active.
Now let’s add some methods to the Session
struct:
Here, the new
function for Session
takes a reference to a Config
instance
with the same lifetime as the Session
it returns. This setup ensures that the
session can’t outlive the configuration it depends on.
Similarly, the get_config_data
method returns a reference to the configuration
data with a lifetime tied to the Config
instance, ensuring the data remains
valid as long as the session needs it.
Parting thoughts
We went through many examples of lifetimes here. If you’re feeling overwhelmed by these things, remember that you’re not alone. Rust’s inherent complexity is part and parcel of it’s steep learning curve, but stick with it and you’ll come out the other side a more competent and more capable systems-level programmer.
While you can do a lot in Rust by ignoring lifetimes, as the complexity of your projects grows, so too will your need to master lifetimes. Whenever you make a struct that holds references to data not owned by that struct, for example, you’ll need to use lifetimes to ensure the data referenced does not go out of scope as long as the struct instance needs it. Additionally, struct methods—especially those returning references to data owned by the struct—need to take into account the lifetimes of the struct’s data.
The thing I want you to remember is this: lifetimes are a way to ensure references always remain valid. Lifetimes are part of the Rust compiler’s zero-cost abstractions, which, while very powerful, do come at the cost of a steep learning curve—but the more you practice, the less daunting it gets.