107-lifetime-structs — Lifetimes in Structs
Tutorial
The Problem
When a struct holds a reference, the struct's validity is tied to the data it borrows. A struct holding &str cannot outlive the String it was created from — if the String is dropped, the struct would hold a dangling pointer. Rust's lifetime annotations on structs make this relationship explicit, preventing the struct from being used after its data is freed.
This is the pattern behind zero-copy parsers (structs holding slices into the original input), iterator adapters (structs holding references to collections), and any API that borrows from caller data.
🎯 Learning Outcomes
<'a>impl blocksnom, winnow, and serdeCode Example
#[derive(Debug)]
struct Excerpt<'a> {
text: &'a str,
page: u32,
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let exc = Excerpt { text: &book[..16], page: 1 };
assert_eq!(exc.text, "Call me Ishmael.");
println!("Excerpt p.{}: {}", exc.page, exc.text);
// Compiler guarantees: exc cannot outlive book
}Key Differences
<'a> on any struct holding a reference; OCaml requires no annotations.<'a, 'b>); OCaml has no equivalent.OCaml Approach
OCaml structs holding string references have no equivalent constraint — the GC keeps everything alive:
type excerpt = { text: string; page: int }
let make_excerpt text page = { text; page }
(* text can be freed by OCaml's GC only when all references are dropped *)
An OCaml excerpt can outlive any particular binding to the original string because the GC tracks reference counts. There is no concept of "borrows from" in OCaml's type system.
Full Source
#![allow(clippy::all)]
// Example 107: Lifetimes in Structs
//
// When a struct holds a reference, it needs a lifetime parameter
// to ensure the referenced data outlives the struct.
// Approach 1: Idiomatic Rust — struct borrowing a string slice
// The lifetime 'a ties the struct's validity to the borrowed data.
#[derive(Debug)]
pub struct Excerpt<'a> {
pub text: &'a str,
pub page: u32,
}
impl<'a> Excerpt<'a> {
pub fn new(text: &'a str, page: u32) -> Self {
Excerpt { text, page }
}
pub fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.text
}
}
// Approach 2: Struct with multiple borrowed fields
// All fields share the same lifetime 'a — the struct is valid for
// exactly as long as the shortest-lived of its borrowed data.
#[derive(Debug)]
pub struct Article<'a> {
pub title: &'a str,
pub author: &'a str,
pub body: &'a str,
}
impl<'a> Article<'a> {
pub fn new(title: &'a str, author: &'a str, body: &'a str) -> Self {
Article {
title,
author,
body,
}
}
pub fn summarize(&self) -> String {
format!(
"{} by {} ({} chars)",
self.title,
self.author,
self.body.len()
)
}
}
// Approach 3: Struct with a lifetime-bound method returning a reference
// The returned &str borrows from self, so it cannot outlive the struct.
#[derive(Debug)]
pub struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
/// Returns the remaining unparsed input — a sub-slice of the original.
pub fn remaining(&self) -> &'a str {
&self.input[self.pos..]
}
/// Advance past the next `n` bytes (ASCII only for simplicity).
pub fn advance(&mut self, n: usize) {
self.pos = (self.pos + n).min(self.input.len());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_excerpt_borrows_slice() {
let book = String::from("Call me Ishmael. Some years ago...");
let exc = Excerpt::new(&book[..16], 1);
assert_eq!(exc.text, "Call me Ishmael.");
assert_eq!(exc.page, 1);
}
#[test]
fn test_excerpt_announce_returns_text() {
let sentence = "Fear is the mind-killer.";
let exc = Excerpt::new(sentence, 42);
let returned = exc.announce("test");
assert_eq!(returned, sentence);
}
#[test]
fn test_article_summarize() {
let title = "Rust Ownership";
let author = "Alice";
let body = "Ownership is the key to Rust's safety guarantees.";
let article = Article::new(title, author, body);
let summary = article.summarize();
assert!(summary.contains("Rust Ownership"));
assert!(summary.contains("Alice"));
assert!(summary.contains(&body.len().to_string()));
}
#[test]
fn test_article_fields_accessible() {
let article = Article {
title: "Zero Cost",
author: "Bob",
body: "Abstractions without overhead.",
};
assert_eq!(article.title, "Zero Cost");
assert_eq!(article.author, "Bob");
}
#[test]
fn test_parser_remaining_advances() {
let input = "fn main() {}";
let mut parser = Parser::new(input);
assert_eq!(parser.remaining(), "fn main() {}");
parser.advance(3);
assert_eq!(parser.remaining(), "main() {}");
}
#[test]
fn test_parser_advance_clamps_to_end() {
let input = "hello";
let mut parser = Parser::new(input);
parser.advance(100);
assert_eq!(parser.remaining(), "");
}
#[test]
fn test_excerpt_debug_format() {
let text = "To be or not to be";
let exc = Excerpt::new(text, 7);
let debug = format!("{:?}", exc);
assert!(debug.contains("To be or not to be"));
assert!(debug.contains('7'));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_excerpt_borrows_slice() {
let book = String::from("Call me Ishmael. Some years ago...");
let exc = Excerpt::new(&book[..16], 1);
assert_eq!(exc.text, "Call me Ishmael.");
assert_eq!(exc.page, 1);
}
#[test]
fn test_excerpt_announce_returns_text() {
let sentence = "Fear is the mind-killer.";
let exc = Excerpt::new(sentence, 42);
let returned = exc.announce("test");
assert_eq!(returned, sentence);
}
#[test]
fn test_article_summarize() {
let title = "Rust Ownership";
let author = "Alice";
let body = "Ownership is the key to Rust's safety guarantees.";
let article = Article::new(title, author, body);
let summary = article.summarize();
assert!(summary.contains("Rust Ownership"));
assert!(summary.contains("Alice"));
assert!(summary.contains(&body.len().to_string()));
}
#[test]
fn test_article_fields_accessible() {
let article = Article {
title: "Zero Cost",
author: "Bob",
body: "Abstractions without overhead.",
};
assert_eq!(article.title, "Zero Cost");
assert_eq!(article.author, "Bob");
}
#[test]
fn test_parser_remaining_advances() {
let input = "fn main() {}";
let mut parser = Parser::new(input);
assert_eq!(parser.remaining(), "fn main() {}");
parser.advance(3);
assert_eq!(parser.remaining(), "main() {}");
}
#[test]
fn test_parser_advance_clamps_to_end() {
let input = "hello";
let mut parser = Parser::new(input);
parser.advance(100);
assert_eq!(parser.remaining(), "");
}
#[test]
fn test_excerpt_debug_format() {
let text = "To be or not to be";
let exc = Excerpt::new(text, 7);
let debug = format!("{:?}", exc);
assert!(debug.contains("To be or not to be"));
assert!(debug.contains('7'));
}
}
Deep Comparison
OCaml vs Rust: Lifetimes in Structs
Side-by-Side Code
OCaml
(* OCaml structs own their data — no lifetime needed *)
type excerpt = { text : string; page : int }
let make_excerpt text page = { text; page }
let () =
let book = "Call me Ishmael. Some years ago..." in
let exc = make_excerpt (String.sub book 0 16) 1 in
assert (exc.text = "Call me Ishmael.");
Printf.printf "Excerpt p.%d: %s\n" exc.page exc.text
Rust (idiomatic — struct borrows data)
#[derive(Debug)]
struct Excerpt<'a> {
text: &'a str,
page: u32,
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let exc = Excerpt { text: &book[..16], page: 1 };
assert_eq!(exc.text, "Call me Ishmael.");
println!("Excerpt p.{}: {}", exc.page, exc.text);
// Compiler guarantees: exc cannot outlive book
}
Rust (functional — struct with multiple borrowed fields)
#[derive(Debug)]
struct Article<'a> {
title: &'a str,
author: &'a str,
body: &'a str,
}
impl<'a> Article<'a> {
fn summarize(&self) -> String {
format!("{} by {} ({} chars)", self.title, self.author, self.body.len())
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Struct with string | type t = { text: string } | struct T<'a> { text: &'a str } |
| Constructor | let make t p = { text=t; page=p } | fn new(text: &'a str, page: u32) -> Self |
| Method returning borrow | returns owned string | fn get(&self) -> &'a str |
| Lifetime parameter | implicit (GC manages) | explicit 'a on struct and impl |
Key Insights
string fields are heap-allocated and reference-counted by the GC — the struct owns a copy. In Rust, &'a str is a borrow: the struct holds a pointer without owning the data, so it must be proven valid.'a on Excerpt<'a> is Rust's way of encoding "this struct is only valid while the referenced str is alive." OCaml's GC makes this implicit — any live reference prevents collection.Excerpt to a scope where the referenced String is no longer live. OCaml, Java, and Python silently keep the source alive (or crash in C). Rust does this at zero runtime cost.&'a str is a slice into existing memory, creating an Excerpt never allocates. The OCaml equivalent (String.sub) copies bytes. Lifetimes make zero-copy safe without any runtime bookkeeping.struct Pair<'a, 'b>) when its fields borrow from different sources with potentially different scopes — something invisible in GC languages but explicit and precise in Rust.When to Use Each Style
**Use borrowing structs (&'a str) when:** you want zero-copy views into existing data — parsing, tokenizing, window operations, or any case where you'd otherwise copy a substring just to store it temporarily.
**Use owned structs (String) when:** the struct needs to outlive its source, be sent across threads, or stored in a collection that must own its data. The tradeoff is an allocation, but you gain unrestricted lifetime.
Exercises
ParseResult<'a> { input: &'a str, consumed: &'a str, remaining: &'a str } struct for a simple parser and implement a parse_word function.Config<'a> struct that borrows from a &'a str configuration file content and provides methods to look up values.Excerpt<'a> in a Vec that outlives the source string.