Lesson 18 of 22ยท17 min

Lifetimes

Every reference in Rust points at data that someone else owns, and the compiler guarantees that the data is still there whenever the reference is used. The ownership lesson showed the rules for how you may borrow (many & or one &mut). This lesson is about how long: the span of code during which a reference is valid is its lifetime.

Most of the time the compiler works lifetimes out on its own, and you never write one. You need to spell them out when a function returns a reference and the compiler can't tell which argument it borrows from, and when a struct stores a reference. This lesson covers why lifetimes exist, the 'a syntax, the rules that let you leave them out, structs holding & and &mut references, and 'static.

References can't outlive their data

Here r borrows name, but name is dropped at the end of the inner block, before r is used:

fn main() {
    let r;
    {
        let name = String::from("Ferris");
        r = &name;
    }
    println!("{r}");
}

In a language without this check, r would point at freed memory, a dangling reference. Rust's borrow checker compares two spans: how long name lives (until the inner }) and how long the borrow r is used (until the println!). The borrow has to fit inside the lifetime of the data, and here it doesn't. Moving the println! into the inner block, or moving name out of it, fixes it.

Inside one function the compiler sees everything, so it never needs help. The interesting cases are at function boundaries.

Lifetimes in function signatures

A function that takes one reference and returns one needs no annotations:

fn first_word(text: &str) -> &str {
    match text.split_once(' ') {
        Some((word, _)) => word,
        None => text,
    }
}
 
fn main() {
    let sentence = String::from("borrow checker");
    let word = first_word(&sentence);
    println!("{word}");
}
borrow

The returned &str can only come from text, so the compiler knows word borrows from sentence and must not outlive it.

With two reference parameters that is no longer obvious. This function looks up a key in a map of settings and falls back to a default:

use std::collections::HashMap;
 
fn setting(map: &HashMap<String, String>, key: &str, fallback: &str) -> &str {
    match map.get(key) {
        Some(value) => value,
        None => fallback,
    }
}
 
fn main() {
    let map = HashMap::new();
    println!("{}", setting(&map, "theme", "dark"));
}

The compiler checks each function on its own, using only its signature, not its body. From the signature alone it can't tell whether the result borrows from map, key or fallback, and callers need to know that to check their own code. You tell it with lifetime parameters:

use std::collections::HashMap;
 
fn setting<'a>(map: &'a HashMap<String, String>, key: &str, fallback: &'a str) -> &'a str {
    match map.get(key) {
        Some(value) => value,
        None => fallback,
    }
}
 
fn main() {
    let mut map = HashMap::new();
    map.insert(String::from("theme"), String::from("light"));
 
    println!("{}", setting(&map, "theme", "dark"));
    println!("{}", setting(&map, "font", "monospace"));
}
light
monospace

A lifetime parameter is a name starting with an apostrophe, usually 'a, 'b and so on. It is declared in angle brackets after the function name, like a generic type, and written after the & of a reference: &'a str, &'a mut Vec<u32>.

The signature now says: the returned &str lives at most as long as both the map borrow and the fallback borrow, because it may come from either. The compiler's suggestion put 'a on key as well, which would compile, but the result never borrows from key, so leaving it out is more accurate: the key can be a temporary that is gone as soon as the call returns.

What an annotation means

Lifetime annotations don't change how long anything lives. They describe a relationship between references, and the compiler then checks both sides of it:

  • Inside setting, the body may only return something that lives for 'a. Returning key would be an error, because key has no 'a.
  • At every call, 'a becomes the shorter of the two borrows passed in, and the result may not be used after that.

The second point means the caller's code is checked against the signature:

use std::collections::HashMap;
 
fn setting<'a>(map: &'a HashMap<String, String>, key: &str, fallback: &'a str) -> &'a str {
    match map.get(key) {
        Some(value) => value,
        None => fallback,
    }
}
 
fn main() {
    let map = HashMap::new();
    let chosen;
    {
        let fallback = String::from("dark");
        chosen = setting(&map, "theme", &fallback);
    }
    println!("{chosen}");
}

The map is empty, so if this ran, chosen would point at fallback, which is gone by the println!. But the compiler doesn't look at what the map holds: it rejects the call because the signature says the result may borrow from fallback, and that would be rejected even if theme were in the map. That is the trade-off of checking by signature. In return, changing a function's body can never break the code that calls it, as long as the signature stays the same.

