为啥 &mut self 允许借用 struct 成员,但不允许将 self 借用到不可变方法?

Posted

技术标签:

【中文标题】为啥 &mut self 允许借用 struct 成员,但不允许将 self 借用到不可变方法?【英文标题】:Why are borrows of struct members allowed in &mut self, but not of self to immutable methods?为什么 &mut self 允许借用 struct 成员,但不允许将 self 借用到不可变方法? 【发布时间】:2017-08-10 06:50:08 【问题描述】:

如果我有一个封装两个成员的结构,并根据另一个更新一个,那么只要我这样做就可以了:

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 很复杂怎么办?

在我的理解中更重要的是,编译器从这里拯救了我什么样的内存不安全?

【问题讨论】:

【参考方案1】:

可变引用的一个关键方面是,当它们存在时,它们保证是访问特定值的唯一方式(除非它们被重新借用,这会暂时“禁用”它们)。

当你写作时

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_bState 移动到B 并在self.b 上致电get_b。这样,编译器可以看到 self.aself.b 上的不同借用,并接受代码。

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

【讨论】:

我没有想到将get_b 移动到B 的策略,但在这种情况下它工作得非常好,因为State 的目的是完全封装A 和@ 987654343@。非常感谢。【参考方案2】:

是的,编译器隔离函数是为了进行安全检查。如果没有,那么基本上每个函数都必须在任何地方内联。至少有两个原因没有人会欣赏这一点:

    编译时间将飞逝,许多并行化的机会将不得不放弃。 对函数 N 调用的更改可能会影响 当前 函数。另请参阅Why are explicit lifetimes needed in Rust?,它涉及相同的概念。

编译器从这里救了我什么样的内存不安全

没有,真的。事实上,正如您的示例所示,可以说它会产生误报。

这确实更有利于保持程序员的理智


当我遇到这个问题时,我给出并遵循的一般建议是编译器正在指导您在现有代码中发现新类型。

您的特定示例有点过于简化,无法理解,但如果您有 struct Foo(A, B, C) 并发现 Foo 上的方法需要 AB,这通常是一个好兆头,表明存在由AB组成的隐藏类型:struct Foo(Bar, C); struct Bar(A, B)

这不是灵丹妙药,因为您最终可以使用需要每对数据的方法,但根据我的经验,它在大多数情况下都有效。

【讨论】:

这很有趣。提取这个最小示例的真实案例是一个包含 Pong 克隆的所有游戏相关状态的结构,AB 是球和桨。在这种情况下,可以从一般GameState 中提取类型PhysicsState。感谢您的洞察力。

以上是关于为啥 &mut self 允许借用 struct 成员,但不允许将 self 借用到不可变方法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在某些 trait 方法调用中会出现来自 &mut 的引用减弱?

循环中的可变借用

别名可变原始指针 (*mut T) 会导致未定义的行为吗?

在“get_trait_mut”中返回对trait的可变引用

如何实现对`async fn(&mut self)`进行轮询的`Future`?

为啥我的变量寿命不够长?