在迭代器循环中对容器对象的可变引用

Ale*_*lex 5 rust

我正在编写一个游戏引擎。在引擎中,我有一个游戏状态,其中包含游戏中的实体列表。

我想在我的游戏状态上提供一个函数,该函数update将依次告诉每个实体进行更新。每个实体都需要能够引用游戏状态才能正确更新自身。

这是我目前所拥有的简化版本。

pub struct GameState {
    pub entities: Vec<Entity>,
}

impl GameState {
    pub fn update(&mut self) {
        for mut t in self.entities.iter_mut() {
            t.update(self);
        }
    }
}

pub struct Entity {
    pub value: i64,
}

impl Entity {
    pub fn update(&mut self, container: &GameState) {
        self.value += container.entities.len() as i64;
    }
}

fn main() {
    let mut c = GameState { entities: vec![] };

    c.entities.push(Entity { value: 1 });
    c.entities.push(Entity { value: 2 });
    c.entities.push(Entity { value: 3 });

    c.update();
}
Run Code Online (Sandbox Code Playgroud)

问题是借用检查器不喜欢我将游戏状态传递给实体:

error[E0502]: cannot borrow `*self` as immutable because `self.entities` is also borrowed as mutable
 --> example.rs:8:22
  |
7 |         for mut t in self.entities.iter_mut() {
  |                      ------------- mutable borrow occurs here
8 |             t.update(self);
  |                      ^^^^ immutable borrow occurs here
9 |         }
  |         - mutable borrow ends here

error: aborting due to previous error
Run Code Online (Sandbox Code Playgroud)

谁能给我一些关于更好地设计更适合 Rust 的方法的建议?

谢谢!

Fra*_*gné 6

首先,让我们回答您没有问过的问题:为什么不允许这样做?

答案在于 Rust 的保证&&mut指针。一个&指针保证指向一个不可变的对象,也就是说,当你可以使用那个指针时,指针后面的对象不可能发生变化。一个&mut指针保证是唯一的活动对象指针,即你可以肯定的是没有人会观察或突变的对象,而你变异了。

现在,让我们看看 的签名Entity::update

impl Entity {
    pub fn update(&mut self, container: &GameState) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这个方法有两个参数: a&mut Entity和 a &GameState。但是等等,我们可以self通过&GameState! 例如,假设这self是第一个实体。如果我们这样做:

impl Entity {
    pub fn update(&mut self, container: &GameState) {
        let self_again = &container.entities[0];
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

thenselfself_again彼此别名(即它们指的是同一件事),根据我上面提到的规则,这是不允许的,因为其中一个指针是可变指针。

你能做些什么呢?

一种选择是在调用实体之前从实体向量中移除实体update,然后在调用之后将其插入。这解决了别名问题,因为我们无法从游戏状态中获取实体的另一个别名。但是,从向量中移除实体并重新插入它是具有线性复杂度的操作(向量需要移动以下所有项),如果您对每个实体都这样做,那么主更新循环以二次复杂度运行。您可以通过使用不同的数据结构来解决这个问题;这可以是简单的Vec<Option<Entity>>,你只需takeEntity每个Option,虽然你可能要包装成一个类型这个隐藏所有None外部代码的值。一个很好的结果是,当一个实体必须与其他实体交互时,它会在迭代实体向量时自动跳过自己,因为它不再存在!

上述的一种变体是简单地获得整个实体向量的所有权,并用空的实体向量临时替换游戏状态的实体向量。

impl GameState {
    pub fn update(&mut self) {
        let mut entities = std::mem::replace(&mut self.entities, vec![]);
        for mut t in entities.iter_mut() {
            t.update(self);
        }
        self.entities = entities;
    }
}
Run Code Online (Sandbox Code Playgroud)

这有一个主要缺点:Entity::update将无法与其他实体进行交互。

另一种选择是将每个实体包装在RefCell.

use std::cell::RefCell;

pub struct GameState {
    pub entities: Vec<RefCell<Entity>>,
}

impl GameState {
    pub fn update(&mut self) {
        for t in self.entities.iter() {
            t.borrow_mut().update(self);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过使用RefCell,我们可以避免在 上保留可变借用self。在这里,我们可以使用iter代替iter_mut来迭代entities。作为回报,我们现在需要调用borrow_mut以获取指向包含在RefCell.

RefCell本质上在运行时执行借用检查。这意味着您最终可能会编写编译良好但在运行时出现恐慌的代码。例如,如果我们这样写Entity::update

impl Entity {
    pub fn update(&mut self, container: &GameState) {
        for entity in container.entities.iter() {
            self.value += entity.borrow().value;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

程序会恐慌:

impl Entity {
    pub fn update(&mut self, container: &GameState) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这是因为我们最终调用borrow的实体,我们目前正在更新,这仍然是通过借用borrow_mut在完成通话GameState::updateEntity::update没有足够的信息来知道哪个实体是self,因此您必须使用try_borrowborrow_state(从 Rust 1.12.1 开始它们都不稳定)或传递额外的数据Entity::update来避免这种方法的恐慌。