Lifetime elision

Writing 'a on every reference would be tedious, so the compiler fills in lifetimes following three elision rules. If the rules leave no doubt about the output lifetime, you don't write any:

  1. Each reference parameter gets its own lifetime: fn f(a: &str, b: &str) means fn f<'a, 'b>(a: &'a str, b: &'b str).
  2. If there is exactly one input lifetime, every output reference gets it: fn first_word(text: &str) -> &str means fn first_word<'a>(text: &'a str) -> &'a str.
  3. If one of the parameters is &self or &mut self, output references get the lifetime of self.

setting needed annotations because rule 1 gave it three input lifetimes and it has no self, so neither rule 2 nor rule 3 applies.

Rule 3 is why methods rarely need annotations. A method that takes &self and another reference returns something borrowed from self by default:

struct Library {
    titles: Vec<String>,
}
 
impl Library {
    fn find(&self, prefix: &str) -> Option<&str> {
        for title in &self.titles {
            if title.starts_with(prefix) {
                return Some(title);
            }
        }
        None
    }
}
 
fn main() {
    let library = Library {
        titles: vec![String::from("Dune"), String::from("Emma")],
    };
 
    let found = {
        let query = String::from("Em");
        library.find(&query)
    };
    println!("{found:?}");
}
Some("Emma")

query is dropped before found is printed, and that's fine: by rule 3 the result borrows from library, not from prefix. If a method returns something borrowed from another parameter instead, rule 3 gets it wrong and you annotate that parameter and the return type yourself.

Structs that hold references

A struct field can be a reference, but then the struct is only valid while the data it points at is. The struct declares a lifetime parameter and uses it on the field:

#[derive(Debug)]
struct Record<'a> {
    name: &'a str,
    city: &'a str,
}
 
fn parse_record(line: &str) -> Option<Record<'_>> {
    let (name, city) = line.split_once(',')?;
    Some(Record {
        name: name.trim(),
        city: city.trim(),
    })
}
 
fn main() {
    let csv = String::from("Ada, London\nGrace, New York\nbroken line");
 
    let mut records = Vec::new();
    for line in csv.lines() {
        match parse_record(line) {
            Some(record) => records.push(record),
            None => println!("skipped: {line}"),
        }
    }
    println!("{records:?}");
    println!("{} lives in {}", records[1].name, records[1].city);
}
skipped: broken line
[Record { name: "Ada", city: "London" }, Record { name: "Grace", city: "New York" }]
Grace lives in New York

Record<'a> reads "a record that borrows from something that lives for 'a". The records don't copy any text: name and city are slices of csv, so parsing allocates nothing but the Vec. The price is that no Record can outlive csv, and the compiler enforces it.

Record<'_> in the return type of parse_record is the elided form: the struct has a lifetime, and rule 2 fills it in with the lifetime of line. Writing '_ instead of plain Record makes it visible that the result borrows.

Methods on a struct with a lifetime

An impl block for such a struct declares the lifetime the same way it declares a generic type: impl<'a> Record<'a>. Methods can then return references with either lifetime, and the difference matters:

struct Record<'a> {
    name: &'a str,
    city: &'a str,
}
 
impl<'a> Record<'a> {
    fn name(&self) -> &str {
        self.name
    }
 
    fn city(&self) -> &'a str {
        self.city
    }
}
 
fn main() {
    let line = String::from("Ada,London");
    let (name, city) = line.split_once(',').unwrap();
 
    let first_name;
    let city_name;
    {
        let record = Record { name, city };
        first_name = record.name().to_string();
        city_name = record.city();
    }
    println!("{first_name} / {city_name}");
}
Ada / London

name follows rule 3, so its result borrows from the Record itself and can't be used once record is dropped; the example copies it into a String instead. city returns &'a str, a borrow of the original line, so city_name stays valid after record is gone. Use the struct's lifetime when you hand back data the struct only borrowed.

Structs that hold mutable references

A struct can hold a &mut reference too, when it needs to change data it doesn't own. The borrow rules still apply: while the struct is in use, it has the only access to that data.

struct Tally<'a> {
    counts: &'a mut Vec<u32>,
}
 
impl Tally<'_> {
    fn record(&mut self, bucket: usize) {
        if bucket >= self.counts.len() {
            self.counts.resize(bucket + 1, 0);
        }
        self.counts[bucket] += 1;
    }
 
    fn total(&self) -> u32 {
        self.counts.iter().sum()
    }
}
 
