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

How to borrow two disjoint fields when the borrow is behind a method call?

提问人:Ted Klein Bergman 提问时间:4/19/2021 最后编辑:Ted Klein Bergman 更新时间:4/26/2021 访问量:1712

问:

在下面的代码中,我有一个带有只读字段和一堆读写字段的结构。直接从结构中借用单独的字段时,借用没有问题。但是,当我将借用隐藏在方法调用后面时,它说我不能再借用。Fooa

#![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 游乐场

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

有没有办法告诉编译器代码很好,我正在借用两个不相交的字段?或者有其他符合人体工程学的解决方案?

Rust 借用检查器

评论

2赞 Stargateur 4/19/2021
play.rust-lang.org/......
0赞 Michael Anderson 4/19/2021
@Stargateur解决方案成语在 Rust 中似乎很常见,但它有一个既定的名称吗?
0赞 Jonas Berlin 4/19/2021
难道这里的原因不是编译器不查看函数内部以查看它们借用结构的哪些部分,因此无法验证是否可以借入吗?bmain
1赞 Masklinn 4/19/2021
@MichaelAnderson 拆分借款可能是最接近的。
1赞 Masklinn 4/19/2021
@JonasBerlin很确定,但与此同时,向类型系统添加借用拆分通常会泄露实现细节并削弱 API,因此这不是一个无害的更改。

答:

9赞 Ted Klein Bergman 4/26/2021 #1

可以使用的不同技术

使用拆分借用

此注释建议使用 Splitting Borrow 来借用字段。这将如以下示例所示。

但是,对于维护者来说,这不是一个符合人体工程学的 API。如果他们已经借入了字段,现在也想借入,他们必须重写他们的借用才能通过拆分借用方法。他们还必须与他们想要借用的领域相匹配。由于它们与元组匹配,因此并不完全清楚它们与哪些字段匹配。fooa

此外,引入一个新的公共领域会破坏一切,因为签名必须改变。Foosplit_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 游乐场

使用内部可变性

此答案显示了如何通过将类型包装在 .如果我们将所有可变字段包装在 a 中并且只对 的不可变借用进行操作,这可能是一个解决方案。mutstd::cell::CellCellFoo

但是,这限制了数据是单线程的,因为实现!同步。它还将数据限制为仅复制。此外,这确实允许可变字段在我们传递了不可变引用的代码位置发生突变,因此期望它们不会发生突变。我不认为这是一个解决方案,但可以工作。std::cell::Cell

包装在 ReadOnly 类型中

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

唯一的不便是您需要在每个模块中定义结构。这是因为您希望只能由拥有(换句话说,不能是公共的)的结构访问。ReadOnlyget_mutReadOnlyget_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 游乐场

TL;博士

单个字段的只读访问器或“getter”很容易破坏有效的借用。相反,字段应改为包装在 ReadOnly 结构中,或者如果字段数量较少,则应提供 Split Borrow 方法。