Skip to main content
đź’» Null Notes

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:

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!