当借用在方法调用之后时,如何借用两个不相交的字段?

9
在下面的代码中,我有一个带有只读字段 a 和一堆可读写字段的结构体 Foo。当直接从结构体借用单独的字段时,没有任何问题。但是,当我将借用隐藏在方法调用后面时,它说我不再可以借用。
#![allow(unused_variables)]
#![allow(unused_mut)]
#![allow(dead_code)]

struct Foo {
    a: Vec<i32>,      // Public read-only  field.
    pub b: Vec<f32>,  // Public read-write field.
    pub c: Vec<i32>,  // Public read-write field.
    // ... maybe more fields ...
    pub z: Vec<bool>, // Public read-write field.
}

impl Foo {
    pub fn new() -> Self {
        Self {
            a: vec![1, 2, 3],
            b: vec![1.0, 2.0, 3.0],
            c: vec![-3, 0, 3],
            z: vec![false, true],
        }
    }
    pub fn borrow_a(&self) -> &Vec<i32> {
        &self.a
    }
}

pub fn main() {
    let mut foo = Foo::new();
    
    {   // This is okay.
        let x     = &foo.a;      // Immutably borrow `a`.
        let mut y = &mut foo.b;  // Mutably borrow `b`.
        for i in x { }           // Immutably use `a`.   
    }


    {   // This creates an error.
        let x = foo.borrow_a();  // Immutably borrow `a`.
        let mut y = &mut foo.b;  // Mutably borrow `b`.
        for i in x { }           // Immutably use `a`.   
    }
}

Rust playground

error[E0502]: cannot borrow `foo.b` as mutable because it is also borrowed as immutable
  --> src/main.rs:39:21
   |
38 |         let x = foo.borrow_a();  // Immutably borrow `a`.
   |                 --- immutable borrow occurs here
39 |         let mut y = &mut foo.b;  // Mutably borrow `b`.
   |                     ^^^^^^^^^^ mutable borrow occurs here
40 |         for i in x { }           // Immutably use `a`.   
   |                  - immutable borrow later used here

有没有什么方法可以让编译器知道这段代码是正确的,而我正在借用两个不相交的字段?还是有其他符合人体工程学的解决方案?


难道原因不是编译器无法查看函数内部以确定它们借用结构体的哪些部分,因此无法验证在main中借用b是否安全? - Jonas Berlin
1
@MichaelAnderson 分离借用 可能是最接近的。 - Masklinn
1
@JonasBerlin 当然,同时将借用分割添加到类型系统中通常会泄漏实现细节并使API变得脆弱,因此这不是一个无害的更改。 - Masklinn
@Stargateur 在制作此示例时可能已经切掉了一些过多的信息。两个字段均应为公共字段,但第一个字段 a 不应直接被更改,即从用户的角度来看它应该是只读的字段。 - Ted Klein Bergman
显示剩余6条评论
1个回答

9

可采用的不同技术

使用Splitting Borrow

该评论建议使用Splitting Borrow来借用字段。这将像下面的示例一样工作。

然而,对于用户或维护者来说,这并不是一个人性化的API。如果已经在foo中借用了字段,现在还想借用a,就必须重写他们的借用以通过Split Borrow方法。他们还必须匹配他们想要借用的字段。由于它们与元组匹配,不完全清楚它们正在匹配哪些字段。

此外,在Foo中引入一个新的公共字段会破坏一切,因为split_borrow的签名必须更改。

总之,当字段数量较少时,这种方法可以起作用。

#![allow(unused_variables)]
#![allow(unused_mut)]
#![allow(dead_code)]

struct Foo {
    a: Vec<i32>,      // Public read-only  field.
    pub b: Vec<f32>,  // Public read-write field.
    pub c: Vec<i32>,  // Public read-write field.
    // ... maybe more fields ...
    pub z: Vec<bool>, // Public read-write field.
}

impl Foo {
    pub fn new() -> Self {
        Self {
            a: vec![1, 2, 3],
            b: vec![1.0, 2.0, 3.0],
            c: vec![-3, 0, 3],
            z: vec![false, true],
        }
    }
    pub fn split_borrow(&mut self) -> (&Vec<i32>, &mut Vec<f32>, &mut Vec<i32>, &mut Vec<bool>) {
        (&self.a, &mut self.b, &mut self.c, &mut self.z)
    }
}

