Guide contents

Lesson 26 of 94

Lifetimes in Structs & Methods

So far every struct has owned its data: String rather than &str, Vec<T> rather than &[T]. A struct can also hold references. That makes it a lightweight view into data owned somewhere else, which is how zero-copy parsers, iterators, and many high-performance libraries work.

The rule is the same as for functions: a struct holding a reference must not outlive the data it refers to. To let the compiler check that, the struct declares a lifetime parameter.

1. A struct holding a reference needs a lifetime

Rust✗ does not compile · E0106
struct Highlight {
    text: &str,
}

fn main() {}
Output
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:11
  |
2 |     text: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Highlight<'a> {
2 ~     text: &'a str,
  |

2. Declaring the lifetime parameter

struct Highlight<'a> means "a Highlight borrows something for a lifetime 'a, and can't outlive it". The field &'a str says what is borrowed.

Rust
#[derive(Debug)]
struct Highlight<'a> {
    text: &'a str,
    line: usize,
}

fn main() {
    let article = String::from("Rust has no garbage collector.\nOwnership replaces it.");
    let first_line = article.lines().next().unwrap();
    let h = Highlight { text: first_line, line: 1 };
    println!("{:?}", h);
}
Output
Highlight { text: "Rust has no garbage collector.", line: 1 }

h points into article. Creating it copies no text.

3. The struct can't outlive its data

Rust✗ does not compile · E0597
struct Highlight<'a> {
    text: &'a str,
}

fn main() {
    let h;
    {
        let article = String::from("temporary text");
        h = Highlight { text: &article };
    }
    println!("{}", h.text);
}
Output
error[E0597]: `article` does not live long enough
  --> src/main.rs:9:31
   |
 8 |         let article = String::from("temporary text");
   |             ------- binding `article` declared here
 9 |         h = Highlight { text: &article };
   |                               ^^^^^^^^ borrowed value does not live long enough
10 |     }
   |     - `article` dropped here while still borrowed
11 |     println!("{}", h.text);
   |                    ------ borrow later used here

4. impl blocks declare the lifetime too

The lifetime is part of the type's name, so the impl block declares it: impl<'a> Highlight<'a>.

Rust
struct Highlight<'a> {
    text: &'a str,
    line: usize,
}

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

    fn word_count(&self) -> usize {
        self.text.split_whitespace().count()
    }

    fn render(&self) -> String {
        format!("line {}: \"{}\" ({} words)", self.line, self.text, self.word_count())
    }
}

fn main() {
    let doc = String::from("Structs can hold references too");
    let h = Highlight::new(&doc, 3);
    println!("{}", h.render());
}
Output
line 3: "Structs can hold references too" (5 words)

5. Returning the borrowed data from a method

A method can hand out the reference the struct holds, with the struct's own lifetime 'a. The result then lives as long as the original data, not just as long as the struct.

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

impl<'a> Highlight<'a> {
    fn text(&self) -> &'a str {
        self.text
    }
}

fn main() {
    let doc = String::from("the original document");
    let extracted;
    {
        let h = Highlight { text: &doc[4..12] };
        extracted = h.text(); // outlives `h`, but not `doc`
    }
    println!("{extracted}");
}
Output
original

If the method were written fn text(&self) -> &str, elision would tie the result to &self, and extracted could not outlive h. Choosing &'a str is a small but real design decision.

6. Methods whose results borrow from self

When a method's result is computed from the struct itself, elision ties it to &self automatically.

Rust
struct Sentence<'a> {
    words: Vec<&'a str>,
}

impl<'a> Sentence<'a> {
    fn parse(text: &'a str) -> Self {
        Self { words: text.split_whitespace().collect() }
    }

    fn longest(&self) -> &str {
        let mut best = "";
        for w in &self.words {
            if w.len() > best.len() {
                best = w;
            }
        }
        best
    }
}

fn main() {
    let text = String::from("lifetimes make references trustworthy");
    let s = Sentence::parse(&text);
    println!("{} words, longest '{}'", s.words.len(), s.longest());
}
Output
4 words, longest 'trustworthy'

