Here is another example to illustrate the lifetime of mutable references:
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// this fails because inmutable/mutable borrow
// but Interface should be already dropped here and the borrow released
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
Let's understand what we're constraining here. I recommend reading the official Rust book in its original English version. A lifetime represents the valid interval of a variable from creation to destruction.
In function parameter and return value constraints, variables x, y, z with the same lifetime 'a means there exists an interval 'a where x, y, z are all valid. This interval starts at the latest creation point of all variables and ends at the earliest destruction point.
Lifetime annotations in struct definitions indicate reference lifetimes, showing that the struct's lifetime must be within the referenced data's lifetime, also determined by finding this interval.
struct ImportantExcerpt<'a,'b,'c,'d,'e> {
part1: &'a str,
part2: &'b str,
part3: &'c str,
part4: &'d Me<'e>
}
struct Me<'a>{
part5: &'a mut Me<>,
}
According to the rules, the struct's lifetime is within the intersection of 'a through 'e. The potential lifetime 'd should also be within 'e, due to the constraints of the Me struct. There are no constraints between 'a, 'b, and 'c.
Lifetimes also have 3 elision rules. Note the terms input lifetimes and output lifetimes, referring to parameters and return values:
Each reference parameter in a function gets a lifetime parameter.
If there's only one input lifetime parameter, it's assigned to all output lifetime parameters.
The first parameter of a method, unless it's a new method, is generally &self
, &mut self
, self
, Box<self>
, Rc<self>
, etc. If it's &self
or &mut self
, its lifetime is assigned to all parameters.
Circular References https://course.rs/compiler/fight-with-compiler/lifetime/too-long1.html
The most unique aspect of this example is that we typically explain lifetimes in functions, structs, and methods separately. But here it's complicated with various constraints. Consider combined approaches like:
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface<'a> {
// ...
}
}
In the method &'a mut self
, the parameter is actually &'a mut List<'a>
. Because methods can be written as functions:
pub fn get_interface(&'a mut List<'a> input) -> Interface<'a> {
// ...
}
This approach combines struct lifetime constraints with function parameter lifetime constraints. It's called "lifetime binding" or "lifetime entanglement", where the compiler thinks this borrow never ends. It means:
The mutable borrow of self
will last until 'a
ends, not just until the method call ends. After the method ends, self is still borrowed. So throughout the entire lifetime 'a
, you cannot borrow this struct instance again.
The lifetime of the returned value is bound to self's lifetime, meaning they are valid and invalid simultaneously, even if the Interface
object is dropped.
When writing code, follow this principle: avoid reference cycles like &'a mut SomeType<'a>
. Change it to:
struct Interface<'b,'a> {
manager: &'b mut Manager<'a>
}
impl<'a> List<'a> {
pub fn get_interface(&mut self) -> Interface {
// ...
}
}
But according to rule 3, the lifetime of &mut self
is assigned to Interface
. The compiler's inferred annotation would be:
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b,'b> {
Interface {
manager: &mut self.manager
}
}
}
However, since the created Interface uses &mut self.manager<'a>
with lifetime 'a
, the compiler thinks the return value is Interface<'_,'a>
. This means the compiler's inference is incorrect, and we need to manually supplement:
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b,'a> {
Interface {
manager: &mut self.manager
}
}
}
Final Recommendations
I emphasize again, avoid mutable reference cycles like &mut'a SomeType<'a>
, because of how Rust types behave with lifetime parameters (variance):
Immutable references are covariant with respect to lifetimes: meaning longer lifetime references can be "shrunk" to shorter lifetime references.
Mutable references are invariant with respect to lifetimes: meaning no lifetime conversions are allowed, they must match exactly.
Immutable circular references are acceptable. For example, modify struct Interface:
struct Interface<'a> {
manager: &'a Manager<'a>
}
// ... rest of the code with immutable references
Also avoid mutable reference propagation like & SomeType<'a>->AnotherType<'a,'a'>
, because you need to manually match lifetimes. Use Rc instead of struggling with this; the performance cost isn't significant.
A third example of mutable circular references: m1's type is &'_ mut S<'a>
, but later m1 creates a self-reference, making m1's type &'a mut S<'a>
, which violates our rule about avoiding such patterns:
struct S<'a> {
i: i32,
r: &'a i32,
}
let mut s = S { i: 0, r: &0 };
let m1 = &mut s;
m1.r = &m1.i; // s.r now references s.i, creating a self-reference
References: https://stackoverflow.com/a/66253247/18309513 and the answers it mentions.