Почему в &mut self разрешено заимствование членов структуры, но не из self в неизменяемые методы?

Если у меня есть структура, которая инкапсулирует два члена и обновляет один на основе другого, это нормально, пока я делаю это следующим образом:

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());
}

То есть, когда я прямо ссылаюсь на self.b. Но когда я меняю do_stuff() на это:

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

Компилятор жалуется: cannot borrow `*self` as immutable because `self.a` is also borrowed as mutable.

Что, если мне нужно сделать что-то более сложное, чем просто вернуть член, чтобы получить аргумент для a.do_something()? Должен ли я создать функцию, которая возвращает b по значению и сохранить его в привязке, а затем передать эту привязку do_something()? Что, если b сложное?

Что еще более важно, насколько я понимаю, от какой небезопасности памяти меня спасает компилятор?


person Leonora Tindall    schedule 18.03.2017    source источник


Ответы (2)


Ключевым аспектом изменяемых ссылок является то, что они гарантированно являются единственным способом доступа к определенному значению, пока они существуют (если только они не заимствованы повторно, что временно «отключает» их).

Когда вы пишете

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

компилятор может видеть, что заимствование для self.a (которое неявно используется для выполнения вызова метода) отличается от заимствования для self.b, потому что оно может рассуждать о прямом доступе к полю.

Однако, когда вы пишете

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

тогда компилятор видит не заимствование self.b, а заимствование self. Это связано с тем, что параметры времени жизни в сигнатурах методов не могут распространять такую ​​подробную информацию о заимствованиях. Следовательно, компилятор не может гарантировать, что значение, возвращаемое self.get_b(), не даст вам доступа к self.a, что создаст две ссылки, которые могут получить доступ к self.a, одна из которых будет изменяемой, что является незаконным.

Причина, по которой заимствование полей не распространяется на функции, заключается в упрощении проверки типов и проверки заимствования (для машин и для людей). Принцип заключается в том, что сигнатуры должно быть достаточно для выполнения этих задач: изменение реализации функции не должно вызывать ошибок в вызывающих ее функциях.

Что, если мне нужно сделать что-то более сложное, чем просто вернуть элемент, чтобы получить аргумент для a.do_something()?

Я бы переместил get_b с State на B и позвонил get_b на self.b. Таким образом, компилятор может увидеть различные заимствования в self.a и self.b и примет код.

self.a.do_something(self.b.get_b());
person Francis Gagné    schedule 18.03.2017
comment
Стратегия перемещения get_b в B мне не пришла в голову, но в данном случае она работает очень хорошо, поскольку цель State состоит в том, чтобы полностью инкапсулировать как A, так и B. Большое спасибо. - person Leonora Tindall; 18.03.2017

Да, компилятор изолирует функции в целях проверки безопасности, которую он выполняет. Если бы это было не так, то каждую функцию пришлось бы встраивать везде. Никто не оценит это как минимум по двум причинам:

  1. Время компиляции будет зашкаливать, и от многих возможностей распараллеливания придется отказаться.
  2. Изменения, внесенные в функцию N вызовов, могут повлиять на текущую функцию. См. также Почему в Rust необходимы явные времена жизни?, где затрагивается такая же концепция.

от какой памяти-небезопасности компилятор спасает меня отсюда

Нет, правда. Фактически, можно утверждать, что это создает ложные срабатывания, как показывает ваш пример.

Это действительно больше полезно для сохранения нормального рассудка программиста.


Общий совет, который я даю и которому следую, когда сталкиваюсь с этой проблемой, состоит в том, что компилятор направляет вас к обнаружению нового типа в вашем существующем коде.

Ваш конкретный пример слишком упрощен, чтобы это имело смысл, но если у вас был struct Foo(A, B, C) и вы обнаружили, что метод Foo требует A и B, это часто является хорошим признаком того, что существует скрытый тип, состоящий из A и B: struct Foo(Bar, C); struct Bar(A, B).

Это не серебряная пуля, так как вы можете получить методы, которым нужна каждая пара данных, но по моему опыту это работает в большинстве случаев.

person Shepmaster    schedule 18.03.2017
comment
Это интересно. Реальный случай, из которого был извлечен этот минимальный пример, представлял собой структуру, содержащую все важные для игры состояния клона Pong, а A и B были мячом и ракеткой. В этом случае тип PhysicsState может быть извлечен из общего GameState. Спасибо за это понимание. - person Leonora Tindall; 19.03.2017