如何表示共享的可变状态?

15

我想学习Rust,但是我一直试图把我熟悉的Java概念硬塞到它的类型系统中,或者尝试硬塞Haskell概念等等,结果只是不停地碰壁。

我想写一个有玩家和多个资源的游戏。每个资源都可以被一个玩家所拥有:

struct Player {
    points: i32,
}

struct Resource<'a> {
    owner: Option<&'a Player>,
}

fn main() {
    let mut player = Player { points: 0 };
    let mut resources = Vec::new();
    resources.push(Resource {
        owner: Some(&player),
    });
    player.points = 30;
}

它无法编译,因为我不能将资源指向玩家,同时又修改它:

error[E0506]: cannot assign to `player.points` because it is borrowed
  --> src/main.rs:15:5
   |
13 |         owner: Some(&player),
   |                      ------ borrow of `player.points` occurs here
14 |     });
15 |     player.points = 30;
   |     ^^^^^^^^^^^^^^^^^^ assignment to borrowed `player.points` occurs here

此外,如果Resource拥有对Player的可变引用,我甚至无法拥有两个拥有相同所有者的Resource

在Rust中解决这种情况的方法是什么?


我过于简化了我的问题,虽然Shepmaster的答案是正确的答案,但它不是我想要得到的(因为我所问的并不是我真正想问的)。我将尝试重新表述并添加更多上下文。

  1. 这些资源以某种方式相互连接 - 所有资源的映射形成了一个(非)定向图。
  2. 每个玩家可以拥有许多资源,每个资源只能被一个玩家拥有。玩家应该能够从他们拥有的资源中获得积分。我想到了一个签名,类似于:fn addPoints(&mut self, allResources: &ResourcesMap) -> ()
  3. 玩家可以接管与他们的某个资源相连的资源,而这些资源原属于另一个玩家。这可能会导致其他玩家失去一些积分。

问题:

  1. 如何在Rust中表示这样的图形(可能是循环结构,每个节点可以从许多节点指向)?
  2. 最初的问题:如果Resource指向一个Player,我无法修改该玩家!

Resource之所以指向Player,是因为 - 执行这种操作的自然方式将从A玩家的某些资源开始,通过地图移动到玩家B的资源,然后从该资源到达玩家B以减去积分。对我来说,这似乎在Rust中并不自然。

3个回答

15
cell文档页面有相当好的示例。Rust将始终尝试保护您免受执行不良操作(例如对同一事物具有两个可变引用)的影响。因此,它并不像使用Rust内置引用那样“容易”,因为您需要进行运行时检查(Rust引用在编译时进行检查)。 RefCell类型正是为此而存在。它在运行时检查可变性规则。您会获得一些内存和计算时间开销,但最终可以获得Rust在编译时检查中承诺的相同的内存安全性。
将您的示例移植到RefCell后,如下所示。
use std::cell::RefCell;

struct Player {
    points: i32,
}

// the lifetime is still needed to guarantee that Resources
// don't outlive their player
struct Resource<'a> {
    owner: &'a RefCell<Player>,
}

impl<'a> Resource<'a> {
    fn test(&self) -> i32 {
        self.owner.borrow().points
    }
}

fn main() {
    let player = RefCell::new(Player { points: 0 });
    let mut resources = Vec::new();
    resources.push(Resource { owner: &player });
    player.borrow_mut().points = 30;
    println!("{:?}", resources[0].test());
}

我的问题是,如果我想在Rust中编写Java代码,是否可以以Rust方式完成而不牺牲编译时安全性?需要避免所有共享可变状态吗?

你并不会牺牲编译时安全性。Rust确保(在编译时)你正确使用库。然而,如果你使用borrow*函数,你的程序可能在运行时panic。如果你改用try_borrow*函数,则可以检查它是否成功,如果没有,则进行一些回退操作。

你也可以使用一个指向类型为RefCell的引用计数框(Rc<RefCell<Player>>)。这样,你只需要确保不创建循环引用,否则你的内存将永远不会被释放。这更像是Java(尽管Java会自动查找循环引用)。