fn main() {
    let mut counts = vec![0, 0];
 
    let mut tally = Tally {
        counts: &mut counts,
    };
    tally.record(0);
    tally.record(3);
    tally.record(3);
    println!("total: {}", tally.total());
 
    counts.push(10);
    println!("{counts:?}");
}
total: 3
[1, 0, 0, 2, 10]

impl Tally<'_> is the short form when no method needs to name the lifetime. record takes &mut self because it changes the data behind the field, and total only reads it.

main uses counts again right after the last use of tally. There is no inner block: a borrow lasts until the last place it is used, not until the end of the scope, so the &mut counts held by tally ends after total(). Using counts while tally still has uses ahead is an error, shown in the mistakes below.

'static

One lifetime has a name of its own: 'static, "valid for the whole run of the program". String literals are &'static str, because their text is stored in the program's binary:

fn weekday_name(day: u8) -> &'static str {
    match day {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        _ => "some other day",
    }
}
 
fn main() {
    let name;
    {
        let day = 2;
        name = weekday_name(day);
    }
    println!("{name}");
}
Tuesday

weekday_name has no reference parameters to borrow from, so it could only ever return something 'static, and the result can be kept as long as you like.

When a lifetime error is hard to fix, the compiler sometimes suggests adding 'static. Only take that suggestion if the data really lives forever, like a literal or a constant. Usually the right fix is different: return an owned String instead of a &str, or keep the owner alive longer. You'll also see 'static as a bound (T: 'static) when values are sent to other threads, which the concurrency lesson covers.

Common mistakes

Returning a reference to a local variable

fn greeting(name: &str) -> &str {
    let message = format!("Hello, {name}!");
    &message
}
 
fn main() {
    println!("{}", greeting("Ferris"));
}

message is dropped when greeting returns, so no lifetime annotation can make this work. The function created the String, so it should hand over ownership: return String and write message instead of &message.

A reference field without a lifetime

struct Record {
    name: &str,
}
 
fn main() {
    let record = Record { name: "Ada" };
    println!("{}", record.name);
}

Elision never applies to struct fields. Declare the lifetime on the struct and use it on the field, struct Record<'a> { name: &'a str }, or store an owned String if the struct should own its text.

Returning a reference with a lifetime the signature doesn't promise

use std::collections::HashMap;
 
fn setting<'a>(map: &'a HashMap<String, String>, key: &str, fallback: &str) -> &'a str {
    match map.get(key) {
        Some(value) => value,
        None => fallback,
    }
}
 
fn main() {
    let map = HashMap::new();
    println!("{}", setting(&map, "theme", "dark"));
}

The return type promises a borrow that lives for 'a, but fallback was only borrowed for some unrelated, possibly shorter, time. Every reference the function might return needs the output's lifetime: write fallback: &'a str.

Using the data while a struct holds a mutable reference to it

struct Tally<'a> {
    counts: &'a mut Vec<u32>,
}
 
impl Tally<'_> {
    fn record(&mut self, bucket: usize) {
        self.counts[bucket] += 1;
    }
}
 
fn main() {
    let mut counts = vec![0, 0];
    let mut tally = Tally {
        counts: &mut counts,
    };
    tally.record(0);
    println!("{counts:?}");
    tally.record(1);
}

tally holds &mut counts until its last use on the final line, so counts can't be read in between. Finish with the struct before touching the data again, or read it through the struct (add a method on Tally that returns what you need).

Summary

  • A lifetime is how long a reference is valid. A reference may never outlive the data it points at, and the borrow checker proves it at compile time.
  • Lifetime annotations ('a) don't change how long anything lives. They tell the compiler which inputs a returned reference borrows from, so both the function and its callers can be checked from the signature alone.
  • Thanks to the elision rules you only annotate when they leave the output ambiguous: several reference inputs and no &self.
  • A struct holding a reference needs a lifetime parameter (struct Record<'a>) and can't outlive what it borrows. Methods that hand back borrowed data can return &'a T so it outlives the struct.
  • A struct holding &mut data has exclusive access to it until the struct's last use.
  • 'static means valid for the whole program, as string literals are. Don't reach for it to silence an error; return owned data instead.

Practise what you read

Challenges that use the ideas from this lesson.