Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
877 views
in Technique[技术] by (71.8m points)

rust - How to represent shared mutable state?

I'm trying to learn Rust, but the only thing I do is keep hitting the wall trying to shoehorn familiar (to me) Java concepts into its type system. Or try to shoehorn Haskell concepts, etc.

I want to write a game with a Player and many Resources. Each Resource can be owned by one Player:

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;
}

It doesn't compile, because I can't have the resource point to player, while at the same time modifying it:

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

Moreover, if the Resource owned a mutable reference to Player, I couldn't even have two Resources with the same owner.

What is the Rust way to solve such cases?


I oversimplified my question, and while Shepmaster's answer is a correct answer to it, it's not what I wanted to get (because what I asked was not what I really wanted to ask). I'll try to rephrase it and add more context.

  1. The resources are connected in some way - the map of all resources forms a (un)directed graph.
  2. Each player can own many resources, each resource can be owned by one player. The player should be able to get points from resources they own. I thought of a signature like: fn addPoints(&mut self, allResources: &ResourcesMap) -> ().
  3. The player can take over a resource connected to one of their resources from another player. It could result in some points loss for the other player.

Problems:

  1. How to represent such graph in Rust (a possibly cyclic structure, where each node can be pointed to from many nodes)?
  2. The original problem: if the Resource points to a Player, I can't modify the player!

Resources point to Player because - the natural way to do such an operation would be to start from some of Player A's resources, move through the map to a player's B resource and from that resource to player B to subtract the points. It just doesn't seem natural in Rust (at least for me).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The cell documentation page has rather good examples. Rust will always try to protect you from doing bad things (like having two mutable references to the same thing). Therefor it's not quite as "easy" as using Rust's built-in references, since you need to do runtime-checking (Rust references are checked at compile-time).

The RefCell type exists just for that. It checks the mutability rules at runtime. You will get some memory and computation-time overhead, but you end up with the same memory-safety that Rust promises in it's compile-time checks.

Your example ported to RefCell looks like the following.

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());
}

My concern is, if what I'm trying to do is trying to write Java code in Rust, can it be done in a Rust-way without sacrificing compile time safety? Avoid that shared mutable state at all?

You are not sacrificing compile-time-safety. Rust makes sure (at compile-time) that you are using your libraries correctly. Still, your program might panic at runtime if you use the borrow* functions. If you use the try_borrow* functions instead, you can check if it succeeded and if not, do some fallback operation.

You can also use a reference counted box of a RefCell to your type (Rc<RefCell<Player>>). Then you only need to make sure that you do not create cycles, or your memory will never be freed. This would be much more Java like (although Java automatically finds cycles).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...