Vec<&'a str> is a vector of slices into text. collect() gathers the words into it; Iterators covers collect.

7. A zero-copy parser

A parser that holds the input and a position is the classic struct with a lifetime. Every token it returns is a slice of the input.

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

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

    fn skip_spaces(&mut self) {
        while self.input[self.pos..].starts_with(' ') {
            self.pos += 1;
        }
    }

    fn next_word(&mut self) -> Option<&'a str> {
        self.skip_spaces();
        if self.pos >= self.input.len() {
            return None;
        }
        let rest = &self.input[self.pos..];
        let end = rest.find(' ').unwrap_or(rest.len());
        self.pos += end;
        Some(&rest[..end])
    }
}

fn main() {
    let command = String::from("  deploy   api-server   production ");
    let mut p = Parser::new(&command);
    let mut words = Vec::new();
    while let Some(w) = p.next_word() {
        words.push(w);
    }
    println!("{:?}", words);
}
Output
["deploy", "api-server", "production"]

next_word takes &mut self but returns &'a str, tied to the input rather than to the parser. That is why words can keep collecting slices while p is mutably borrowed again on each call. If the return type were elided to &str, each word would keep p borrowed and the loop would not compile.

8. Several references with one lifetime

When every reference in a struct points into the same data, one lifetime parameter is enough.

Rust
#[derive(Debug)]
struct KeyValue<'a> {
    key: &'a str,
    value: &'a str,
}

fn parse_pair(line: &str) -> Option<KeyValue<'_>> {
    let (k, v) = line.split_once('=')?;
    Some(KeyValue { key: k.trim(), value: v.trim() })
}

fn main() {
    let lines = String::from("host = example.com\nport=443\ninvalid line");
    for line in lines.lines() {
        println!("{:?}", parse_pair(line));
    }
}
Output
Some(KeyValue { key: "host", value: "example.com" })
Some(KeyValue { key: "port", value: "443" })
None

KeyValue<'_> in the return type means "a KeyValue with the lifetime elision would pick", here the lifetime of line. The '_ is called the anonymous lifetime.

9. Two lifetimes when the data comes from two places

If a struct's references can point into different owners that live for different times, give them separate lifetimes so that each is checked on its own.

Rust
struct Message<'text, 'meta> {
    body: &'text str,
    sender: &'meta str,
}

impl<'text, 'meta> Message<'text, 'meta> {
    fn body(&self) -> &'text str {
        self.body
    }
}

fn main() {
    let body = String::from("the deploy finished");
    let kept_body;
    {
        let sender = String::from("ci-bot"); // shorter-lived
        let m = Message { body: &body, sender: &sender };
        println!("{} says: {}", m.sender, m.body);
        kept_body = m.body();
    }
    println!("still have: {kept_body}");
}
Output
ci-bot says: the deploy finished
still have: the deploy finished

With a single lifetime 'a for both fields, kept_body would be limited by sender and the last line would not compile. Most structs need only one lifetime; reach for two when you hit exactly this situation.

10. A view over a slice of records

A struct can borrow a slice of a larger collection: a page of results, a window of a time series, a batch.

Rust
struct Order {
    id: u32,
    total_cents: u64,
}

struct Page<'a> {
    items: &'a [Order],
    number: usize,
    total_pages: usize,
}

impl<'a> Page<'a> {
    fn of(all: &'a [Order], number: usize, size: usize) -> Self {
        let start = (number * size).min(all.len());
        let end = (start + size).min(all.len());
        Self { items: &all[start..end], number, total_pages: all.len().div_ceil(size) }
    }

    fn revenue(&self) -> u64 {
        self.items.iter().map(|o| o.total_cents).sum()
    }
}

fn main() {
    let orders: Vec<Order> = (1..=7).map(|i| Order { id: i, total_cents: i as u64 * 1_000 }).collect();
    for n in 0..3 {
        let page = Page::of(&orders, n, 3);
        let ids: Vec<u32> = page.items.iter().map(|o| o.id).collect();
        println!("page {}/{}: {:?} revenue {}", page.number + 1, page.total_pages, ids, page.revenue());
    }
}
Output
page 1/3: [1, 2, 3] revenue 6000
page 2/3: [4, 5, 6] revenue 15000
page 3/3: [7] revenue 7000

