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
struct Highlight {
text: &str,
}
fn main() {}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.
#[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);
}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
struct Highlight<'a> {
text: &'a str,
}
fn main() {
let h;
{
let article = String::from("temporary text");
h = Highlight { text: &article };
}
println!("{}", h.text);
}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 here4. 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>.
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());
}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.
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}");
}originalIf 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.
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());
}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.
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);
}["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.
#[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));
}
}Some(KeyValue { key: "host", value: "example.com" })
Some(KeyValue { key: "port", value: "443" })
NoneKeyValue<'_> 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.
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}");
}ci-bot says: the deploy finished
still have: the deploy finishedWith 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.
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());
}
}page 1/3: [1, 2, 3] revenue 6000
page 2/3: [4, 5, 6] revenue 15000
page 3/3: [7] revenue 700011. 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.
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}");
}wrote 3 lines
1| Build succeeded
2| 12 tests passed
3| 0 warnings12. 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.
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");
}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.
#[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);
}
}Word("width")
Symbol('=')
Number(80)
Symbol('*')
Word("scale")14. Returning a borrowed struct from a function
#[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"));
}Some(RequestLine { method: "GET", path: "/index.html", version: "HTTP/1.1" })
None15. 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.
struct Document {
text: String,
title: &str, // wants to point into `text`
}
fn main() {}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.
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());
}Release Notes17. Alternative 2: separate the owner and the view
Keep the owned data in one variable and create a borrowing view struct next to it.
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());
}title: Release Notes
body lines: 218. 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.
#[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);
}[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
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());
}60.50 EUR
18.15 EURMany invoices share one configuration without copying it. The lifetime proves the configuration outlives all the invoices.
20. Realistic example: a CSV record view
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);
}["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
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());
}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.
#[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));
}6 spans
[borrow] here, [borrow] there, [borrow] everywherefn 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.
#[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);
}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.
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));
}last 1s at t=10s: [9100, 9500, 9900]
allowed (limit 3/s): false
allowed (limit 5/s): trueSummary
- 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.
