Featured image of post Understanding Rust Lifetimes

Understanding Rust Lifetimes

A beginner-friendly introduction to lifetimes in Rust.

Understanding Rust Lifetimes

Lifetimes are one of the most powerful and, at times, most confusing features of Rust. They are a key part of Rust’s ownership system, which allows Rust to guarantee memory safety without a garbage collector. In this post, we’ll demystify lifetimes and show you how they work with some practical examples.

What are Lifetimes?

In Rust, every reference has a lifetime, which is the scope for which that reference is valid. Most of the time, lifetimes are implicit and inferred by the compiler. You don’t need to write them out. However, when you have functions that take references as input and return references as output, the compiler might not be able to figure out the lifetimes on its own. This is when you need to annotate them explicitly.

The main purpose of lifetimes is to prevent dangling references. A dangling reference is a reference that points to memory that has been deallocated. This can happen if you create a reference to something, and then the original data goes out of scope before you’re done with the reference.

Lifetime Annotations

Lifetime annotations don’t change how long any of the references live. Rather, they describe the relationships of the lifetimes of multiple references to each other.

Lifetime annotations have a slightly unusual syntax: they start with an apostrophe ' and are usually lowercase and very short, like 'a.

Let’s look at a simple example.

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Here, we have a function longest that takes two string slices and returns the longest one. The <'a> part is where we declare the lifetime parameter. Then we use 'a to annotate the references in the function signature.

This signature tells Rust that for some lifetime 'a, the function takes two parameters, both of which are string slices that live at least as long as lifetime 'a. The function will return a string slice that also lives at least as long as lifetime 'a.

This is important because it tells the compiler that the returned reference will be valid as long as both of the input references are valid.

In Practice

Let’s see how this works with some code.

1
2
3
4
5
6
7
8
9
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

If you try to compile this code, you’ll get an error! The compiler will complain that string2 does not live long enough. This is because string2 is created inside the inner scope, and it’s dropped at the end of that scope. The result reference, however, is still alive after that scope. The lifetime annotations on longest allowed the compiler to catch this bug for us.

If we fix the code like this, it will compile:

1
2
3
4
5
6
fn main() {
    let string1 = String::from("long string is long");
    let string2 = String::from("xyz");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
}

Now, both string1 and string2 live long enough for the result reference to be valid.

Conclusion

Lifetimes are a core concept in Rust that enable its memory safety guarantees. While they can seem intimidating at first, they are a powerful tool for writing safe and correct code. The key is to remember that you are not changing how long things live, but rather describing the relationships between the lifetimes of references to the compiler.

Built with Hugo
Theme Stack designed by Jimmy