11. A struct holding a &mut

A struct can also hold a mutable reference, for example a writer that appends to a buffer it does not own.

Rust
struct LineWriter<'a> {
    out: &'a mut String,
    lines_written: usize,
}

impl<'a> LineWriter<'a> {
    fn new(out: &'a mut String) -> Self {
        Self { out, lines_written: 0 }
    }

    fn line(&mut self, text: &str) -> &mut Self {
        self.lines_written += 1;
        self.out.push_str(&format!("{:>3}| {text}\n", self.lines_written));
        self
    }
}

fn main() {
    let mut report = String::new();
    {
        let mut w = LineWriter::new(&mut report);
        w.line("Build succeeded").line("12 tests passed").line("0 warnings");
        println!("wrote {} lines", w.lines_written);
    }
    print!("{report}");
}
Output
wrote 3 lines
  1| Build succeeded
  2| 12 tests passed
  3| 0 warnings

12. The original is locked while the &mut struct is in use

LineWriter holds a mutable borrow of report, so report can't be read until the writer is done.

Rust✗ does not compile · E0502
struct LineWriter<'a> {
    out: &'a mut String,
}

impl<'a> LineWriter<'a> {
    fn line(&mut self, text: &str) {
        self.out.push_str(text);
        self.out.push('\n');
    }
}

fn main() {
    let mut report = String::new();
    let mut w = LineWriter { out: &mut report };
    w.line("first");
    println!("so far: {report}");
    w.line("second");
}
Output
error[E0502]: cannot borrow `report` as immutable because it is also borrowed as mutable
  --> src/main.rs:16:24
   |
14 |     let mut w = LineWriter { out: &mut report };
   |                                   ----------- mutable borrow occurs here
15 |     w.line("first");
16 |     println!("so far: {report}");
   |                        ^^^^^^ immutable borrow occurs here
17 |     w.line("second");
   |     - mutable borrow later used here
   |

Moving the println! after the last use of w fixes it: the mutable borrow ends at the last use.

13. Enums with lifetimes

Enums can hold references in their variants in exactly the same way.

Rust
#[derive(Debug)]
enum Token<'a> {
    Word(&'a str),
    Number(i64),
    Symbol(char),
}

fn tokens(src: &str) -> Vec<Token<'_>> {
    let mut out = Vec::new();
    for part in src.split_whitespace() {
        if let Ok(n) = part.parse() {
            out.push(Token::Number(n));
        } else if part.len() == 1 && !part.chars().next().unwrap().is_alphanumeric() {
            out.push(Token::Symbol(part.chars().next().unwrap()));
        } else {
            out.push(Token::Word(part));
        }
    }
    out
}

fn main() {
    let src = String::from("width = 80 * scale");
    for t in tokens(&src) {
        println!("{:?}", t);
    }
}
Output
Word("width")
Symbol('=')
Number(80)
Symbol('*')
Word("scale")

14. Returning a borrowed struct from a function

Rust
#[derive(Debug)]
struct RequestLine<'a> {
    method: &'a str,
    path: &'a str,
    version: &'a str,
}

fn parse_request_line(line: &str) -> Option<RequestLine<'_>> {
    let mut parts = line.split(' ');
    let method = parts.next()?;
    let path = parts.next()?;
    let version = parts.next()?;
    if parts.next().is_some() || !version.starts_with("HTTP/") {
        return None;
    }
    Some(RequestLine { method, path, version })
}

fn main() {
    let raw = String::from("GET /index.html HTTP/1.1");
    println!("{:?}", parse_request_line(&raw));
    println!("{:?}", parse_request_line("GARBAGE"));
}
Output
Some(RequestLine { method: "GET", path: "/index.html", version: "HTTP/1.1" })
None

15. A struct can't borrow from its own field

A common first idea is a struct that owns a String and also holds a &str into it. This is a self-referential struct, and Rust does not allow it: if the struct were moved, the String's header would move with it and the reference's relationship to it could not be checked.

