Rust Notes: Lifetime
A review of Lifetime in Rust.
Lifetimes in Rust
Lifetimes are one of the most important parts of Rust’s ownership system.
At first, lifetimes can feel strange because Rust introduces syntax such as:
1
&'a str
and:
1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
It is tempting to think that 'a represents a duration of time.
It does not.
A lifetime is primarily a relationship between references.
The compiler uses lifetimes to make sure that a reference never outlives the data it refers to.
The most important idea is:
A lifetime describes how long a reference is valid, and lifetime annotations describe relationships between references.
1. Why Do We Need Lifetimes?
Consider:
1
2
3
4
5
6
7
8
9
10
fn main() {
let r;
{
let x = 10;
r = &x;
}
println!("{r}");
}
This is invalid.
Why?
Because x is destroyed at the end of the inner scope:
1
2
3
4
5
6
{
let x = 10;
r = &x;
} ← x is dropped here
println!("{r}"); ← r would point to dead data
Rust prevents this at compile time.
Conceptually:
1
2
3
4
5
6
7
x:
|----------------|
scope ends
r:
|------------------------|
would outlive x
Therefore:
1
2
3
reference lifetime > data lifetime
↓
invalid
Rust rejects the program.
2. Lifetimes Are About References
Suppose:
1
2
3
let x = String::from("hello");
let r = &x;
There are two things involved:
1
2
3
4
5
x
└── owns String data
r
└── borrows x
The lifetime is associated with the reference:
1
r: &'a String
It describes the period during which r is valid as a reference to x.
It does not mean that x itself has an 'a lifetime.
3. Lifetime Is Not a Runtime Timer
This is one of the most important things to understand.
A lifetime is not:
1
2
3
5 seconds
10 seconds
100 milliseconds
Rust does not insert a timer into your program.
Instead, lifetimes are mainly a compile-time concept.
For example:
1
2
3
4
let x = String::from("hello");
let r = &x;
println!("{r}");
The compiler analyzes the scopes and determines that:
1
x is alive while r is used
Therefore the reference is valid.
4. Lifetime as a Region of Validity
It is useful to imagine a lifetime as a region in the program where a reference is valid.
For example:
1
2
3
4
5
6
let x = String::from("hello");
{
let r = &x;
println!("{r}");
}
Conceptually:
1
2
3
4
5
x lifetime:
|-----------------------------|
r lifetime:
|-------------|
The lifetime of r is shorter than the lifetime of x.
That is perfectly valid.
The important rule is:
A reference cannot be used outside the lifetime of the value it refers to.
5. The Borrow Checker Checks Lifetimes
The borrow checker uses ownership, borrowing, and lifetime analysis together.
For example:
1
2
3
4
5
6
7
fn main() {
let x = String::from("hello");
let r = &x;
println!("{r}");
}
The compiler knows:
1
2
3
x owns the String
r borrows x
r must not outlive x
Because x remains alive while r is used, the program is valid.
6. The Most Important Example
Consider:
1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
You might expect this to work.
But Rust cannot determine the lifetime of the returned reference.
Why?
Because the function can return either:
1
x
or:
1
y
The compiler needs to know the relationship between:
1
2
3
lifetime of x
lifetime of y
lifetime of returned reference
We express that relationship using a lifetime parameter.
7. Lifetime Parameters
The function becomes:
1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Here:
1
'a
is a lifetime parameter.
It says:
1
2
3
x is valid for lifetime 'a
y is valid for lifetime 'a
returned reference is valid for lifetime 'a
The important relationship is:
1
2
3
4
5
'a
│
├── x: &'a str
├── y: &'a str
└── return: &'a str
This does NOT mean:
x,y, and the return value live for exactly the same amount of time.
Instead, it expresses a relationship between them.
8. What Does 'a Actually Mean?
Consider:
1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
A useful way to read it is:
For some lifetime
'a, both input references are valid for'a, and the returned reference is also valid for'a.
The compiler chooses an appropriate lifetime when the function is called.
For example:
1
2
3
4
5
6
7
8
9
10
11
let result;
let s1 = String::from("long string");
{
let s2 = String::from("short");
result = longest(&s1, &s2);
println!("{result}");
}
The returned reference cannot remain valid after s2 disappears if the function might return s2.
Therefore the compiler effectively chooses a lifetime constrained by both references.
Conceptually:
1
2
3
4
5
6
7
8
s1:
|-----------------------------|
s2:
|-------------|
'a:
|-------------|
The lifetime must fit within the validity of both inputs.
9. Lifetime Parameters Do Not Extend Lifetimes
This is a common misunderstanding.
Writing:
1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
does NOT make x and y live longer.
It does not say:
Keep these strings alive for
'a.
Instead, it says:
The returned reference cannot be valid for longer than the lifetime represented by
'a.
The caller still controls how long the actual values live.
10. Lifetime Annotation Syntax
The basic syntax is:
1
'a
A lifetime name starts with an apostrophe:
1
2
3
'a
'b
'c
For example:
1
&'a str
A reference with a mutable borrow:
1
&'a mut String
A lifetime parameter:
1
2
fn foo<'a>() {
}
Multiple lifetime parameters:
1
2
fn foo<'a, 'b>() {
}
11. Lifetime on Immutable References
Syntax:
1
&'a T
Example:
1
2
3
fn print<'a>(value: &'a String) {
println!("{value}");
}
Here:
1
&'a String
means:
A reference to a
Stringthat is valid for lifetime'a.
12. Lifetime on Mutable References
Syntax:
1
&'a mut T
Example:
1
2
3
fn modify<'a>(value: &'a mut String) {
value.push_str(" world");
}
Here:
1
&'a mut String
is a mutable reference with lifetime 'a.
Remember that lifetime and mutability are separate concepts:
1
&'a T
means:
1
2
3
immutable reference
+
lifetime 'a
while:
1
&'a mut T
means:
1
2
3
mutable reference
+
lifetime 'a
13. Lifetime Parameters on Functions
Syntax:
1
2
3
fn function<'a>(x: &'a T) -> &'a T {
x
}
Example:
1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
x
}
This expresses:
1
2
3
4
5
input lifetime
↓
'a
↓
output lifetime
The returned reference is tied to the input reference.
14. Multiple Lifetimes
A function can have multiple lifetime parameters:
1
2
3
fn choose<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
Here:
1
2
'a → lifetime of x
'b → lifetime of y
The returned reference is tied specifically to 'a.
This makes sense because the function always returns x.
15. Multiple Lifetimes with Different Relationships
Consider:
1
2
3
4
5
6
fn first<'a, 'b>(
x: &'a str,
y: &'b str,
) -> &'a str {
x
}
The compiler knows:
1
return → x
Therefore:
1
return lifetime → 'a
It does not need to be related to 'b.
16. Lifetime Relationships
You can express relationships between lifetimes.
For example:
1
2
3
4
5
fn foo<'a, 'b>(x: &'a str, y: &'b str)
where
'a: 'b,
{
}
The syntax:
1
'a: 'b
means:
Lifetime
'aoutlives lifetime'b.
Conceptually:
1
2
3
4
5
'a:
|----------------------|
'b:
|-----------|
So:
1
'a: 'b
means 'a lasts at least as long as 'b.
17. Lifetime Bounds
A lifetime can also appear in a type bound.
For example:
1
T: 'a
This means:
Tcontains no references that are invalid before'a.
It is often easier to think of it as:
Tis valid for at least lifetime'a.
Example:
1
2
fn foo<'a, T: 'a>(value: T) {
}
18. where Lifetime Bounds
The same constraint can be written using where:
1
2
3
4
5
fn foo<'a, T>(value: T)
where
T: 'a,
{
}
This is often easier to read when there are many bounds.
19. Structs Containing References
A struct can contain a reference.
For example:
1
2
3
struct Important<'a> {
part: &'a str,
}
The lifetime parameter belongs to the struct:
1
2
3
Important<'a>
│
└── part: &'a str
Example:
1
2
3
4
5
let text = String::from("hello");
let important = Important {
part: &text,
};
The struct cannot outlive text.
20. Why Does the Struct Need a Lifetime?
Consider:
1
2
3
struct Important {
part: &str,
}
This does not tell Rust how the reference relates to the lifetime of the struct.
Instead:
1
2
3
struct Important<'a> {
part: &'a str,
}
means:
Important<'a>contains a reference that must remain valid for'a.
Therefore:
1
2
3
4
5
6
7
8
9
text
│
├── owns String
│
└── must outlive
↓
Important<'a>
│
└── part: &'a str
21. Multiple References in a Struct
You can have multiple lifetime parameters:
1
2
3
4
struct Pair<'a, 'b> {
first: &'a str,
second: &'b str,
}
Here:
1
2
first → 'a
second → 'b
The two references do not have to have the same lifetime.
22. One Lifetime for Multiple References
You can also use one lifetime:
1
2
3
4
struct Pair<'a> {
first: &'a str,
second: &'a str,
}
This expresses a relationship between the two references.
It means both references are valid for 'a.
Again, this does not mean they necessarily have identical real-world lifetimes.
23. Lifetime on impl
If a struct has a lifetime parameter:
1
2
3
struct Important<'a> {
part: &'a str,
}
its impl usually includes the lifetime:
1
2
3
4
5
impl<'a> Important<'a> {
fn part(&self) -> &str {
self.part
}
}
The two occurrences have different roles:
1
2
3
4
5
6
7
impl<'a>
↑
declares lifetime parameter
Important<'a>
↑
uses that lifetime parameter
24. Lifetime on Methods
Example:
1
2
3
4
5
impl<'a> Important<'a> {
fn get_part(&self) -> &'a str {
self.part
}
}
Here the returned reference uses the lifetime stored in the struct.
You can also introduce a separate lifetime for the method:
1
2
3
4
5
impl<'a> Important<'a> {
fn choose<'b>(&self, other: &'b str) -> &'b str {
other
}
}
Now:
1
2
'a → struct lifetime
'b → method lifetime
25. Lifetime Elision
You will often see functions without lifetime annotations:
1
2
3
fn first(s: &str) -> &str {
s
}
You might wonder:
Where is the lifetime?
Rust has lifetime elision rules.
The compiler can infer the lifetime relationship.
Conceptually, the compiler treats the function approximately like:
1
2
3
fn first<'a>(s: &'a str) -> &'a str {
s
}
Therefore you usually don’t need to write the lifetime explicitly.
26. Lifetime Elision Rule #1
Every reference parameter gets its own lifetime parameter.
For example:
1
2
fn foo(x: &str, y: &str) {
}
is conceptually treated like:
1
2
fn foo<'a, 'b>(x: &'a str, y: &'b str) {
}
Conceptually, not literally in the source code.
27. Lifetime Elision Rule #2
If there is exactly one input lifetime, that lifetime is assigned to all output references.
For example:
1
2
3
fn identity(x: &str) -> &str {
x
}
is approximately:
1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
x
}
This is why many simple functions don’t need lifetime annotations.
28. Lifetime Elision and Methods
Methods have an additional rule.
The lifetime of &self or &mut self is assigned to output references.
For example:
1
2
3
4
5
impl User {
fn name(&self) -> &str {
&self.name
}
}
Conceptually:
1
2
3
4
5
impl User {
fn name<'a>(&'a self) -> &'a str {
&self.name
}
}
This is extremely common in Rust.
29. Why longest Needs an Explicit Lifetime
Now compare:
1
2
3
fn first(x: &str) -> &str {
x
}
with:
1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
The first has one input reference.
Rust can infer:
1
input → output
The second has two input references:
1
2
x → ?
y → ?
and the output can come from either one.
Therefore Rust needs us to specify the relationship:
1
2
3
4
fn longest<'a>(
x: &'a str,
y: &'a str,
) -> &'a str
30. Lifetimes Do Not Determine Which Value Is Returned
This function:
1
2
3
4
fn longest<'a>(
x: &'a str,
y: &'a str,
) -> &'a str
does not mean:
Return the value that has lifetime
'a.
It means:
The returned reference is connected to the lifetime represented by
'a.
The actual returned value is determined by:
1
2
3
4
5
if x.len() > y.len() {
x
} else {
y
}
31. Lifetime Annotations Do Not Change Runtime Behavior
Adding:
1
'a
does not make your program:
- slower
- faster
- allocate memory
- create a runtime object
- keep a variable alive
- extend a variable’s scope
Lifetimes are primarily used by the compiler.
For example:
1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
x
}
does not create a runtime 'a object.
32. Lifetime and Ownership
Lifetimes make sense when combined with ownership.
Suppose:
1
2
3
4
fn get_name() -> &str {
let name = String::from("Amir");
&name
}
This is invalid.
Why?
1
2
3
4
5
6
7
8
9
name created
↓
&name returned
↓
function ends
↓
name dropped
↓
returned reference would point to dead data
Rust rejects it.
33. You Cannot Return a Reference to a Local Variable
This is one of the most common lifetime errors.
Invalid:
1
2
3
4
fn create() -> &String {
let s = String::from("hello");
&s
}
The correct solution is usually to return ownership:
1
2
3
fn create() -> String {
String::from("hello")
}
Then:
1
let s = create();
The caller owns the returned String.
This is an important Rust design principle:
If the created data needs to outlive the function, return ownership instead of a reference to local data.
34. Lifetime vs Ownership
Compare:
1
2
3
fn create() -> String {
String::from("hello")
}
with:
1
2
3
fn view<'a>(s: &'a String) -> &'a str {
&s[..]
}
The first transfers ownership:
1
2
3
4
5
function
│
└── creates String
│
└── ownership returned to caller
The second borrows:
1
2
3
caller owns String
│
└── function temporarily borrows it
Lifetimes describe the second relationship.
35. Lifetime vs Scope
These concepts are related but not identical.
Scope is about where a variable name is accessible:
1
2
3
4
{
let x = 10;
println!("{x}");
}
The scope of x is the block.
A lifetime describes how long a reference is valid.
For example:
1
2
3
4
5
6
let x = 10;
{
let r = &x;
println!("{r}");
}
Here:
1
2
3
4
5
x scope:
|------------------------|
r scope:
|----------|
The reference r has a shorter lifetime than x.
36. Non-Lexical Lifetimes
Modern Rust uses non-lexical lifetimes (NLL).
This means a borrow can end when the compiler determines that the reference is no longer used, rather than necessarily waiting until the end of the lexical scope.
For example:
1
2
3
4
5
6
7
let mut x = String::from("hello");
let r = &x;
println!("{r}");
x.push_str(" world");
The immutable borrow can end after the last use of r.
Conceptually:
1
2
3
4
5
6
7
x:
|-----------------------------|
r:
|--------|
↑
last use
After that point, x can be mutably borrowed.
This makes Rust’s borrowing system much more flexible.
37. Lifetime of a Mutable Reference
Consider:
1
2
3
4
5
6
7
let mut x = String::from("hello");
let r = &mut x;
r.push_str(" world");
println!("{r}");
While r is actively borrowed:
1
2
3
x
│
└── exclusively borrowed by r
You cannot simultaneously use x in a conflicting way.
This is related to the rule:
At any point, you can have either one mutable reference or any number of immutable references.
Lifetimes describe how long those borrows are active.
38. Lifetime and Borrowing
Think of a borrow as having a lifetime.
1
2
3
4
5
let x = String::from("hello");
let r = &x;
println!("{r}");
Conceptually:
1
2
3
4
5
6
x owns data
│
├─────────────── lifetime of x
│
└── r borrows x
└──────── lifetime of borrow
The borrow checker ensures the borrowing rules remain valid throughout the lifetime of the borrow.
39. 'static
One special lifetime is:
1
'static
It means the reference can remain valid for the entire duration of the program.
For example:
1
let s: &'static str = "hello";
String literals have 'static lifetime because they are embedded in the program’s binary.
40. String Literals and 'static
Consider:
1
let s = "hello";
The type is:
1
&'static str
conceptually.
The data exists for the entire program.
Therefore:
1
let s: &'static str = "hello";
is valid.
41. 'static Does Not Always Mean “Lives Forever”
There is another important use:
1
T: 'static
This does not necessarily mean:
T itself lives forever.
It means:
Tdoes not contain references that are shorter-lived than'static.
For example:
1
String
is 'static in this sense because it owns its data.
This can therefore satisfy:
1
T: 'static
even though a particular String value can be dropped normally.
42. 'static and Owned Types
Consider:
1
2
fn spawn<T: 'static>(value: T) {
}
A String can satisfy this:
1
2
3
let s = String::from("hello");
spawn(s);
because String owns its data.
But this may not:
1
2
3
4
let s = String::from("hello");
let r = &s;
spawn(r);
because r refers to s, which may not live for 'static.
The important distinction is:
1
2
3
4
5
6
7
String
└── owns data
└── no borrowed data
&String
└── borrows data
└── lifetime matters
43. Why Thread APIs Often Require 'static
You will often see:
1
T: Send + 'static
when working with threads.
For example:
1
2
3
thread::spawn(move || {
println!("{value}");
});
The spawned thread may continue running after the current scope ends.
Therefore Rust cannot allow the thread to hold a reference to a local variable that might disappear.
For example:
1
2
3
4
5
let s = String::from("hello");
thread::spawn(|| {
println!("{s}");
});
A normal spawned thread cannot safely assume that s remains alive.
Using:
1
move
moves ownership into the thread:
1
2
3
thread::spawn(move || {
println!("{s}");
});
Now the thread owns the String.
This is one reason why ownership is often preferable to borrowing for spawned threads.
44. Scoped Threads and Lifetimes
Scoped threads are different.
For example:
1
2
3
4
5
6
7
let s = String::from("hello");
thread::scope(|scope| {
scope.spawn(|| {
println!("{s}");
});
});
The scope guarantees that the thread finishes before the borrowed data can go away.
Conceptually:
1
2
3
4
5
6
7
8
s:
|---------------------------|
scope:
|-----------------|
thread:
|-----------|
The thread cannot outlive the scope.
Therefore borrowing s is safe.
This is a great example of lifetimes being used to express relationships between concurrency and data.
45. Lifetime Bounds and Threads
This distinction is important:
1
2
3
4
5
6
7
thread::spawn
↓
thread may outlive current scope
↓
borrowed references generally cannot escape
↓
often requires 'static
while:
1
2
3
4
5
thread::scope
↓
threads must finish before scope ends
↓
borrowing local data is possible
Lifetimes are therefore deeply connected to Rust’s concurrency safety.
46. Lifetime on Trait Objects
You may encounter:
1
Box<dyn Trait>
and:
1
Box<dyn Trait + 'a>
The latter explicitly specifies a lifetime bound.
Example:
1
Box<dyn Display + 'a>
means the trait object may contain references valid for 'a.
You can also see:
1
&'a dyn Trait
which means:
A reference with lifetime
'ato a trait object.
47. dyn Trait + 'static
A common form is:
1
Box<dyn Trait + 'static>
This means the object does not contain non-'static borrowed references.
For example:
1
2
3
struct User {
name: String,
}
can generally be stored in:
1
Box<dyn SomeTrait + 'static>
because String owns its data.
But a type containing a short-lived reference may not satisfy that bound.
48. Lifetime Bounds on Traits
You can define:
1
2
3
trait Trait<'a> {
fn get(&self) -> &'a str;
}
Here the trait itself has a lifetime parameter.
A type can implement it:
1
2
3
4
5
6
7
8
9
struct Data<'a> {
value: &'a str,
}
impl<'a> Trait<'a> for Data<'a> {
fn get(&self) -> &'a str {
self.value
}
}
This means the trait’s behavior is explicitly associated with lifetime 'a.
49. Generic Types and Lifetimes Together
You can have both type parameters and lifetime parameters:
1
2
3
struct Container<'a, T> {
value: &'a T,
}
Here:
1
2
'a → lifetime parameter
T → type parameter
Example:
1
2
3
4
5
let value = 10;
let container = Container {
value: &value,
};
50. Lifetime Bounds with Generic Types
Example:
1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
T: 'a,
{
}
This says:
1
T must be valid for at least 'a
This becomes especially relevant when T itself might contain references.
51. Higher-Ranked Trait Bounds
An advanced lifetime syntax is:
1
for<'a>
For example:
1
F: for<'a> Fn(&'a str) -> &'a str
This means:
Fmust work for every possible lifetime'a.
This is called a higher-ranked trait bound (HRTB).
It is useful when a function or closure must accept references with arbitrary lifetimes.
52. Understanding for<'a>
Consider:
1
for<'a> Fn(&'a str) -> &'a str
Read it as:
For every lifetime
'a, this function can accept a&'a strand return a&'a str.
Conceptually:
1
2
3
4
'a = lifetime #1 → works
'a = lifetime #2 → works
'a = lifetime #3 → works
...
It is not one particular lifetime.
It means all possible lifetimes.
53. Anonymous Lifetimes
Rust also has anonymous lifetime syntax in certain contexts:
1
'_
For example:
1
2
fn foo(x: &'_ str) {
}
'_ means:
Let the compiler infer this lifetime.
You may also encounter:
1
impl Iterator<Item = &'_ str>
where the lifetime is intentionally left for inference.
54. '_ vs 'a
Compare:
1
&'a str
and:
1
&'_ str
'a is a named lifetime parameter.
For example:
1
2
3
fn foo<'a>(x: &'a str) -> &'a str {
x
}
'_ is an anonymous lifetime:
1
2
fn foo(x: &'_ str) {
}
It tells Rust:
Infer this lifetime rather than giving it a name.
55. Lifetime Elision vs '_
These are related but not identical concepts.
This:
1
fn foo(x: &str) -> &str
uses lifetime elision rules.
This:
1
fn foo(x: &'_ str) -> &'_ str
explicitly uses anonymous lifetime syntax.
Usually, the first form is preferred when lifetime elision makes the relationship obvious.
56. Lifetime on References in Type Aliases
You can define a lifetime-aware type alias:
1
type StrRef<'a> = &'a str;
Then:
1
2
3
fn foo<'a>(value: StrRef<'a>) -> StrRef<'a> {
value
}
This is useful when a reference type appears repeatedly.
57. Lifetime on Type Aliases with Generic Types
For example:
1
type Ref<'a, T> = &'a T;
Then:
1
2
3
fn get<'a, T>(value: Ref<'a, T>) -> Ref<'a, T> {
value
}
58. Lifetime Parameters in Enums
Enums can contain references too:
1
2
3
4
enum Message<'a> {
Text(&'a str),
Number(i32),
}
Example:
1
2
3
let text = String::from("hello");
let message = Message::Text(&text);
The enum cannot outlive the referenced data.
59. Lifetime Parameters in Traits
Traits can have lifetime parameters:
1
2
3
trait Parser<'a> {
fn parse(&self, input: &'a str);
}
Implementation:
1
2
3
4
5
6
7
struct MyParser;
impl<'a> Parser<'a> for MyParser {
fn parse(&self, input: &'a str) {
println!("{input}");
}
}
60. Lifetime Parameters in Functions Returning References
Common pattern:
1
2
3
fn get<'a>(value: &'a String) -> &'a str {
value.as_str()
}
This means:
1
2
3
4
5
input:
&'a String
output:
&'a str
The returned reference cannot outlive the input reference.
61. Lifetime on self
You can explicitly name the lifetime of self:
1
2
3
4
5
impl User {
fn name<'a>(&'a self) -> &'a str {
&self.name
}
}
Usually this is unnecessary because Rust’s lifetime elision handles it:
1
2
3
4
5
impl User {
fn name(&self) -> &str {
&self.name
}
}
The second form is idiomatic.
62. Lifetime on &mut self
Similarly:
1
2
3
4
5
impl User {
fn name_mut<'a>(&'a mut self) -> &'a mut String {
&mut self.name
}
}
Usually written:
1
2
3
4
5
impl User {
fn name_mut(&mut self) -> &mut String {
&mut self.name
}
}
Again, lifetime elision handles the relationship.
63. Lifetime Subtyping
Rust allows lifetime relationships where one lifetime outlives another.
For example:
1
'a: 'b
means:
1
'a lasts at least as long as 'b
This is sometimes described as lifetime subtyping.
Conceptually:
1
2
3
4
5
'a
|-------------------------|
'b
|---------------|
The longer lifetime can be used where a shorter lifetime is required.
64. Reborrowing
Lifetimes also help explain reborrowing.
Consider:
1
2
3
4
5
6
7
let mut value = String::from("hello");
let r = &mut value;
r.push_str(" world");
r.push_str("!");
Rust can create shorter mutable borrows from r.
Conceptually:
1
2
3
4
5
6
7
8
original mutable borrow
|---------------------------|
first reborrow:
|------|
second reborrow:
|------|
This is one reason Rust can safely support complex mutable-reference code.
65. Lifetime and &mut
A mutable reference is exclusive for its active lifetime.
For example:
1
2
3
let mut x = 10;
let r = &mut x;
While that borrow is active, another conflicting borrow cannot be used.
This:
1
2
let r1 = &mut x;
let r2 = &mut x;
is invalid if both are simultaneously active.
The borrow checker tracks the relevant lifetimes.
66. Lifetimes and Interior Mutability
Types such as:
1
RefCell<T>
allow borrowing rules to be checked at runtime rather than compile time.
For example:
1
2
3
let value = RefCell::new(10);
let borrow = value.borrow();
The reference returned by the borrow still has a lifetime.
The difference is that RefCell manages the borrowing rules dynamically.
So:
1
2
3
4
5
6
7
normal reference
↓
borrow rules checked at compile time
RefCell
↓
borrow rules checked at runtime
Lifetimes remain part of the type system even though some borrow checking is moved to runtime.
67. Lifetimes and Rc
Rc<T> provides shared ownership.
For example:
1
2
3
4
5
6
use std::rc::Rc;
let value = Rc::new(String::from("hello"));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
There is no lifetime parameter required for the Rc itself because Rc owns the data.
Conceptually:
1
2
3
4
Rc
├── owner #1
├── owner #2
└── data
The data remains alive while at least one Rc exists.
This is different from:
1
&'a T
which is a borrowed reference and therefore needs lifetime reasoning.
68. Lifetimes and Box
Similarly:
1
Box<T>
owns its value.
For example:
1
let value = Box::new(String::from("hello"));
Box<T> does not need a lifetime parameter for the owned value.
Compare:
1
2
3
4
5
6
Box<T>
└── owns T
&'a T
└── borrows T
└── lifetime required
This distinction is fundamental.
69. Owned vs Borrowed Data
A useful rule:
1
2
3
4
5
6
7
8
9
10
Owned:
String
Vec<T>
Box<T>
Rc<T>
Arc<T>
Borrowed:
&T
&mut T
Borrowed data requires lifetime reasoning.
Owned data generally does not require explicit lifetime parameters because ownership determines how long the data remains valid.
70. Lifetimes and Arc
Arc<T> is shared ownership across threads.
1
2
3
4
5
6
use std::sync::Arc;
let value = Arc::new(String::from("hello"));
let a = Arc::clone(&value);
let b = Arc::clone(&value);
Again, the Arc owns the data.
This is different from sharing:
1
&'a T
The Arc keeps the data alive through ownership.
71. Lifetimes and Send / Sync
Lifetimes also appear indirectly when working with concurrency.
For example, a spawned thread often requires:
1
Send + 'static
Send answers:
Can this value be transferred to another thread?
'static answers:
Does this value contain references that could become invalid before the thread is finished?
These are different properties.
For example:
1
String
can be Send and 'static.
But:
1
&String
may have a short lifetime and therefore cannot necessarily be sent to an independently running thread.
72. A Lifetime Does Not Mean Ownership
This is especially important.
Consider:
1
2
3
let s = String::from("hello");
let r = &s;
r has a lifetime.
But r does not own s.
The ownership relationship is:
1
2
3
4
5
s
└── owns String
r
└── borrows s
The lifetime describes:
1
how long r can safely be used
It does not transfer ownership.
73. Can We Talk About Ownership of &T?
Yes, but carefully.
A reference itself is a value.
Therefore:
1
2
let r1 = &s;
let r2 = r1;
moves or copies the reference value, depending on its type semantics.
But this does not mean:
1
r1 owns s
It means:
1
r1 contains a reference to s
References are themselves values, but they do not own the data they point to.
This distinction is important when reasoning about Send, Sync, ownership, and lifetimes.
74. Lifetime of a Reference vs Lifetime of Referenced Data
Suppose:
1
2
3
4
5
6
let s = String::from("hello");
{
let r = &s;
println!("{r}");
}
There are two different lifetimes:
1
2
3
4
5
s:
|-----------------------------|
r:
|-----------|
The data’s lifetime is longer.
The reference’s lifetime is shorter.
This is normal.
A lifetime parameter does not mean:
The data itself lives exactly for
'a.
It means:
The reference is valid for
'a.
75. Lifetime of a Reference Can Be Shorter Than the Named Lifetime
When you write:
1
2
fn foo<'a>(x: &'a str) {
}
'a represents a lifetime chosen by the compiler for that call.
The actual borrow may be shorter than the entire lifetime of the underlying object.
This is why lifetime annotations should be understood as constraints and relationships, not timers.
76. Common Lifetime Syntax Cheat Sheet
Reference with lifetime
1
&'a T
Mutable reference with lifetime
1
&'a mut T
Function lifetime parameter
1
2
fn foo<'a>() {
}
Multiple lifetime parameters
1
2
fn foo<'a, 'b>() {
}
Input and output relationship
1
2
3
fn foo<'a>(x: &'a T) -> &'a T {
x
}
Multiple inputs
1
2
3
4
5
fn foo<'a, 'b>(
x: &'a T,
y: &'b T,
) {
}
Struct with lifetime
1
2
3
struct Foo<'a> {
value: &'a T,
}
Multiple lifetimes in a struct
1
2
3
4
struct Foo<'a, 'b> {
first: &'a T,
second: &'b T,
}
impl with lifetime
1
2
impl<'a> Foo<'a> {
}
Lifetime bound
1
T: 'a
Lifetime outlives relationship
1
'a: 'b
where lifetime bound
1
2
where
T: 'a,
Trait with lifetime
1
2
trait Foo<'a> {
}
Trait implementation with lifetime
1
2
impl<'a> Foo<'a> for Bar<'a> {
}
Trait object with lifetime
1
dyn Trait + 'a
Reference to trait object
1
&'a dyn Trait
Boxed trait object
1
Box<dyn Trait + 'a>
Static lifetime
1
'static
Anonymous lifetime
1
'_
Higher-ranked trait bound
1
for<'a>
Example:
1
F: for<'a> Fn(&'a str) -> &'a str
77. A Practical Decision Process
When you see a lifetime error, ask these questions in order.
1. Who owns the data?
1
2
3
4
5
String?
Vec?
Box?
Rc?
Arc?
If something owns the data, determine whether ownership can be returned or moved instead of borrowing.
2. Who is borrowing?
Look for:
1
2
&T
&mut T
3. How long does the owner live?
Find the scope where the owned value is valid.
4. How long does the reference need to live?
Find where the reference is used.
5. Does the reference outlive its owner?
If yes, the program is invalid.
6. Does the function return a reference?
If yes, ask:
Which input does the returned reference come from?
7. Can lifetime elision express the relationship?
If yes, you probably don’t need explicit lifetime syntax.
78. The Most Common Lifetime Pattern
You will encounter this pattern constantly:
1
2
3
fn get<'a>(value: &'a T) -> &'a U {
// ...
}
The key idea is:
1
2
3
4
5
6
7
8
9
input borrow
│
│ 'a
▼
function
│
│ 'a
▼
output borrow
The output cannot outlive the input reference.
79. Another Common Pattern: Struct Holding a Reference
1
2
3
struct Parser<'a> {
input: &'a str,
}
Think:
1
2
3
4
5
Parser<'a>
│
└── borrows input
│
└── input must remain alive
This is common in parsers and zero-copy data structures.
80. Zero-Copy Programming
Lifetimes are especially important in zero-copy programming.
Instead of creating a new String:
1
2
3
fn get_name(input: &str) -> String {
input[..5].to_string()
}
you can return a slice:
1
2
3
fn get_name<'a>(input: &'a str) -> &'a str {
&input[..5]
}
Now the result borrows the original data.
Conceptually:
1
2
3
4
5
6
7
8
9
input:
"Amir Nikpour"
│
└────────────── owned data
result:
&input[..5]
│
└── points into existing data
No new string allocation is necessary.
This is one of the major practical benefits of lifetimes.
81. Lifetimes Enable Safe Zero-Copy APIs
Consider:
1
2
3
struct User<'a> {
name: &'a str,
}
The User does not own the name.
It borrows it:
1
2
3
4
5
6
7
8
9
input buffer
┌──────────────────────────┐
│ Amir Nikpour │
└──────────────────────────┘
▲
│
│ &'a str
│
User<'a>
Rust ensures the User cannot outlive the input buffer.
This allows efficient zero-copy designs while maintaining memory safety.
82. Lifetime Errors Are Often Design Errors
When Rust gives you a lifetime error, don’t immediately try to “fight the borrow checker.”
First ask:
What ownership relationship am I trying to express?
For example, if you write:
1
2
3
4
fn create() -> &str {
let s = String::from("hello");
&s
}
adding random lifetime annotations cannot fix the problem.
The actual problem is:
1
2
3
4
5
6
7
function owns data
↓
function ends
↓
data destroyed
↓
reference would become invalid
The correct design is probably:
1
2
3
fn create() -> String {
String::from("hello")
}
83. When Should You Add an Explicit Lifetime?
Do not add lifetime annotations everywhere.
Usually add them when:
- a function returns a reference
- multiple input references are involved
- a struct stores references
- a trait has lifetime-dependent behavior
- you need to express an outlives relationship
- the compiler cannot infer the relationship
- you are implementing advanced generic abstractions
For simple borrowing:
1
2
3
fn print(value: &str) {
println!("{value}");
}
is better than:
1
2
3
fn print<'a>(value: &'a str) {
println!("{value}");
}
The explicit lifetime adds no useful information here.
84. When Should You Avoid Lifetimes?
Sometimes the easiest solution is to use ownership.
Instead of:
1
2
3
struct User<'a> {
name: &'a str,
}
you can use:
1
2
3
struct User {
name: String,
}
Now:
1
2
User
└── owns name
rather than:
1
2
User<'a>
└── borrows name
Owned data often makes APIs simpler.
Borrowed data can be useful when:
- performance matters
- allocations should be avoided
- data is naturally borrowed
- zero-copy parsing is desired
- the lifetime relationship is clear
85. Lifetimes Are Part of Rust’s Memory Safety Model
Rust’s memory safety comes from several concepts working together:
1
2
3
4
5
6
7
8
9
Ownership
+
Borrowing
+
Lifetimes
+
Type system
+
Borrow checker
Ownership answers:
Who is responsible for the data?
Borrowing answers:
Who can temporarily access the data?
Lifetimes answer:
How long is that access valid?
The borrow checker uses these rules to prevent problems such as:
- use-after-free
- dangling references
- invalid mutable aliases
- references to destroyed stack variables
86. Lifetimes and Dangling References
Languages with manual memory management can accidentally create:
1
pointer → freed memory
Rust prevents this.
For example:
1
2
3
4
fn bad() -> &String {
let value = String::from("hello");
&value
}
The compiler detects that:
1
2
3
4
5
value dies
↓
returned reference would remain
↓
dangling reference
Therefore the program is rejected before it runs.
87. A Good Mental Model
Don’t think:
'ameans this object lives for some fixed amount of time.
Think:
'ais a name for a region of validity of a reference, and annotations tell Rust how different references are related.
For example:
1
2
3
4
fn longest<'a>(
x: &'a str,
y: &'a str,
) -> &'a str
Think:
1
2
3
4
5
6
7
8
9
10
'a
│
┌─────┴─────┐
│ │
x: &'a y: &'a
│ │
└─────┬─────┘
│
returned
&'a str
The annotation tells the compiler:
The returned reference is tied to the lifetime of the input references.
88. Final Mental Model
The easiest way to remember lifetimes is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ownership
│
└── Who owns the data?
Borrowing
│
└── Who temporarily accesses it?
Lifetime
│
└── How long is that reference valid?
Borrow checker
│
└── Are all these relationships safe?
And when reading Rust code:
1
'a
think:
“A named lifetime relationship.”
1
&'a T
think:
“A reference to
Tvalid for'a.”
1
fn foo<'a>(x: &'a T) -> &'a T
think:
“The returned reference is tied to the input reference.”
1
'a: 'b
think:
”
'aoutlives'b.”
1
T: 'a
think:
“
Tis valid for at least'a.”
1
'static
think:
“Valid for the entire program, or, for a type bound, contains no shorter-lived borrowed references.”
1
'_
think:
“Let the compiler infer this lifetime.”
1
for<'a>
think:
“For every possible lifetime
'a.”
The central idea is:
Lifetimes do not control how long data lives. Ownership and scope determine that. Lifetimes describe and constrain how long references can safely be used in relation to the data they borrow.
Once you see lifetimes as relationships between references rather than timers, most lifetime syntax becomes much easier to understand.