rust-lifetimes-anotaciones-tiempo-vida

Lifetimes in Rust: Understanding and Annotating Lifetimes

  • 5 min

A lifetime is the interval during which a reference is valid; its annotations express relationships between references when the compiler cannot elide them.

So far, the Borrow Checker has been our guardian angel. It ensured that no reference outlives the data it points to.

Let’s review the classic case of a Dangling Reference, which Rust prevents:

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}
Copied!

The compiler looks at this code and says: “Error: x has a shorter lifetime than r. If I allow this, r will point to freed memory when the inner scope closes.”

In this case, the scopes are visually obvious. But what happens when we pass references to functions?

The Ambiguity Problem

Imagine a function that compares two strings and returns the longer one.

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

If you try to compile this, Rust will give you an error: missing lifetime specifier.

Why?

Let’s analyze it from the compiler’s perspective:

  1. The function receives two references: x and y.
  2. It returns a reference.
  3. The signature does not specify whether the returned reference is tied to x, y, or both.
  4. Rust needs that relationship in the function’s contract to check each call without relying on its internal implementation.

For these cases, we use Lifetime Annotations.

The 'a Syntax

A Lifetime is a type of generic. Just as <T> generalizes over types, <'a> generalizes over lifetimes. The syntax uses an apostrophe followed by a name (usually a, b, c…).

Let’s fix the function above:

// Reading: "The function longer lives for a lifetime 'a.
// It receives two parameters that live AT LEAST as long as 'a.
// It returns a reference that will live AT LEAST as long as 'a."
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
Copied!

What Does This Really Mean?

This is the key to understanding it: Annotations do NOT change the actual lifetime of variables. They don’t make them live longer or shorter.

They simply tell the compiler how they relate to each other. In the example above, we are telling Rust:

“The returned reference will be valid as long as both input references remain valid.”

In practice, 'a will be equal to the shortest lifetime of x and y.

fn main() {
    let string1 = String::from("long");
    let result;

    {
        let string2 = String::from("xyz");
        // Here, 'a is the scope of string2 (the shortest)
        result = longer(string1.as_str(), string2.as_str());
    }

    // ❌ Error: `result` is no longer valid because string2 died,
    // and the contract stated the result lives as long as the shorter of the inputs.
    // println!("The longer one is {}", result);
}
Copied!

Thanks to the 'a annotation, Rust detects the error correctly.

Structs with References

So far, our Structs have always owned their data (String, Vec<i32>). But what if we want a Struct to hold a reference to data owned by someone else?

Rust needs to know that the Struct will not outlive the data it points to.

// This struct does NOT own the text. It only stores a reference (slice).
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("In a place in La Mancha...");
    let first_sentence = novel.split('.').next().expect("No periods found");

    // Create the struct
    let i = Excerpt {
        part: first_sentence,
    };

    // This works because 'novel' outlives 'i'
}
Copied!

If we didn’t put <'a>, Rust would not let us compile the struct. It would be protecting us from creating a struct that, when used later, would point to freed memory.

Lifetime Elision

I bet you’re thinking: “Wait, we’ve written functions that returned &str before and we didn’t put 'a. Why now?”

Rust has some baked-in rules called Elision Rules. The compiler tries to infer lifetimes in common cases to save you from writing them.

The 3 rules the compiler applies automatically:

  1. Each input reference gets its own lifetime (fn foo(x: &'a i32, y: &'b i32)).
  2. If there is only one input parameter, that lifetime is assigned to all outputs (fn foo(x: &'a i32) -> &'a i32).
  3. If it’s a method (&self), the lifetime of self is assigned to all outputs.

Only when these rules do not resolve the ambiguity (as in longer, where there are two inputs and one output) do you have to write it by hand.

The Static Lifetime: 'static

There is a special reserved lifetime: 'static. It means the reference can live for the entire duration of the program.

All string literals are 'static because they are stored directly in the program’s binary.

let s: &'static str = "I have an infinite lifetime";
Copied!

Sometimes you’ll see 'static as a constraint on generics (Trait Bounds), indicating that the type cannot contain temporary references.