[Rust Series Part 14] Lifetime Fundamentals

한국어 버전

Most Rust code compiles once you understand ownership and borrowing, but functions that return references often hit "please specify lifetimes" errors. Here we cover why lifetime annotations exist, the minimal syntax needed to solve common issues, and how the compiler tracks borrowed references.

This is one of the most abstract chapters in the Rust series. So instead of trying to memorize every rule at once, focus on one idea first: a lifetime is a label that describes the relationship between references.

What Is a Lifetime?

  • Definition: A named label that helps the compiler reason about how long a reference stays valid.
  • Purpose: Prevent dangling references that point to freed data.
  • Notation: Short names like 'a, 'b prefixed with an apostrophe.

The key insight: 'a does not measure wall-clock time. It simply ties references together and says "these all have to live at least as long as each other."

A useful metaphor is a badge label, not a stopwatch. A lifetime does not add more time; it marks which references are allowed into the same safe region.

Why Lifetime Annotations Show Up

At the simplest level, the problem looks like this:

  • A function takes references as input.
  • The function returns a reference.
  • The compiler cannot tell which input reference the output is tied to.

That is when lifetime annotations become necessary.

fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
  • This function fails to compile because the compiler cannot prove whether the returned reference came from a or b.
  • Without extra clues, it cannot guarantee the return value will live long enough.
  • As code grows more complex—say, storing references in structs—the compiler needs explicit relationships.

So lifetime annotations are not about making references live longer. They are about telling the compiler which input reference the output depends on.

Adding Explicit Lifetime Parameters

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}
  • <'a> declares that the function uses a lifetime parameter named 'a.
  • &'a str means "a string slice that lives for 'a."
  • Because the return type also uses 'a, callers know the output cannot outlive the shorter of the two inputs.

At first, it helps to read the signature line by line:

  • a: &'a str -> input a is valid for 'a
  • b: &'a str -> input b is also valid for 'a
  • -> &'a str -> the returned reference is valid only inside that same relationship

If that part makes sense, you already understand most beginner lifetime syntax.

Using Lifetimes in Structs

struct Highlight<'a> {
    snippet: &'a str,
}

impl<'a> Highlight<'a> {
    fn show(&self) {
        println!("{}", self.snippet);
    }
}
  • Structs that store references must declare lifetime parameters.
  • Highlight<'a> promises that it only borrows data for 'a, so an instance cannot outlive the data it points to.
  • If the struct owned a String, no lifetime annotation would be necessary.

Common Lifetime Patterns in Signatures

Pattern Meaning Example
fn foo<'a>(x: &'a str) -> &'a str Output lives as long as input Return whichever slice is longer
fn bar<'a, 'b>(x: &'a str, y: &'b str) -> &'a str Inputs each keep their own lifetime Return only one of the inputs
impl<'a> Struct<'a> Struct fields hold references Parsers, lexers, etc.

Lifetime Elision Rules

Rust applies three default rules so you rarely have to annotate lifetimes manually:

  1. Each reference parameter gets its own lifetime parameter.
  2. If there's only one input reference, its lifetime is assigned to the output.
  3. For methods with &self or &mut self, the receiver's lifetime flows to the output.

When a function grows more complicated than those rules, add explicit annotations. If the compiler says it's ambiguous, write the lifetimes down.

At the beginner level, this rule of thumb works well:

  • One input reference: elision often works.
  • Two or more input references: the return relationship can become ambiguous.
  • References stored in structs: explicit lifetimes are common.

The 'static Lifetime

  • Represents data that lives for the entire program run.
  • String literals like "hello" are 'static.
  • Accepting &'static str (fn foo(x: &'static str)) severely limits callers, so slapping 'static on everything rarely solves real problems.

Reading Borrow Checker Errors

fn main() {
    let result;
    {
        let s = String::from("rust");
        result = s.as_str();
    }
    println!("{}", result);
}
  • This fails because s drops before result is used.
  • Lifetime annotations cannot extend s's lifetime; they only describe relationships. If the data dies early, no annotation can resurrect it.

That is the most important boundary to remember: lifetimes explain safety, but they do not magically keep data alive.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Practice Exercises

  1. Implement fn find<'a>(haystack: &'a str, needle: &str) -> Option<&'a str> and ensure the returned slice shares haystack's lifetime.
  2. Build a logger struct that stores references in its fields and annotate it with 'a to satisfy the compiler.
  3. Deliberately write a function that breaks the elision rules and read the resulting compiler message.

Completion Checklist

  • Can explain why lifetime annotations exist and how to write the syntax.
  • Have added basic lifetime parameters to functions or structs.
  • Can recite the three elision rules.

Next we will keep that reference safety while learning iterators and closures that streamline data pipelines.

💬 댓글

이 글에 대한 의견을 남겨주세요