Rust✗ does not compile · E0106
struct Document {
    text: String,
    title: &str, // wants to point into `text`
}

fn main() {}
Output
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     title: &str, // wants to point into `text`
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Document<'a> {
2 |     text: String,
3 ~     title: &'a str, // wants to point into `text`
  |

Even with a lifetime parameter, there is no way to say "'a is the lifetime of my own text field". Two good alternatives follow.

16. Alternative 1: store positions instead of references

Store the byte range of the title and compute the slice on demand. The struct is fully owned and can move freely.

Rust
struct Document {
    text: String,
    title_range: (usize, usize),
}

impl Document {
    fn new(text: String) -> Self {
        let end = text.find('\n').unwrap_or(text.len());
        Self { text, title_range: (0, end) }
    }

    fn title(&self) -> &str {
        &self.text[self.title_range.0..self.title_range.1]
    }
}

fn main() {
    let doc = Document::new(String::from("Release Notes\n- faster builds\n- fewer bugs"));
    let moved = doc; // moving is fine: there are no internal references
    println!("{}", moved.title());
}
Output
Release Notes

17. Alternative 2: separate the owner and the view

Keep the owned data in one variable and create a borrowing view struct next to it.

Rust
struct DocumentView<'a> {
    title: &'a str,
    body: &'a str,
}

fn view(text: &str) -> DocumentView<'_> {
    match text.split_once('\n') {
        Some((title, body)) => DocumentView { title, body },
        None => DocumentView { title: text, body: "" },
    }
}

fn main() {
    let text = String::from("Release Notes\n- faster builds\n- fewer bugs");
    let v = view(&text);
    println!("title: {}", v.title);
    println!("body lines: {}", v.body.lines().count());
}
Output
title: Release Notes
body lines: 2

18. Borrowed or owned fields?

Both designs are useful. A common pattern is to have both: a borrowed view for fast processing and an owned version for storing.

Rust
#[derive(Debug)]
struct UserRef<'a> {
    name: &'a str,
    email: &'a str,
}

#[derive(Debug)]
struct User {
    name: String,
    email: String,
}

impl<'a> UserRef<'a> {
    fn to_owned_user(&self) -> User {
        User { name: self.name.to_string(), email: self.email.to_string() }
    }
}

fn parse(line: &str) -> Option<UserRef<'_>> {
    let (name, email) = line.split_once(',')?;
    Some(UserRef { name: name.trim(), email: email.trim() })
}

fn main() {
    let mut saved: Vec<User> = Vec::new();
    {
        let csv = String::from("Ana, ana@example.com\nBen, ben@example.com\nnot valid");
        for line in csv.lines() {
            if let Some(u) = parse(line) {
                if u.email.ends_with("example.com") {
                    saved.push(u.to_owned_user()); // copy only what we keep
                }
            }
        }
    } // the CSV text is freed here
    println!("{:?}", saved);
}
Output
[User { name: "Ana", email: "ana@example.com" }, User { name: "Ben", email: "ben@example.com" }]
Owned fields (String, Vec<T>) Borrowed fields (&'a str, &'a [T])
The struct can be stored, returned, and moved anywhere The struct must not outlive the data it borrows
Creating it copies the data Creating it copies nothing
No lifetime parameters Lifetime parameters on the struct and its impl
Right default for application data Right for parsers, views, iterators, short-lived helpers

19. A struct holding a reference to another struct

Rust
struct Config {
    currency: String,
    tax_percent: u32,
}

struct Invoice<'a> {
    config: &'a Config,
    lines: Vec<(String, u32)>,
}

impl<'a> Invoice<'a> {
    fn total(&self) -> String {
        let net: u32 = self.lines.iter().map(|(_, cents)| cents).sum();
        let gross = net + net * self.config.tax_percent / 100;
        format!("{}.{:02} {}", gross / 100, gross % 100, self.config.currency)
    }
}

fn main() {
    let config = Config { currency: String::from("EUR"), tax_percent: 21 };
    let a = Invoice { config: &config, lines: vec![(String::from("hosting"), 5_000)] };
    let b = Invoice { config: &config, lines: vec![(String::from("domain"), 1_200), (String::from("email"), 300)] };
    println!("{}", a.total());
    println!("{}", b.total());
}
Output
60.50 EUR
18.15 EUR

