战斗模拟引擎中的Rust借用检查器



好的,我有一个Combatants在Battlefield上战斗。我的直觉告诉我,什么东西应该放在哪个地方。它很接近于一个游戏,但我现在卡住了。

我希望Battlefield具有tick()功能,允许所有Combatant做出决定,如攻击另一个对面团队或移动接近一个,如果没有人在范围内。我有问题,使借用检查器高兴这样做。

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

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中工作?

因为在您的场景中,您需要同时对中相同的中的两个元素具有可变引用和不可变引用集装箱,我想你需要内部可变性的帮助。

这将在运行时检查同一个元素不能通过可变(.borrow_mut())和不可变(.borrow())引用同时访问(否则会出现panic)。显然,您必须自己确保这一点(这是相当难看的,因为我们必须比较指针!)。

显然,必须到达指针,因为不能比较引用(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);
}
}
}
}

请注意,这个内部可变性一开始让我很困扰,看起来像是"违反规则",因为我本质上是根据"不可变突然变"(就像c++中的常量转换)进行推理的,而共享/排他方面只是结果。但是,这个问题的答案解释说,我们应该首先考虑对数据的共享/独占访问,然后不可变/可变方面只是结果。

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

这并不是"扭曲规则",因为严格的检查是在.borrow()/.borrow_mut()上完成的(目前开销很小),并且获得的Ref/RefMut的生命周期允许在它们出现的代码部分进行通常的(静态)借用检查。它比我们可以用其他编程语言执行的自由不可变/可变访问安全得多。例如,即使在c++中,我们可以将target视为const(指向const的引用/指针),而迭代非const的combatants向量,一次迭代可能会意外地改变我们认为是const的target(指向const的引用/指针意味着«我不会改变它»,而不是)。"它不会被任何人变异"),这可能会产生误导。在其他语言中,const/mut甚至不存在,任何东西都可以在任何时候发生变化(除了严格不可变的对象,比如Python中的str,但是很难管理状态随时间变化的对象,比如你的例子中的current_health)。

问题是:当您遍历战斗人员时,这需要不可变地借用所有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);
}
}
}

您也可以使用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);
}
}

游乐场

注意,如果idx >= len (self.combatants).

最新更新