问题描述
我正在编写游戏引擎.在引擎中,我有一个游戏状态,其中包含游戏中的实体列表.
I'm writing a game engine. In the engine, I've got a game state which contains the list of entities in the game.
我想在我的游戏状态 update
上提供一个函数,它会依次告诉每个实体进行更新.每个实体都需要能够引用游戏状态才能正确更新自身.
I want to provide a function on my gamestate update
which will in turn tell each entity to update. Each entity needs to be able to refer to the gamestate in order to correctly update itself.
这是我目前所拥有的简化版本.
Here's a simplified version of what I have so far.
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();
}
问题是借用检查器不喜欢我将游戏状态传递给实体:
The problem is the borrow checker doesn't like me passing the gamestate to the entity:
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
谁能给我一些更好的方法来设计更适合 Rust 的建议?
Can anyone give me some suggestions on better ways to design this that fits with Rust better?
谢谢!
推荐答案
首先,让我们回答你没有问过的问题:为什么不允许这样做?
First, let's answer the question you didn't ask: Why is this not allowed?
答案在于 Rust 对 &
和 &mut
指针的保证.&
指针保证指向一个 immutable 对象,即指针后面的对象不可能发生变化,而您可以使用该指针.&mut
指针保证是指向对象的唯一活动指针,也就是说,您可以确保在更改对象时没有人会观察或更改该对象.
The answer lies around the guarantees that Rust makes about &
and &mut
pointers. A &
pointer is guaranteed to point to an immutable object, i.e. it's impossible for the objects behind the pointer to mutate while you can use that pointer. A &mut
pointer is guaranteed to be the only active pointer to an object, i.e. you can be sure that nobody is going to observe or mutate the object while you're mutating it.
现在,让我们看看Entity::update
的签名:
Now, let's look at the signature of Entity::update
:
impl Entity {
pub fn update(&mut self, container: &GameState) {
// ...
}
}
这个方法有两个参数:一个 &mut Entity
和一个 &GameState
.但是等等,我们可以通过 &GameState
获得另一个对 self
的引用!例如,假设 self
是第一个实体.如果我们这样做:
This method takes two parameters: a &mut Entity
and a &GameState
. But hold on, we can get another reference to self
through the &GameState
! For example, suppose that self
is the first entity. If we do this:
impl Entity {
pub fn update(&mut self, container: &GameState) {
let self_again = &container.entities[0];
// ...
}
}
then self
和 self_again
互为别名(即它们指的是同一个东西),根据我上面提到的规则,这是不允许的,因为其中一个指针是一个可变指针.
then self
and self_again
alias each other (i.e. they refer to the same thing), which is not allowed as per the rules I mentioned above because one of the pointers is a mutable pointer.
对此你能做些什么?
一种选择是在调用 update
之前从实体向量中删除实体,然后在调用后将其插入.这解决了别名问题,因为我们无法从游戏状态中获取实体的另一个别名.然而,从向量中移除实体并重新插入它是具有线性复杂度的操作(向量需要移动以下所有项),如果你对每个实体都这样做,那么主更新循环以二次复杂度运行.您可以通过使用不同的数据结构来解决这个问题;这可以像 Vec>
一样简单,您只需 take
来自每个 Option
的 Entity
,虽然你可能想要将其包装成一种类型,将所有 None
值隐藏到外部代码中.一个很好的结果是,当一个实体必须与其他实体交互时,它会在迭代实体向量时自动跳过自己,因为它不再存在!
One option is to remove an entity from the entities vector before calling update
on it, then inserting it back after the call. This solves the aliasing problem because we can't get another alias to the entity from the game state. However, removing the entity from the vector and reinserting it are operations with linear complexity (the vector needs to shift all the following items), and if you do it for each entity, then the main update loop runs in quadratic complexity. You can work around that by using a different data structure; this can be as simple as a Vec<Option<Entity>>
, where you simply take
the Entity
from each Option
, though you might want to wrap this into a type that hides all None
values to external code. A nice consequence is that when an entity has to interact with other entities, it will automatically skip itself when iterating on the entities vector, since it's no longer there!
上述的一种变体是简单地获得整个实体向量的所有权,并用空的实体向量临时替换游戏状态的实体向量.
A variation on the above is to simply take ownership of the whole vector of entities and temporarily replace the game state's vector of entities with an empty one.
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;
}
}
这有一个主要缺点:Entity::update
将无法与其他实体交互.
This has one major downside: Entity::update
will not be able to interact with the other entities.
另一种选择是将每个实体包装在 RefCell
.
Another option is to wrap each entity in a 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);
}
}
}
通过使用RefCell
,我们可以避免在self
上保留一个可变借用.在这里,我们可以使用 iter
而不是 iter_mut
来迭代 entities
.作为回报,我们现在需要调用 borrow_mut
来获取一个指向包裹在 RefCell
中的值的可变指针.
By using RefCell
, we can avoid retaining a mutable borrow on self
. Here, we can use iter
instead of iter_mut
to iterate on entities
. In return, we now need to call borrow_mut
to obtain a mutable pointer to the value wrapped in the RefCell
.
RefCell
本质上是在运行时执行借用检查.这意味着您最终可能会编写编译良好但在运行时出现恐慌的代码.例如,如果我们这样写 Entity::update
:
RefCell
essentially performs borrow checking at runtime. This means that you can end up writing code that compiles fine but panics at runtime. For example, if we write Entity::update
like this:
impl Entity {
pub fn update(&mut self, container: &GameState) {
for entity in container.entities.iter() {
self.value += entity.borrow().value;
}
}
}
程序会崩溃:
thread 'main' panicked at 'already mutably borrowed: BorrowError', ../src/libcore/result.rs:788
那是因为我们最终对当前正在更新的实体调用了 borrow
,它仍然被 GameState:: 中的
.borrow_mut
调用借用了.更新Entity::update
没有足够的信息来知道哪个实体是 self
,因此您必须使用 try_borrow
或 borrow_state
(从 Rust 1.12 开始都是不稳定的.1) 或将额外数据传递给 Entity::update
以避免这种方法出现恐慌.
That's because we end up calling borrow
on the entity that we're currently updating, which is still borrowed by the borrow_mut
call done in GameState::update
. Entity::update
doesn't have enough information to know which entity is self
, so you would have to use try_borrow
or borrow_state
(which are both unstable as of Rust 1.12.1) or pass additional data to Entity::update
to avoid panics with this approach.
这篇关于在迭代器循环中对容器对象的可变引用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!