17.3Lifetimes in structs
Lesson 10.8 hit a wall: trying to put a &str in a struct produced E0106, "missing lifetime specifier," and the advice was "use owned fields until chapter 17." This is chapter 17, and the IOU comes due. A struct can hold a reference, you just have to declare a lifetime parameter, and this lesson shows how, what it guarantees, and why owned fields are still usually the better call.
The problem, revisited
Here's the struct that failed in lesson 10.8:
struct Excerpt {
text: &str,
}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 Excerpt<'a> {
2 ~ text: &'a str,
|
The compiler's worry is the dangling-reference rule (lesson 9.5) applied to storage. If an Excerpt holds a &str, that borrowed text must stay alive for at least as long as the Excerpt does, or the struct would outlive its own data and hold a dangling reference. The compiler can't just assume that relationship; it needs you to state it with a lifetime parameter.
Declaring a lifetime on a struct
The fix is the one the compiler suggested: give the struct a lifetime parameter, declared in angle brackets after the name (just like a generic type parameter, chapter 15), and attach it to the reference field:
struct Excerpt<'a> {
text: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("no sentence");
let excerpt = Excerpt { text: first_sentence };
println!("{}", excerpt.text);
}Call me Ishmael
struct Excerpt<'a> reads "an Excerpt is tied to some lifetime 'a," and text: &'a str says "the text field borrows for that lifetime." Together they declare the rule the compiler wanted: an Excerpt cannot outlive the text it borrows. In main, excerpt borrows from novel, and because novel lives for the rest of main, the borrow is valid, so it compiles. If novel were dropped while excerpt still lived, the borrow checker would refuse, exactly as it would for a plain reference that outlived its value.
The annotation does the same job it did for functions (lesson 17.2): it describes the relationship (the struct borrows, and is bounded by the borrow's lifetime) so the compiler can enforce it. It doesn't extend anything.
Methods on a struct with a lifetime
When you write impl blocks for such a struct, the lifetime parameter comes along, just like a generic parameter does (lesson 15.4):
impl<'a> Excerpt<'a> {
fn announce(&self, note: &str) -> &str {
println!("Attention: {note}");
self.text
}
}
impl<'a> Excerpt<'a> declares the lifetime for the impl block, mirroring impl<T> Point<T> from chapter 15. The method bodies mostly don't need extra lifetime annotations thanks to elision (the next lesson), which is why announce returns &str without a written lifetime. The pattern to recognize: a struct holding a reference threads its lifetime parameter through its definition and its impl blocks, the same way a generic struct threads its type parameter.
Key insight
A lifetime on a struct ties the struct's validity to its borrowed data: the struct may not outlive what it borrows. This is the borrow checker's "references must be valid" rule (lesson 9.5) extended to data stored inside a type, not just passed through a function. The annotation makes that relationship explicit so the compiler can check every place an Excerpt is created and used, refusing any where the borrowed text would die first.
Why owned fields are still the default
You can now store references in structs, but lesson 10.8's advice still stands: prefer owned fields (String, not &str) unless you have a specific reason not to. A struct with a lifetime parameter is constrained: it can't outlive its borrowed data, which means it can't be returned from a function that created the data locally, can't be stored just anywhere, and threads <'a> through every signature that mentions it. An owning struct has none of those constraints; it carries its own data and goes wherever it likes.
So when should a struct hold a reference? When the borrow is genuinely temporary and the performance of avoiding a copy matters: a parser that holds a view into a source string it's processing, a struct that exists only briefly within a function that owns the underlying data. These are real and useful, but they're intermediate patterns. For most types you design, owned fields are simpler, more flexible, and free of lifetime annotations entirely.
Best practice
Default to owned fields in structs. Reach for a reference field (and its lifetime parameter) only when you specifically want to avoid copying and the struct's lifetime is naturally bounded, like a short-lived view into data owned elsewhere. If adding <'a> to a struct makes its uses awkward (it can't be returned, stored, or moved where you need), that's the signal to switch the field to owned data instead.
Quiz time
Question #1
Why does a struct with a &str field require a lifetime parameter?
Show solution
Because the struct stores a borrow, and the borrowed data must stay alive at least as long as the struct, or the struct would hold a dangling reference (lesson 9.5). The lifetime parameter (<'a>, with text: &'a str) declares that relationship so the compiler can enforce that no instance outlives the data it borrows. Without it, E0106.
Question #2
What does struct Excerpt<'a> { text: &'a str } guarantee about an Excerpt instance?
Show solution
That the Excerpt cannot outlive the text it borrows: the instance is valid only for as long as the borrowed &str is valid. Create one borrowing from a String, and the borrow checker ensures the Excerpt is gone before (or when) that String is dropped.
Question #3
You're designing a Config struct that the program holds for its whole run. Should its name field be String or &str? Why?
Show solution
String (owned). A long-lived, freely-stored struct should own its data so it carries no lifetime constraint and can live and move anywhere. A &'a str field would tie Config's lifetime to some borrowed source and thread <'a> through everything, with no benefit here. Reference fields are for short-lived views where avoiding a copy matters, not for a config held for the whole program.
You've now written lifetimes on functions and structs. But you've also written hundreds of reference-taking functions without them, which means the compiler was filling them in. The next lesson reveals the three elision rules that let it do that, and why you'll rarely write 'a in practice.