Many invoices share one configuration without copying it. The lifetime proves the configuration outlives all the invoices.

20. Realistic example: a CSV record view

Rust
struct Record<'a> {
    fields: Vec<&'a str>,
}

impl<'a> Record<'a> {
    fn parse(line: &'a str) -> Self {
        Self { fields: line.split(',').map(|f| f.trim()).collect() }
    }

    fn get(&self, index: usize) -> Option<&'a str> {
        self.fields.get(index).copied()
    }
}

fn main() {
    let csv = String::from("id,name,city\n1,Ada,London\n2,Linus,Helsinki\n3,Grace,Arlington");
    let mut lines = csv.lines();
    let header = Record::parse(lines.next().unwrap());
    let city_col = header.fields.iter().position(|&h| h == "city").unwrap();
    let mut cities = Vec::new();
    for line in lines {
        let r = Record::parse(line);
        if let Some(city) = r.get(city_col) {
            cities.push(city);
        }
    }
    println!("{:?}", cities);
}
Output
["London", "Helsinki", "Arlington"]

cities holds slices of csv even though each Record is dropped at the end of its loop iteration. That works because get returns &'a str, tied to the CSV text rather than to the record.

21. Realistic example: a log analyzer over borrowed lines

Rust
struct LogLine<'a> {
    level: &'a str,
    service: &'a str,
    message: &'a str,
}

struct Analyzer<'a> {
    lines: Vec<LogLine<'a>>,
}

impl<'a> Analyzer<'a> {
    fn new(log: &'a str) -> Self {
        let mut lines = Vec::new();
        for raw in log.lines() {
            let mut parts = raw.splitn(3, ' ');
            if let (Some(level), Some(service), Some(message)) = (parts.next(), parts.next(), parts.next()) {
                lines.push(LogLine { level, service, message });
            }
        }
        Self { lines }
    }

    fn errors_for(&self, service: &str) -> Vec<&'a str> {
        let mut out = Vec::new();
        for l in &self.lines {
            if l.level == "ERROR" && l.service == service {
                out.push(l.message);
            }
        }
        out
    }

    fn noisiest_service(&self) -> Option<&'a str> {
        let mut best: Option<(&str, usize)> = None;
        for l in &self.lines {
            let count = self.lines.iter().filter(|x| x.service == l.service).count();
            if best.is_none_or(|(_, c)| count > c) {
                best = Some((l.service, count));
            }
        }
        best.map(|(s, _)| s)
    }
}

fn main() {
    let log = String::from(
        "INFO api request ok\nERROR db connection refused\nERROR api upstream timeout\nWARN db slow query\nERROR db connection refused again",
    );
    let a = Analyzer::new(&log);
    println!("db errors: {:?}", a.errors_for("db"));
    println!("noisiest: {:?}", a.noisiest_service());
}
Output
db errors: ["connection refused", "connection refused again"]
noisiest: Some("db")

22. Realistic example: highlighting search matches

The result is a list of pieces of the original text, each marked as matching or not, ready to be rendered with colors. No text is copied.

Rust
#[derive(Debug)]
enum Span<'a> {
    Plain(&'a str),
    Match(&'a str),
}

fn highlight<'a>(text: &'a str, needle: &str) -> Vec<Span<'a>> {
    let mut spans = Vec::new();
    let mut rest = text;
    while let Some(i) = rest.find(needle) {
        if i > 0 {
            spans.push(Span::Plain(&rest[..i]));
        }
        spans.push(Span::Match(&rest[i..i + needle.len()]));
        rest = &rest[i + needle.len()..];
    }
    if !rest.is_empty() {
        spans.push(Span::Plain(rest));
    }
    spans
}

fn render(spans: &[Span]) -> String {
    let mut out = String::new();
    for s in spans {
        match s {
            Span::Plain(t) => out.push_str(t),
            Span::Match(t) => out.push_str(&format!("[{t}]")),
        }
    }
    out
}

