kota's memex

Lifetimes allow us to ensure that references are valid as long as we need them to be.

Every reference in rust has a lifetime, which is the scope for which that reference is valid. Most of the time, lifetimes are implicit and inferred, just like how must of the time types are inferred.

When the lifetimes of references could be related in different ways we must annotate the lifetimes to inform the compiler of what we actually want. The primary goal of lifetimes is the prevention of dangling references, which cause a program to reference data other than the data it's intended to reference.

missing lifetime specifier

This function will not compile. Rust cannot tell at compile time whether the reference in the return type refers to x or y because the if statement could return either.

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

We also don't know the concrete lifetimes of the references that will be passed in, so we can't look at the scopes to determine whether the reference we return will always be valid. The borrow checker can't determine this either, because it doesn't know how the lifetimes of x and y relate to the lifetime of the return value.

To fix this error we'll add generic lifetime parameters that define the relationship between the references so the borrow checker can perform its analysis.

annotations

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

Just as functions can accept any type when the signature specifies a generic type parameter, functions can accept references with any lifetime by specifying a generic lifetime parameter.

The names of lifetime parameters must start with an apostrophe ' and are usually all lowercase and short. Most people use 'a for the first lifetime parameter.

To fix the earlier function we need to express the following constraint: the returned reference will be valid as long as both the parameters are valid. This is the relationship between the lifetimes of the parameters and the return value.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

This function signature now 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 signature also tells rust that the string slice returned from the function will live at least as long as lifetime 'a. In practice, it means that he lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the values referred to by the function arguments. These relationships are what we want rust to use when analyzing this code.

Note that the longest function doesn't need to know exactly how long x and y will live, only that some scope can be substituted for 'a that will satisfy this signature.

In this example we try to use result after string2 has gone out of scope, but since our function declares that the lifetime of both arguments must be around at least as long as the result.

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 the print was instead inside the inner block there would be no issues:

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}");
    }
}

dangling references

Lifetime annotations do not change the lifetime our types, they just annotate it. This code fails to compile:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Despite trying to provide a lifetime annotation this will still fail to compile because result goes out of scope when our function ends and returning a pointer to it would create a dangling pointer.

We could simply return the owned type rather than using references and lifetimes:

fn longest(x: &str, y: &str) -> String {
    if x.len() > y.len() {
        String::from(x)
    } else {
        String::from(y)
    }
}

static lifetimes

The 'static lifetime is a special lifetime annotation which denotes that the affected reference can live for the entire duration of the program. All string literals have the 'static lifetime, which we can annotate as follows:

let s: &'static str = "I have a static lifetime.";

You might see suggestions to use the 'static lifetime in error messages. But before specifying 'static as the lifetime for a reference, think about whether the reference you have actually lives the entire lifetime of your program or not, and whether you want it to. Most of the time, an error message suggesting the 'static lifetime results from attempting to create a dangling reference or a mismatch of the available lifetimes. In such cases, the solution is to fix those problems, not to specify the 'static lifetime.