【问题标题】:Rust borrow checker in a battl simulation enginebattl 模拟引擎中的 Rust 借用检查器
【发布时间】:2021-08-24 20:03:58
【问题描述】:

好的,我有Combatants 与Battlefield 战斗。我对在哪个地方发生的事情的直觉有点偏离。它非常接近成为一个游戏,但我现在卡住了。

我希望Battlefield 有一个tick() 功能,它允许所有Combatants 做出决定,例如攻击对方的另一支队伍,或者在没有人在范围内时靠近一个队伍。我在让借阅检查员对此感到满意时遇到了问题。

这是一个包含我的代码所有问题的最小版本。

struct Combatant{
    current_health: i16,
    max_health: i16
}

struct Battlefield{
    combatants: Vec<Combatant>
}

impl Combatant {
    fn attack(&self, other: &mut Combatant) {
        other.current_health -= 3;
    }
}

impl Battlefield {

    fn tick(&mut self) {
        let target = &mut self.combatants[0];
        for combatant in &self.combatants {
            combatant.attack(target);
        }
    }
}

cargo check 返回

error[E0502]: cannot borrow `self.combatants` as immutable because it is also borrowed as mutable
  --> src/main.rs:20:26
   |
19 |         let target = &mut self.combatants[0];
   |                           --------------- mutable borrow occurs here
20 |         for combatant in &self.combatants {
   |                          ^^^^^^^^^^^^^^^^ immutable borrow occurs here
21 |             combatant.attack(target);
   |                              ------ mutable borrow later used here

我如何设计这个函数(或者更像是整个场景,呵呵)让它在 Rust 中工作?

【问题讨论】:

  • 看看像 hecs 或 specs 这样的 ECS 库,在这些基础上构建类似游戏的应用程序逻辑通常要容易得多。
  • 是的,我之前使用过 Bevy。我这样做是为了挑战自己,也许会从中得到一些乐趣作为副作用。

标签: rust borrow-checker


【解决方案1】:

问题是这样的:当您迭代战斗人员时,这需要对Vec 中的所有个战斗人员进行不可变的借用。然而,其中之一,combatants[0]已经借用了,它是一个可变借用。

你不能同时对同一事物有一个可变和不可变的借用。

这可以防止很多逻辑错误。例如,在您的代码中,如果实际允许借用,您实际上会受到 combatants[0] 攻击本身!

那该怎么办?在上面的具体示例中,您可以做的一件事是使用 vec 的 split_first_mut 方法,https://doc.rust-lang.org/std/vec/struct.Vec.html#method.split_first_mut

let (first, rest) = self.combatants.split_first_mut();
if let Some(first) = first {
  if let Some(rest) = rest {
    for combatant in rest {
      combatant.attack(first);
    }
  }
}

【讨论】:

    【解决方案2】:

    由于在您的场景中,您需要在同一个容器中的两个元素上同时拥有一个可变引用和一个不可变引用,我认为您需要interior mutability 的帮助。

    这将检查在运行时是否通过可变 (.borrow_mut()) 和不可变 (.borrow()) 引用同时访问相同的元素(否则它会崩溃)。 显然,您必须自己确保(这很丑陋,因为我们必须比较指针!)。

    Apparently,因为无法比较引用,所以必须访问指针(std::cmp::PartialEq::eq()self 参数将被取消引用)。 std::ptr::eq() 的文档(应该在这里使用)显示了比较引用和比较指针之间的区别。

    struct Combatant {
        current_health: i16,
        max_health: i16,
    }
    
    struct Battlefield {
        combatants: Vec<std::cell::RefCell<Combatant>>,
    }
    
    impl Combatant {
        fn attack(
            &self,
            other: &mut Combatant,
        ) {
            other.current_health -= 3;
        }
    }
    
    impl Battlefield {
        fn tick(&mut self) {
            let target_cell = &self.combatants[0];
            let target = &*target_cell.borrow();
            for combatant_cell in &self.combatants {
                let combatant = &*combatant_cell.borrow();
                // if combatant as *const _ != target as *const _ {
                if !std::ptr::eq(combatant, target) {
                    let target_mut = &mut *target_cell.borrow_mut();
                    combatant.attack(target_mut);
                }
            }
        }
    }
    

    请注意,这个内部可变性bothering me at first,看起来像是“违反规则”,因为我的推理本质上是“不可变突然变为可变”(就像 C++ 中的 const-casting) ,而共享/独占方面只是其后果。 但是对相关问题的回答表明,我们首先应该考虑对数据的共享/独占访问,然后才是不可变/可变方面的结果。

    回到你的例子,对向量中Combatants 的共享访问似乎是必不可少的,因为在任何时候,它们中的任何一个都可以访问任何其他的。 因为这种选择(共享方面)的结果是可变访问变得几乎不可能,所以我们需要内部可变性的帮助。

    这不是“违反规则”,因为对.borrow()/.borrow_mut()(此时开销很小)进行了严格检查,并且获得的Ref/RefMut具有允许通常(静态)借用的生命周期-检查它们出现的代码部分。 它比我们可以使用其他编程语言执行的免费不可变/可变访问安全得多。 例如,即使在 C++ 中,我们可以在迭代非 const combatants 向量时将 target 视为 const(引用/指向 const),但一次迭代可能会意外地改变我们认为是 const 的 target (引用/指向const 的意思是“我不会改变它”,不是“它不能被任何人改变”),这可能会产生误导。而对于其他甚至不存在const/mut 的语言,任何东西都可以随时发生变异(除了严格不可变的对象,如 Python 中的str,但管理具有以下状态的对象变得困难可能会随着时间而变化,例如您的示例中的current_health)。

    【讨论】:

    • 也许迭代 &amp;self.combatants[1..] 可以防止第一个战斗人员在没有复杂的基于指针的身份检查的情况下攻击自己?
    • @user4815162342 当然可以,但我想这里的索引 0 只是一个最小可重现示例的特定简单案例;可能在实际应用中,目标可以是向量的任何元素。
    • 好点。我想我宁愿有一个 target_index 并用它来进行比较,但仍然存在一些复杂性 - 没有灵丹妙药。
    • 它当然可以是向量的任何元素。看起来我们需要大大改变规则才能在这里完成,即使效果很简单 - 有没有我错过的不同的处理方式?
    • @user93114 我添加了一个段落来解释这个选择(以及为什么我最初和你的观点相同)。
    【解决方案3】:

    您也可以使用split_at_mut 仅迭代其他元素:

    fn tick(&mut self) {
        let idx = 0;
        let (before, after) = self.combatants.split_at_mut (idx);
        let (target, after) = after.split_at_mut (1);
        let target = &mut target[0];
        for combatant in before {
            combatant.attack(target);
        }
        for combatant in after {
            combatant.attack(target);
        }
    }
    

    Playground

    请注意,如果idx &gt;= len (self.combatants),这将引发恐慌。

    【讨论】:

      猜你喜欢
      • 2018-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-11
      相关资源
      最近更新 更多