Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why Lifetimes Exist

References must always point to valid data. In C#, the garbage collector ensures this. In Rust, the compiler proves it at compile time using lifetimes.


The Problem: Dangling References

What happens if a reference outlives the data it points to?

fn main() {
    let reference;
    
    {
        let value = 42;
        reference = &value;
    }  // value is dropped here
    
    println!("{}", reference);  // ERROR: `value` does not live long enough
}

The compiler catches this: reference would point to freed memory. This is a dangling reference, and Rust prevents it.


References in Functions

When functions return references, where does the data live?

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    
    println!("First word: {}", word);
}

This works - but how does the compiler know word is valid? It needs to know that the returned reference lives as long as the input.


The Compiler's Question

When the compiler sees a function that returns a reference, it asks: "How long is this reference valid?"

// This function returns a reference, but to what?
// The compiler will emit an error here because it doesn't know where the &str comes from
fn get_name() -> &str {
    // Doesn't matter what we do here ...
}

fn main() {
    println!("{}", get_name());
}

The compiler needs to prove the returned reference won't dangle. If it can't, it rejects the code.


Lifetime Annotations: Describing Relationships

Lifetime annotations tell the compiler how references relate to each other:

// 'a is a lifetime parameter
// This says: the returned reference lives as long as the input
fn first_word<'a>(s: &'a str) -> &'a str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("First word: {}", word);
}

The 'a connects the input and output: "the output reference is valid for the same scope as the input reference."


Lifetimes are Descriptive, Not Prescriptive

Important: lifetimes don't change how long data lives. They describe relationships that already exist.

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

fn main() {
    let string1 = String::from("hello");
    let string2 = String::from("world!");
    
    // The compiler knows: result is valid as long as BOTH inputs are valid
    let result = longer(&string1, &string2);
    println!("Longer: {}", result);
}

You're telling the compiler: "I'm returning one of these two references, so the result is valid only while both are valid."


Lifetime Elision: When You Don't Write Them

You've been using lifetimes all along - the compiler infers them in common cases:

// You write:
fn first_word(s: &str) -> &str { /* ... */ }

// Compiler sees:
// fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }

The elision rules let you skip explicit lifetimes when the relationship is obvious:

  1. Each input reference gets its own lifetime
  2. If there's exactly one input lifetime, it's used for all outputs
  3. If there's &self or &mut self, that lifetime is used for outputs

When Elision Isn't Enough

Sometimes the compiler can't infer the relationship:

// Which input does the output come from?
fn pick_one(x: &str, y: &str) -> &str {
    if x.len() > 0 { x } else { y }
}
// ERROR: missing lifetime specifier

fn main() {}

We must be explicit:

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

fn main() {
    let a = String::from("hello");
    let b = String::from("world");
    println!("{}", pick_one(&a, &b));
}

Two inputs, one output - the compiler needs you to specify the relationship.


The Mental Model

C# thinking: "References just work. The GC tracks what's alive."

Rust thinking: "References have lifetimes. The compiler tracks how long each reference is valid and proves they never dangle."

Lifetimes make explicit what C# hides behind garbage collection. The benefit: zero runtime cost, guaranteed safety, and documentation of reference relationships in your API.