Why are borrowing members of the structure allowed in & mut self, but not from themselves to immutable methods?

If I have a structure that encapsulates two members and updates one based on the other, this is fine as long as I do it this way:

struct A { value: i64 } impl A { pub fn new() -> Self { A { value: 0 } } pub fn do_something(&mut self, other: &B) { self.value += other.value; } pub fn value(&self) -> i64 { self.value } } struct B { pub value: i64 } struct State { a: A, b: B } impl State { pub fn new() -> Self { State { a: A::new(), b: B { value: 1 } } } pub fn do_stuff(&mut self) -> i64 { self.a.do_something(&self.b); self.a.value() } pub fn get_b(&self) -> &B { &self.b } } fn main() { let mut state = State::new(); println!("{}", state.do_stuff()); } 

That is, when I directly refer to self.b But when I change do_stuff() to this:

 pub fn do_stuff(&mut self) -> i64 { self.a.do_something(self.get_b()); self.a.value() } 

The compiler complains: cannot borrow `*self` as immutable because `self.a` is also borrowed as mutable .

What if I need to do something more complex than just returning a member to get an argument for a.do_something() ? Should I make a function that returns b by value and stores it in a binding, and then pass that binding to do_something() ? What if b is complicated?

More importantly for my understanding, what memory insecurity is the compiler that saves me from here?

+5
source share
2 answers

The key aspect of mutable links is that they are only guaranteed to have access to a specific value during their existence (unless they are reinstalled, which temporarily disables them).

When you write

 self.a.do_something(&self.b); 

the compiler can see that the loan on self.a (which is implicitly used to call the method) is different from borrowing on self.b because it can talk about direct calls to the field.

However, when you write

 self.a.do_something(self.get_b()); 

then the compiler does not see a loan on self.b , but rather a loan on self . This is because lifetime parameters on method signatures cannot propagate such detailed information about records. Therefore, the compiler cannot guarantee that the value returned by self.get_b() will not give you access to self.a , which will create two links that can access self.a , one of which is mutable, which is illegal.

Reason field fields are not propagated through functions to simplify type checking and borrow checking (for cars and people). The principle is that the signature should be sufficient to perform these tasks: a change in the implementation of a function should not cause errors in its callers.

What if I need to do something more complex than just returning a member to get an argument for a.do_something() ?

I would move get_b from State to B and get_b on self.b Thus, the compiler can see individual entries on self.a and self.b and will accept the code.

 self.a.do_something(self.b.get_b()); 
+6
source

Yes, the compiler isolates functions for the purpose of the security checks that it does. If this did not happen, then each function essentially would have to be integrated everywhere. No one would appreciate this for at least two reasons:

  • Compilation time will go through the roof, and many parallelization options should be discarded.
  • Changing the function of N calls may affect the current function. See Also Why do we need explicit life times in Rust? that affect the same concept.

what memory - insecurity - is the compiler saving me from here.

No really. In fact, it can be argued that it creates false positives, as your example shows.

This is really more useful for preserving the skill of a programmer .


The general advice that I give and follow when I come across this problem is that the compiler leads you to discover a new type in your existing code.

Your specific example is too simplified to make sense, but if you have struct Foo(A, B, C) and find that the Foo method needs A and B , this is often a good sign that there is a hidden type consisting of A and B : struct Foo(Bar, C); struct Bar(A, B) struct Foo(Bar, C); struct Bar(A, B) .

This is not a silver bullet, since you can get methods that require each pair of data, but in my experience it works most of the time.

+1
source

Source: https://habr.com/ru/post/1265654/


All Articles