pub fn main() {
    let mut foo = Foo::new();
    
    {   // This is okay.
        let (a, ref mut b, ..) = foo.split_borrow();
        for i in a { }
    }
    
    {   // This is okay.
        let (a, _, _, ref mut z) = foo.split_borrow();
        for i in a { }
    }

    {   // This is okay if we re-borrow the values
        // between each use.
        let (a, ref mut b, ..)   = foo.split_borrow();
        b.push(4.0);
        
        let (a, _, _, ref mut z) = foo.split_borrow();
        // Can't use b from this point.
        z.push(false);
        
        println!("{:?}, {:?}", a, z);
    }

    
    {   // It's not okay to mix-and-match variables
        // from different borrows, as they're exclusively
        // bound to `foo`.
        let (a, ref mut b, ..)   = foo.split_borrow();
        let (_, _, _, ref mut z) = foo.split_borrow();
        for i in a { }
    }
}

Rust Playground

使用内部可变性

这个答案展示了如何通过将类型包装在std::cell::Cell中来模拟在字段中使用mut的旧构造。如果我们将所有可变字段都包装在Cell中,并且只对Foo的不可变借用进行操作,则可以采用此解决方案。

然而,这限制了数据为单线程,因为std::cell::Cell实现了!Sync。它还限制了数据只能是Copy。此外,在我们传递不可变引用并期望它们不被改变的代码位置中,这确实允许可变字段发生变化。我不认为这是一个解决方案,但可以工作。

包装为ReadOnly类型

这个答案展示了如何将只读值包装到不可变结构体中。这是迄今为止最清晰和最符合人体工程学的解决方案,如下例所示。由于所有字段现在都是公共的,借用检查器能够确定我们实际上正在借用不相交的字段。

唯一的不便之处在于您需要在每个模块中定义ReadOnly结构体。原因是您希望get_mut只能被拥有ReadOnly的结构体访问(也就是说,get_mut不能是公共的)。
#![allow(unused_variables)]
#![allow(unused_mut)]
#![allow(dead_code)]

use std::ops::Deref;

struct ReadOnly<T> {
    data: T,
}

impl<T> ReadOnly<T> {
    pub fn new(data: T) -> Self {
        ReadOnly { data }
    }
    
    pub fn get(&self) -> &T {
        &self.data
    }

    // Private function for mutating the
    // data from within Foo itself.
    fn get_mut(&mut self) -> &mut T {
        &mut self.data
    }
}

impl<T> Deref for ReadOnly<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}



struct Foo {
    pub a: ReadOnly<Vec<i32>>,  // Public read-only  field.
    pub b: Vec<f32>,            // Public read-write field.
    pub c: Vec<i32>,            // Public read-write field.
    // ... maybe more fields ...
    pub z: Vec<bool>,           // Public read-write field.
}

impl Foo {
    pub fn new() -> Self {
        Self {
            a: ReadOnly::new(vec![1, 2, 3]),
            b: vec![1.0, 2.0, 3.0],
            c: vec![-3, 0, 3],
            z: vec![false, true],
        }
    }
}

pub fn main() {
    let mut foo = Foo::new();

    {   // This now works.
        let x     = foo.a.get();  // Immutably borrow `a`.
        let mut y = &mut foo.b;   // Mutably borrow `b`.
        for i in x { }            // Immutably use `a`.   
    }

    
    {   // This is now erroneous.
        let mut x = &mut foo.a;    // Can still borrow ReadOnly as mutable.
        let mut y = &mut foo.b;    // Mutably borrow `b`.
        for i in x.iter_mut() { }  // Can't use `a` as mutable.
    }

}

Rust Playground

简述

针对单个字段的只读访问器或“getter”会轻易破坏有效的借用。因此,应该将这些字段包装在一个ReadOnly结构中,或者如果字段数量较少,则应该提供Split Borrow方法。


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