The excellent online book Rust By Practice in the Lifetime chapter has a difficult exercise called... A difficult exercise. I struggled with it for a while, then looked at the solution and - I still did not get it. Not until I read the chapter on ownership from The Rustonomicon. Let's dive into the problem and describe the solution in detail.
First we need to go over the code in the exercise and explain what it does. It defines a structure called List
; that structure has nothing to do with an actual list, it is just a container for a single object. The object it holds is an instance of a Manager
structure. The List
gives mutable access to the Manager
but not directly - instead it creates container called Interface
that holds mutable reference to the Manager
. This is done in get_interface
method that returns Interface
. The only method of Interface
is noop(self)
.
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(); // <--- Interface created and used here.
println!("Interface should be dropped here and the borrow released");
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
When trying to run this code, rust fails to compile it with the following error:
According to rust's lifetime rules, borrow is valid from it's declaration to it's last use. Last use of Interface
is when noop()
is called in the main
function. It should be similar to this example:
fn main() {
let mut x = 1;
let y = &mut x;
// print!("{x}"); <-- this will not compile,
// x is mutably borrowed so print cannot borrow it again
*y = 42; // last use of y
print!("{x}"); // x no longer borrowed, can be used
}
So why it does not work? Let's analyze reference lifetimes in the main
function. I am using similar notation that the The Rustonomicon is using, showing scopes of each lifetime. Note this is not valid rust syntax!
fn main() {
'a {
let mut list = List { // list has lifetime 'a
manager: Manager {
text: "hello"
}
};
'b {
list.get_interface().noop(); // Interface uses lifetime 'b
}
// 'b is out of scope
println!("Interface should be dropped here and the borrow released");
use_list(&list);
} // lifetime 'a ends here
}
But... this still does not explain the issue. 'b
lifetime is short and ends before use_list
is called. To understand what happens we need to look closer at get_interface()
method declaration:
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager // what is lifetime of this?
}
}
}
Rust lifetime elision allows us to skip some lifetime parameters. If we make all lifetimes explicit, the declaration will look as below
impl<'a> List<'a> {
pub fn get_interface(&'a mut self) -> Interface<'a> {
Interface::<'a> {
manager: &'a mut self.manager
}
}
}
The lifetime 'a
from above snippet will be the same as the lifetime 'a
from main
function since we are calling this method on list
object in main
. This means Interface.manager
will hold reference with 'a
lifetime. And if you look inside Manager
struct it uses this lifetime in text
reference. Recall the implementation of Interface
:
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
Even if we stop using Interface
, the manager
reference is still alive as it is using 'a
lifetime, which in get_interface
will be bound to List
's lifetime also called 'a
here.
How do we fix this? We need to untangle Interface
's lifetime from Manager
's lifetime. Let's introduce separate lifetime 'b
for Interface's reference. I will consistently use 'a
and 'b
which should make it easier to track which lifetime is which.
struct Interface<'b, 'a> {
manager: &'b mut Manager<'a> // reference with lifetime 'b
// holding Manager with lifetime 'a
}
impl<'b, 'a> Interface<'b, 'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
We could bind the lifetimes declaring 'a: 'b
to indicate 'a
outlives 'b
, but this is not necessary in this example.
The last thing remaining is to create Interface
instance with proper lifetimes. List
uses only one lifetime 'a
, but we can extend get_interface
method to take another generic lifetime parameter 'b
.
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a> {
Interface {
manager: &mut self.manager
}
}
}
Now we explicitly told rust to use 'b
lifetime for reference inside Interface
. Once usage is done, rust can release the reference and list
becomes available.
The Rustonomicon describes itself as the awful details that you need to understand when writing Unsafe Rust programs. In this example we did not do any unsafe rust, yet the knowledge from that book was very helpful. I recommend reading The Rustonomicon even if you only plan to play safe.
Sources:
Top comments (0)