为什么在“&mut self”中允许借用结构体成员,但是不允许在不可变方法中借用“self”?

8
如果我有一个封装了两个成员变量的结构体,并且基于其中一个更新另一个,只要我按照以下方式进行更新,那就没问题:
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个回答

9

可变引用的一个关键特点是,只要它们存在(除非它们被重新借用并且被“禁用”了),它们保证是访问特定值的唯一方式。

当你编写代码时:

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,并在self.b上调用get_b。这样,编译器就可以看到self.a和self.b上的不同借用,并接受代码。
self.a.do_something(self.b.get_b());

get_b 移动到 B 的策略并没有想到过我,但在这种情况下它非常有效,因为 State 的目的是完全封装 AB。非常感谢。 - Leonora Tindall

2
是的,编译器为了进行安全检查隔离函数。如果它没有这样做的话,那么每个函数本质上都必须在任何地方内联。至少有两个原因没有人会欣赏这种方式:
  1. 编译时间将会大幅增加,许多并行化的机会也将不得不被废弃。
  2. 距离 N 个函数调用发生的更改可能会影响当前函数。另请参阅为什么 Rust 需要显式生命周期,其中涉及同样的概念。

编译器正在帮助我避免哪些内存不安全问题?

实际上,没有帮助你避免任何内存不安全问题。事实上,正如你的例子所显示的那样,可以说它创建了虚假阳性。
它确实更多地是为保护程序员的心理健康而提供的好处。
当我遇到这个问题时,我给出并遵循的一般建议是:编译器指导你在现有代码中发现一个新类型。
对于您的特定示例,这有点太简单化了,但如果您有struct Foo(A,B,C)并发现Foo 的一个方法需要A和B ,那通常表明存在由A 和B 组成的隐藏类型:struct Foo(Bar,C);结构体Bar(A,B)
这不是万能药,因为您可能会得到需要每对数据的方法,但根据我的经验,它在大多数情况下都是有效的。

1
这很有趣。这个最小化的例子是从一个包含所有与 Pong 克隆游戏相关状态的结构中提取出来的,而 AB 则分别代表球和挡板。在这种情况下,可以从通用的 GameState 中提取出类型为 PhysicsState 的内容。感谢您提供的见解。 - Leonora Tindall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接