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

时间:2015-02-07 17:44:46

标签: rust

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

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对象进行多次调用,并对结果执行许多复杂的逻辑。

2 个答案:

答案 0 :(得分:3)

你想在一个没有别名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中,这样您就可以从不可变引用中改变它们。它通过执行无限慢的动态借用检查(没有检查与某些检查)来实现这一点,但仍然是“安全的”。

  • 绝对不得已:unsafe。但实际上,如果你这样做,你就会把所有的内存安全保障抛到窗外,所以我不推荐它。

答案 1 :(得分:0)

总结:Rust通过拒绝编译我编写的内容来做正确的事情。在编译时无法知道我不会使我正在使用的数据无效。如果我得到一个指向一只动物的可变指针,编译器就无法知道我对该载体的只读访问权限不会因我对该特定动物的突变而失效。

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

如果我们想要以运行时检查为代价来保证安全,那么

RefCell就是可行的方法。 UnsafeCell至少有一个选项可以在没有开销的情况下解决这个问题,当然是以安全为代价。

我得出结论,在大多数情况下,RefCell更可取。开销应该是最小的。如果我们在获得它们之后做了与价值相当复杂的任何事情,那就更是如此:有用操作的成本会使RefCell的检查成本相形见绌。虽然UnsafeCell可能会快一点,但它会让我们犯错误。

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

  • 由2D图块组成的地图,其中每个图块的渲染状态取决于其邻居。例如。水旁边的草需要渲染海岸纹理。当该图块或其任何邻居发生更改时,给定图块的呈现状态会更新。

  • 如果AI的任何盟友与玩家发生战争,AI会向玩家宣战。

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

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

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让我走上正确的解决方案。