Skip to editor content
learningrust.orglesson 13 of 26

Lifetimes

Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime - the scope for which that reference is valid. Most of the time, lifetimes are inferred, but sometimes you need to annotate them explicitly.

What Are Lifetimes?

A lifetime is the scope during which a reference is valid. Consider this:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    // println!("{}", r); // ERROR: x doesn't live long enough
}                         // ---------+

The reference r has lifetime 'a, but it refers to x which only has lifetime 'b. Since 'b is shorter than 'a, the code won't compile.

Lifetime Annotation Syntax

Lifetime annotations describe relationships between lifetimes:

&i32        // a reference
&'a i32     // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'a

When You Need Lifetime Annotations

The compiler needs help when:

  1. A function returns a reference
  2. A struct holds references
  3. Multiple references have ambiguous relationships

Function Signatures

// This won't compile - Rust doesn't know which input's lifetime to use
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }

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

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");

    let result = longest(&string1, &string2);
    println!("The longest string is: {}", result);
}

The 'a annotation means: "the returned reference will be valid for the smaller of the two input lifetimes."

Lifetime Elision Rules

Rust has rules for inferring lifetimes so you don't always need annotations:

Rule 1: Each Input Gets Its Own Lifetime

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

Rule 2: One Input Lifetime = Output Lifetime

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

Rule 3: &self Lifetime = Output Lifetime

impl MyStruct {
    // Written:
    fn get_name(&self) -> &str { ... }
    // Compiler infers:
    fn get_name<'a>(&'a self) -> &'a str { ... }
}

Structs with References

When a struct holds references, you must annotate lifetimes:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();

    let excerpt = ImportantExcerpt {
        part: first_sentence,
    };

    println!("Excerpt: {}", excerpt.part);
}

The annotation 'a means: "an instance of ImportantExcerpt can't outlive the reference it holds."

Methods with Lifetimes

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    // Lifetime elision: &self lifetime is used for return
    fn level(&self) -> i32 {
        3
    }

    // Return type uses 'a from struct
    fn announce_and_return_part(&self, announcement: &str) -> &'a str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = ImportantExcerpt {
        part: novel.split('.').next().unwrap(),
    };

    println!("Level: {}", excerpt.level());
    println!("Part: {}", excerpt.announce_and_return_part("Here it comes!"));
}

The Static Lifetime

'static means the reference lives for the entire program duration:

fn main() {
    // String literals have 'static lifetime
    let s: &'static str = "I live forever!";

    println!("{}", s);
}

Use 'static sparingly - it's usually a sign you should reconsider your design.

Multiple Lifetime Parameters

Sometimes you need multiple lifetime parameters:

fn longest_with_announcement<'a, 'b>(
    x: &'a str,
    y: &'a str,
    ann: &'b str,
) -> &'a str {
    println!("Announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world!");
    let ann = String::from("Comparing strings");

    let result = longest_with_announcement(&s1, &s2, &ann);
    println!("Longest: {}", result);
}

Lifetime Bounds

You can specify that a generic type must live at least as long as a lifetime:

fn print_ref<'a, T>(t: &'a T)
where
    T: std::fmt::Display + 'a,
{
    println!("{}", t);
}

fn main() {
    let x = 5;
    print_ref(&x);
}

Common Lifetime Patterns

Pattern 1: Input/Output Relationship

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

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

Pattern 2: Struct Holding a Reference

struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Parser<'a> {
        Parser { input, position: 0 }
    }

    fn remaining(&self) -> &'a str {
        &self.input[self.position..]
    }
}

fn main() {
    let text = String::from("hello world");
    let parser = Parser::new(&text);
    println!("Remaining: {}", parser.remaining());
}

Pattern 3: Returning References from Methods

struct Container {
    data: Vec<String>,
}

impl Container {
    fn get(&self, index: usize) -> Option<&String> {
        self.data.get(index)
    }

    fn first(&self) -> Option<&String> {
        self.data.first()
    }
}

fn main() {
    let container = Container {
        data: vec![String::from("a"), String::from("b")],
    };

    if let Some(first) = container.first() {
        println!("First: {}", first);
    }
}

Practice Exercise

// A struct that borrows a string slice
struct Highlight<'a> {
    text: &'a str,
    start: usize,
    end: usize,
}

impl<'a> Highlight<'a> {
    fn new(text: &'a str, start: usize, end: usize) -> Highlight<'a> {
        Highlight { text, start, end }
    }

    fn highlighted_portion(&self) -> &'a str {
        &self.text[self.start..self.end]
    }

    fn full_text(&self) -> &'a str {
        self.text
    }
}

fn main() {
    let document = String::from("Rust is a systems programming language");

    let highlight = Highlight::new(&document, 0, 4);

    println!("Highlighted: '{}'", highlight.highlighted_portion());
    println!("Full text: '{}'", highlight.full_text());
}

Key Takeaways

  • Lifetimes ensure references are always valid
  • Most lifetimes are inferred by the compiler
  • Use 'a syntax when the compiler needs help
  • Lifetime annotations describe relationships, they don't change how long things live
  • Structs holding references need lifetime parameters
  • 'static means "lives for the entire program"
  • Lifetime elision rules reduce annotation boilerplate

Lifetimes are one of Rust's most powerful features for memory safety!

Next Steps

With lifetimes understood, you're ready for traits and generics — Rust's tools for writing flexible, reusable code that works across different types.

Next lesson

Traits & Generics

Learn to write flexible, reusable code with Rust's trait system and generic programming

30 min