1
听起来不错。我的担心是,如果我试图在 Rust 中编写 Java 代码,能否以 Rust 的方式完成而不牺牲编译时的安全性?完全避免共享可变状态吗? - PL_kolek
注意:您不会牺牲编译时安全性。Rust确保(在编译时)您正确使用库。但是,如果您使用borrow函数,则您的程序可能在运行时出现panic。如果您改用try_borrow函数,则可以检查它是否成功,如果没有,则执行一些回退操作。 - oli_obk
1
你也可以使用引用计数盒子 (http://doc.rust-lang.org/std/rc/index.html) 来引用 RefCell 到你的类型。然后你只需要确保不创建循环引用,否则你的内存将永远不会被释放。这将更像 Java(尽管 Java 自动查找循环引用)。 - oli_obk

6

每个资源都可以被一个玩家所拥有。

那就让类型去做吧:

struct Player {
    points: i32,
    resources: Vec<Resource>,
}

struct Resource {
    gold: i32,
}

fn main() {
    let player1 = Player {
        points: 30,
        resources: vec![Resource { gold: 54 }],
    };
    let player2 = Player {
        points: 50,
        resources: vec![Resource { gold: 99 }],
    };

    // If you really need an array of all the resources...
    // Although this seems like you should just ask the Player to do something
    let mut resources: Vec<_> = vec![];
    resources.extend(player1.resources.iter());
    resources.extend(player2.resources.iter());
}

编辑 感谢 @ziggystar 指出我的原始版本只允许玩家拥有一个 Resource。现在,玩家可以拥有 N 个资源,但仍然是唯一的资源所有者。


3
现在每个玩家都拥有一种资源,这和之前不同。(没有读清问题) - ziggystar
1
我修改了我的问题,因为以我之前的陈述方式,你给出的答案是正确的,但它并没有在我理解Rust方面帮助我很多。 - PL_kolek
在编码风格方面,创建结构体实例的首选方式是什么:Player{ 还是 Player {(带有空格)? - Incerteza
@ker,他在哪里说的?http://aturon.github.io/style/braces.html - 没有提到。 - Incerteza
我的意思是官方的样式指南每次都在那里留一个空格。 - oli_obk
他在这里说:http://aturon.github.io/style/whitespace.html 但是http://aturon.github.io是一个非常不稳定的目标。因此,URL很快就会失效。 - zzeroo

0
这是一个常见的问题,Rc<RefCell<T>>是一个常见的答案。它在小例子中运行良好,或者当你只需要一点点共享时。但是当你的程序状态是一个带有循环的图时,Rc往往会导致内存泄漏,而RefCell往往会在运行时出现恐慌。你失去了一些你从Rust中期望的编译时正确性,而且它也非常冗长。这在游戏中尤其是一个问题。
对于游戏和一般具有图形/关系状态的程序来说,一个更好的方法是使用索引而不是引用。你的例子可能看起来像这样(我会避免假设每个资源都由一个玩家唯一引用,以使解决方案更通用):
struct Player {
    resource_ids: Vec<usize>,
    points: i32,
}

struct Resource {
    owner_id: usize,
}

struct GameState {
    players: Vec<Player>,
    resources: Vec<Resource>,
}

fn new_player(state: &mut GameState) -> usize {
    state.players.push(Player { points: 0, resource_ids: Vec::new() });
    state.players.len() - 1
}

fn new_resource(state: &mut GameState, owner_id: usize) -> usize {
    state.resources.push(Resource { owner_id });
    let new_id = state.resources.len() - 1;
    state.players[owner_id].resource_ids.push(new_id);
    state.players[owner_id].points += 30;
    new_id
}

fn main() {
    let mut state = GameState { players: Vec::new(), resources: Vec::new() };
    let player_id = new_player(&mut state);
    let resource_id = new_resource(&mut state, player_id);
}

写游戏是一个深入的话题,我们可能想要以多种方式扩展这个例子。我们可能不想为所有的东西都使用usize,而是想要为PlayerResource使用不同的索引类型,这样我们就不会把它们弄混了。如果我们需要支持删除,我们可能想要使用HashMap而不是Vec。但这只是一个开始。另请参阅使用Rust进行游戏开发对象汤是由索引组成的

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接