从所有者对象读取时可变的自我

Mutable self while reading from owner object

我有一个对象拥有另一个对象。拥有的对象有一个依赖于其所有者的非变异方法的变异方法。架构(尽可能简化)如下所示:

struct World {
    animals: Vec<Animal>,
}

impl World {
    fn feed_all(&mut self) {
        for i in 0..self.animals.len() {
            self.animals[i].feed(self);
        }
    }
}

struct Animal {
    food: f32,
}

impl Animal {
    fn inc_food(&mut self) {
        self.food += 1.0;
    }

    fn feed(&mut self, world: &World) {
        // Imagine this is a much more complex calculation, involving many
        // queries to world.animals, several loops, and a bunch of if
        // statements. In other words, something so complex it can't just
        // be moved outside feed() and pass its result in as a pre-computed value.
        for other_animal in world.animals.iter() {
            self.food += 10.0 / (other_animal.food + self.food);
        }
    }
}

fn main() {
    let mut world = World {
        animals: Vec::with_capacity(1),
    };

    world.animals.push(Animal { food: 0.0 });

    world.feed_all();
}

以上不编译。编译器说:

error[E0502]: cannot borrow `*self` as immutable because `self.animals` is also borrowed as mutable
 --> src/main.rs:8:34
  |
8 |             self.animals[i].feed(self);
  |             ------------         ^^^^- mutable borrow ends here
  |             |                    |
  |             |                    immutable borrow occurs here
  |             mutable borrow occurs here

我明白为什么会出现这个错误,但是 Rust 惯用的方法是什么?

需要说明的是,示例代码不是真实的。它旨在尽可能简单地呈现核心问题。我正在编写的实际应用程序要复杂得多,与动物和喂养无关。

假设在调用 feed() 之前预先计算食物价值是不切实际的。在真实的应用程序中,类似于 feed() 的方法对 World 对象进行多次调用,并对结果执行大量复杂的逻辑。

您想要以不使用别名 self 的形式计算参数 first,然后将其传入。就目前而言,它似乎是有点奇怪的是,一只动物通过观察其他动物来决定它要吃多少食物……无论如何,你可以添加一个方法 Animal::decide_feed_amount(&self, world: &World) -> f32。您可以安全地调用它(&self&World 都是不可变的,所以没关系),将结果存储在一个变量中,然后将其传递给 Animal::feed.

编辑以解决您的编辑问题:好吧,那么您有点搞砸了。 Rust 的借用检查器不够复杂,无法证明您对 Animal 所做的更改不可能干扰对包含 World 的任何可能的不可变访问。您可以尝试的一些事情:

  • 进行函数式更新。复制您要更新的 Animal 以便它有自己的生命周期,更新它,然后覆盖原来的。如果您预先复制整个数组,这会给您有效整个数组的原子更新。

    作为一个在模拟器上工作了大约五年的人,我希望我做了类似的事情而不是改变更新。 感叹

  • 更改为 Vec<Option<Animal>>,这将允许您将 Animal 移出(而不是复制)数组,对其进行变异,然后将其放回原位(参见 std::mem::replace).缺点是现在一切都必须检查数组的每个位置是否有动物。

  • Animal 放在 CellRefCell 中,这将允许您从不可变引用中改变它们。它通过执行无限慢的动态借用检查来做到这一点(没有检查与一些检查),但仍然是 "safe".

  • 绝对不得已:unsafe。但实际上,如果你这样做,你就把所有的内存安全保证都丢掉了 window,所以我不推荐它。

总结:Rust 做正确的事情 拒绝编译我写的东西。在编译时没有办法知道我不会使我正在使用的数据无效。如果我得到一个指向一只动物的可变指针,编译器就不知道我对向量的只读访问不会因为我对那只动物的突变而失效。

因为这在编译时无法确定,所以我们需要某种运行时检查,或者我们需要使用不安全操作来完全绕过安全检查。

如果我们想要以运行时检查为代价的安全性,

RefCell 是可行的方法。 UnsafeCell 至少是一种无需开销即可解决此问题的选项,当然是以安全为代价的。

我得出的结论是 RefCell 在大多数情况下更可取。开销应该是最小的。如果我们在获取这些值后对这些值进行任何稍微复杂的操作,则尤其如此:有用操作的成本将是 RefCell 的检查成本的 war 倍。虽然 UnsafeCell 可能会快一点,但它会让我们犯错。

下面是用 RefCell 解决这个 class 问题的示例程序。我没有选择动物和喂食,而是选择了玩家、墙壁和碰撞检测。不同的风景,相同的想法。该解决方案可推广到游戏编程中的许多非常常见的问题。例如:

  • 一张由二维图块组成的地图,其中每个图块的渲染状态取决于其相邻图块。例如。水边的草需要渲染海岸纹理。当给定图块或其任何相邻图块发生变化时,该图块的渲染状态会更新。

  • 如果 AI 的任何盟友与玩家处于 war 位置,则 AI 宣布 war 反对玩家。

  • 一块地形正在计算它的顶点法线,它需要知道相邻块的顶点位置。

无论如何,这是我的示例代码:

use std::cell::RefCell;

struct Vector2 {x: f32, y: f32}

impl Vector2 {
    fn add(&self, other: &Vector2) -> Vector2 {
        Vector2 {x: self.x + other.x, y: self.y + other.y}
    }
}

struct World {
    players: Vec<RefCell<Player>>,
    walls: Vec<Wall>
}

struct Wall;

impl Wall {
    fn intersects_line_segment(&self, start: &Vector2, stop: &Vector2) -> bool {
        // Pretend this actually does a computation.
        false
    }
}

struct Player {position: Vector2, velocity: Vector2}

impl Player {
    fn collides_with_anything(&self, world: &World, start: &Vector2, stop: &Vector2) -> bool {
        for wall in world.walls.iter() {
            if wall.intersects_line_segment(start, stop) {
                return true;
            }
        }

        for cell in world.players.iter() {
            match cell.try_borrow_mut() {
                Some(player) => {
                  if player.intersects_line_segment(start, stop) {
                      return true;
                  }
                },
                // We don't want to collision detect against this player. Nor can we,
                // because we've already mutably borrowed this player. So its RefCell
                // will return None.
                None => {}
            }
        }

        false
    }

    fn intersects_line_segment(&self, start: &Vector2, stop: &Vector2) -> bool {
        // Pretend this actually does a computation.
        false
    }

    fn update_position(&mut self, world: &World) {
        let new_position = self.position.add(&self.velocity);
        if !Player::collides_with_anything(self, world, &self.position, &new_position) {
            self.position = new_position;
        }
    }
}

fn main() {
    let world = World {
        players: vec!(
            RefCell::new(
              Player {
                  position: Vector2 { x: 0.0, y: 0.0},
                  velocity: Vector2 { x: 1.0, y: 1.0}
              }
            ),
            RefCell::new(
              Player {
                  position: Vector2 { x: 1.1, y: 1.0},
                  velocity: Vector2 { x: 0.0, y: 0.0}
              }
            )
        ),

        walls: vec!(Wall, Wall)
    };

    for cell in world.players.iter() {
        let player = &mut cell.borrow_mut();
        player.update_position(&world);
    }
}

以上可以更改为使用 UnsafeCell,只需很少的更改。但同样,我认为 RefCell 在这种情况下和大多数其他情况下更可取。

感谢@DK 让我走上了这个解决方案的正确轨道。