Mutable borrow of self doesn't change to immut

2020-02-05 13:33发布

问题:

This code fails the dreaded borrow checker (playground):

struct Data {
    a: i32,
    b: i32,
    c: i32,
}

impl Data {
    fn reference_to_a(&mut self) -> &i32 {
        self.c = 1;
        &self.a
    }
    fn get_b(&self) -> i32 {
        self.b
    }
}

fn main() {
    let mut dat = Data{ a: 1, b: 2, c: 3 };
    let aref = dat.reference_to_a();
    println!("{}", dat.get_b());
}

Error:

error[E0502]: cannot borrow `dat` as immutable because it is also borrowed as mutable
  --> <anon>:19:20
   |
18 |     let aref = dat.reference_to_a();
   |                --- mutable borrow occurs here
19 |     println!("{}", dat.get_b());
   |                    ^^^ immutable borrow occurs here
20 | }
   | - mutable borrow ends here

Can someone explain exactly why this is? I would have thought that the mutable borrow of dat is converted into an immutable one when reference_to_a() returns, because that function only returns an immutable reference. Is the borrow checker just not clever enough yet? Is this planned? Is there a way around it?

回答1:

Lifetimes are separate from whether a reference is mutable or not. Working through the code:

fn reference_to_a(&mut self) -> &i32

Although the lifetimes have been elided, this is equivalent to:

fn reference_to_a<'a>(&'a mut self) -> &'a i32

i.e. the input and output lifetimes are the same. That's the only way to assign lifetimes to a function like this (unless it returned an &'static reference to global data), since you can't make up the output lifetime from nothing.

That means that if you keep the return value alive by saving it in a variable, you're keeping the &mut self alive too.

Another way of thinking about it is that the &i32 is a sub-borrow of &mut self, so is only valid until that expires.

As @aSpex points out, this is covered in the nomicon.



回答2:

Why is this an error: While a more precise explanation was already given by @Chris some 2.5 years ago, you can read fn reference_to_a(&mut self) -> &i32 as a declaration that:

“I want to exclusively borrow self, then return a shared/immutable reference which lives as long as the original exclusive borrow” (source)

Apparently it can even prevent me from shooting myself in the foot.

Is the borrow checker just not clever enough yet? Is this planned?

There's still no way to express "I want to exclusively borrow self for the duration of the call, and return a shared reference with a separate lifetime". It is mentioned in the nomicon as @aSpex pointed out, and is listed among the Things Rust doesn’t let you do as of late 2018.

I couldn't find specific plans to tackle this, as previously other borrow checker improvements were deemed higher priority. The idea about allowing separate read/write "lifetime roles" (Ref2<'r, 'w>) was mentioned in the NLL RFC, but no-one has made it into an RFC of its own, as far as I can see.

Is there a way around it? Not really, but depending on the reason you needed this in the first place, other ways of structuring the code may be appropriate:

  • You can return a copy/clone instead of the reference
  • Sometimes you can split a fn(&mut self) -> &T into two, one taking &mut self and another returning &T, as suggested by @Chris here
  • As is often the case in Rust, rearranging your structs to be "data-oriented" rather than "object-oriented" can help
  • You can return a shared reference from the method: fn(&mut self) -> (&Self, &T) (from this answer)
  • You can make the fn take a shared &self reference and use interior mutability (i.e. define the parts of Self that need to be mutated as Cell<T> or RefCell<T>). This may feel like cheating, but it's actually appropriate, e.g. when the reason you need mutability as an implementation detail of a logically-immutable method. After all we're making a method take a &mut self not because it mutates parts of self, but to make it known to the caller so that it's possible to reason about which values can change in a complex program.