Working Code
Borrowing lets you use values without copying them. But holding on to a borrowed value too long causes problems.
fn main() {
let result;
{
let text = String::from("hello");
result = &text;
} // text is dropped here!
// println!("{result}"); // result still points to the dropped value!
}
Uncomment the last line and you get an error. text was dropped inside the braces, but result still points to it. This is a dangling reference — the same danger from Lesson 08!
Rust catches this at compile time. How? By tracking lifetimes.
"Why?" — Loan Expiry Dates
Think of lifetimes as loan expiry dates:
- When you borrow a book from the library, it comes with an expiry date
- You can read freely within that window, but must return it when it expires
- Borrowed values (references) work the same — they're valid only while the original is alive
fn main() {
let text = String::from("hello"); // text's lifetime begins
let r = &text; // r borrows text
println!("{r}"); // OK! text is alive
} // both text and r end
This code is fine. r borrows text, and r is used only while text is alive.
Returning References From Functions
The problem arises when a function returns a reference:
fn longer(s1: &str, s2: &str) -> &str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:35
|
1 | fn longer(s1: &str, s2: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but the signature does not say whether it is borrowed
from `s1` or `s2`
What it means: "The return value is borrowed, but I can't tell if it comes from s1 or s2."
Fix: Use a lifetime annotation to say "the return value lives as long as the inputs."
Think from the compiler's perspective: the function might return s1 or s2. It can't determine how long the returned reference stays valid, so it raises an error.
Lifetime Annotation 'a
Lifetime annotations tell the compiler "these references are valid for the same duration."
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() {
s1
} else {
s2
}
}
fn main() {
let a = String::from("hello");
let b = String::from("hi");
let result = longer(&a, &b);
println!("Longer: {result}");
}
'a is a lifetime parameter. It means "the return value's lifetime equals the shorter of s1 and s2."
Lifetime annotations might look intimidating, but don't worry. Just remember the core idea:
Lifetime annotations are hints that tell the compiler how long references stay valid.
Lifetime Elision — You Usually Don't Need Them
In practice, you rarely have to write lifetimes explicitly. The Rust compiler applies lifetime elision rules to infer them automatically.
// No lifetime annotation needed here
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
When there's only one input reference, the compiler infers "the output lifetime = the input lifetime." You only need explicit annotations when there are multiple input references.
Rest assured — in most code, you'll rarely write lifetimes by hand. The compiler handles it!
Putting References in Structs
In Lesson 12 you got an error trying to put &str in a struct. Now you know why:
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let text = String::from("Learning never exhausts the mind");
let author = String::from("Da Vinci");
let quote = Quote {
text: &text,
author: &author,
};
println!("\"{}\" - {}", quote.text, quote.author);
}
When a struct holds references, you must specify their lifetime. It means "Quote holds references that are valid for lifetime 'a."
In most cases, using String in structs is simpler and safer. Use &str only when you truly need it.
Learn More: 'static Lifetime
'static is a lifetime that lasts for the entire program. String literals are the classic example:
fn main() {
let s: &'static str = "This string lives for the entire program";
println!("{s}");
}
String literals are embedded in the program binary, so they're valid for the program's entire runtime. That's why they have the 'static lifetime.
Exercise 1. Explain why this code doesn't compile, then fix it.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{r}");
}
Hint: Compare the lifetime of x with when r is used.
Exercise 2. Add lifetime annotations to make this function compile.
fn first_or_default(s: &str, default: &str) -> &str {
if s.is_empty() {
default
} else {
s
}
}
fn main() {
let result = first_or_default("hello", "default");
println!("{result}");
}
Hint: The return value could be either s or default, so you need to link both inputs' lifetimes.
Q1. What is a lifetime?
- A) The amount of memory a variable occupies
- B) The scope during which a reference is valid
- C) The total execution time of a program
- D) The number of times a function is called
Q2. What is the role of 'a in this code?
fn pick<'a>(s1: &'a str, s2: &'a str) -> &'a str {
s1
}
- A) It defines a new type
- B) It indicates that the return value's lifetime is tied to the input references
- C) It manually manages memory
- D) It makes the function generic
Q3. When do you most commonly need to write lifetimes explicitly?
- A) When declaring variables
- B) When using
println! - C) When a function returns one of several input references
- D) When using integers