Почему заимствования членов структуры допускаются в &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 это сложно?

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

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

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

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

что за нестабильность памяти спасает меня отсюда

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

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


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

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

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

Другие вопросы по тегам