fn main() {
    let text = String::from("borrow here, borrow there, borrow everywhere");
    let spans = highlight(&text, "borrow");
    println!("{} spans", spans.len());
    println!("{}", render(&spans));
}
Output
6 spans
[borrow] here, [borrow] there, [borrow] everywhere

fn render(spans: &[Span]) leaves out the lifetime of Span. That is allowed in function parameters, but writing &[Span<'_>] makes it visible that Span borrows; the elided_lifetimes_in_paths lint can require that style.

23. Realistic example: an HTTP request view

A complete request parser that borrows every field from the raw request.

Rust
#[derive(Debug)]
struct Request<'a> {
    method: &'a str,
    path: &'a str,
    headers: Vec<(&'a str, &'a str)>,
    body: &'a str,
}

impl<'a> Request<'a> {
    fn parse(raw: &'a str) -> Result<Self, String> {
        let (head, body) = raw.split_once("\r\n\r\n").unwrap_or((raw, ""));
        let mut lines = head.split("\r\n");
        let request_line = lines.next().ok_or("empty request")?;
        let mut parts = request_line.split(' ');
        let method = parts.next().ok_or("missing method")?;
        let path = parts.next().ok_or("missing path")?;
        let mut headers = Vec::new();
        for line in lines {
            let (name, value) = line.split_once(':').ok_or(format!("bad header line: {line}"))?;
            headers.push((name.trim(), value.trim()));
        }
        Ok(Self { method, path, headers, body })
    }

    fn header(&self, name: &str) -> Option<&'a str> {
        self.headers.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| *v)
    }
}

fn main() {
    let raw = String::from("POST /api/orders HTTP/1.1\r\nHost: shop.example\r\nContent-Type: application/json\r\n\r\n{\"sku\":\"K-1\"}");
    let req = Request::parse(&raw).unwrap();
    println!("{} {}", req.method, req.path);
    println!("content-type: {:?}", req.header("content-type"));
    println!("authorization: {:?}", req.header("Authorization"));
    println!("body: {}", req.body);
}
Output
POST /api/orders
content-type: Some("application/json")
authorization: None
body: {"sku":"K-1"}

24. Realistic example: a sliding-window rate checker

A struct that borrows a slice of timestamps and answers questions about it.

Rust
struct Window<'a> {
    timestamps_ms: &'a [u64],
}

impl<'a> Window<'a> {
    fn in_last(&self, now_ms: u64, span_ms: u64) -> &'a [u64] {
        let start = now_ms.saturating_sub(span_ms);
        let first = self.timestamps_ms.iter().position(|&t| t >= start).unwrap_or(self.timestamps_ms.len());
        &self.timestamps_ms[first..]
    }

    fn allowed(&self, now_ms: u64, span_ms: u64, limit: usize) -> bool {
        self.in_last(now_ms, span_ms).len() < limit
    }
}

fn main() {
    let history = vec![1_000, 1_200, 5_000, 9_100, 9_500, 9_900];
    let w = Window { timestamps_ms: &history };
    println!("last 1s at t=10s: {:?}", w.in_last(10_000, 1_000));
    println!("allowed (limit 3/s): {}", w.allowed(10_000, 1_000, 3));
    println!("allowed (limit 5/s): {}", w.allowed(10_000, 1_000, 5));
}
Output
last 1s at t=10s: [9100, 9500, 9900]
allowed (limit 3/s): false
allowed (limit 5/s): true

Summary

  • A struct or enum that holds a reference declares a lifetime parameter: struct View<'a> { data: &'a str }.
  • impl<'a> View<'a> declares it again for the methods.
  • Such a struct can't outlive the data it borrows. While it holds a &mut, the original is locked.
  • A method can return &'a T (tied to the original data) or elided &T (tied to &self). The first is more flexible for callers.
  • '_ stands for "the lifetime elision would choose", as in -> Option<View<'_>>.
  • A struct can't borrow from its own fields. Store positions, or keep the owner and the view separate.
  • Use owned fields for data you store, and borrowed fields for parsers, views, and short-lived helpers.

Next: Lifetime Elision & 'static.