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
In a restaurant, when you make a reservation, you're getting an exclusive reservation to a specific dining table. You're not given ownership of the dining table, but you do have guaranteed access to it for a predetermined duration.
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.
Computer Crustacean
When memory is deallocated, it is marked as available and can be reallocated by different programs.
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.
Sammy Segfault
Lifetimes tell us how long a reference is valid.
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:
fn get_reference(x: &i32) -> &i32 {
x
}
And here is that same code with lifetimes included:
fn get_reference<'a>(x: &'a i32) -> &'a i32 {
x // ^ ^ ^
} // | | |
// Lifetime annotations
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.
Dragon Drop
When a reference has a static lifetime, the compiler guarantees that reference to be valid for the duration of the entire program execution.
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:
// Return a ref to the first string (x) if it's not empty, otherwise,
// return the second string (y)
fn choose_non_empty(x: &str, y: &str) -> &str {
if !x.is_empty() { x } else { y }
}
See the above snippet with explicit lifetime annotations
// Lifetime annotations:_____________________________
// | | | |
fn choose_non_empty<'a>(x: &'a str, y: &'a str) -> &'a str {
if !x.is_empty() { x } else { y }
}
Rusty
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.
Computer Crustacean
The 'a
syntax is how you annotate lifetimes. The a
can be
anything — 'a
, 'b
, 'stuff
, 'etc
, except for 'static
(which is for
static lifetimes only).
Sammy Segfault
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:
struct Reservation<'a> {
table_number: u32,
customer_name: &'a str,
}
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."
Dragon Drop
Can we please have a non-food reference? I'm getting hungry!
Okay, okay. For our second example, 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:
struct QueueItem<'a> {
item_id: u32,
file_reference: &'a str,
}
In this struct, 'a
tells Rust that the file_reference
must not outlive the
lifetime 'a
.
Dragon Drop
It's like saying, "As long as this item is in the queue for processing, the reference to the file must also be available."
That's right!
Rusty
We have to annotate lifetimes for references in struct objects to ensure that the referenced data will always be available as long as we need the struct object to be available.
Yes, because 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're writing a user session and configuration system.
Computer Crustacean
Okay: "We're writing a user session and configuration system."
Hey come on we're trying to learn!
Computer Crustacean
Sorry.
Anyway, let's say we are building a session and config system where a user's session contains their ID and reference to some configuration data:
struct Config<'a> {
data: &'a str,
}
struct Session<'a> {
user_id: u32,
config: &'a Config<'a>,
}
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:
impl<'a> Session<'a> {
fn new(user_id: u32, config: &'a Config<'a>) -> Self {
Self { user_id, config }
}
fn get_config_data(&self) -> &'a str {
self.config.data
}
fn get_user_id(&self) -> &u32 {
&self.user_id
}
}
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.
Rusty
And don't forget the most important rule about programming in Rust!
Sammy Segfault
What's that?
Rusty
Have fun!