79541251

Date: 2025-03-28 11:12:26
Score: 0.5
Natty:
Report link

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.

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:

  1. Each reference parameter in a function gets a lifetime parameter.

  2. If there's only one input lifetime parameter, it's assigned to all output lifetime parameters.

  3. 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:

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):

  1. Immutable references are covariant with respect to lifetimes: meaning longer lifetime references can be "shrunk" to shorter lifetime references.

  2. 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.

Reasons:
  • Blacklisted phrase (1): stackoverflow
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Howard