RustArena

Step 6 of 7

Rc<RefCell<T>>: shared state you can change

Rc<T> gives you many owners, but read-only. RefCell<T> gives you mutation through a shared reference. Put them together, Rc<RefCell<T>>, and you get many owners who can all change the value. It's the standard way to share mutable state in single-threaded Rust (in threaded code the same idea is Arc<Mutex<T>>).

A shared ledger

Every account records its transactions in one bank-wide ledger. Each Account holds its own Rc to the ledger:

use std::cell::RefCell;
use std::rc::Rc;

struct Account {
    name: String,
    balance: i64,
    ledger: Rc<RefCell<Vec<String>>>,
}

impl Account {
    fn new(name: &str, ledger: &Rc<RefCell<Vec<String>>>) -> Account {
        Account { name: name.to_string(), balance: 0, ledger: Rc::clone(ledger) }
    }

    fn deposit(&mut self, amount: i64) {
        self.balance += amount;
        self.ledger.borrow_mut().push(format!("{} +{}", self.name, amount));
    }
}

fn main() {
    let ledger = Rc::new(RefCell::new(Vec::new()));
    let mut alice = Account::new("alice", &ledger);
    let mut bob = Account::new("bob", &ledger);
    alice.deposit(100);
    bob.deposit(40);
    alice.deposit(5);

    println!("alice {}, bob {}", alice.balance, bob.balance);
    for entry in ledger.borrow().iter() {
        println!("{entry}");
    }
    println!("ledger owners: {}", Rc::strong_count(&ledger));
}
alice 105, bob 40
alice +100
bob +40
alice +5
ledger owners: 3

A graph of nodes

Graphs are the other classic: a node can be the target of many edges, so no single edge owns it.

use std::cell::RefCell;
use std::rc::Rc;

struct Room {
    name: String,
    exits: Vec<Rc<RefCell<Room>>>,
}

type RoomRef = Rc<RefCell<Room>>;

fn room(name: &str) -> RoomRef {
    Rc::new(RefCell::new(Room { name: name.to_string(), exits: Vec::new() }))
}

fn connect(from: &RoomRef, to: &RoomRef) {
    from.borrow_mut().exits.push(Rc::clone(to));
}

fn main() {
    let gate = room("gate");
    let hall = room("hall");
    let arena = room("arena");
    connect(&gate, &hall);
    connect(&hall, &arena);
    connect(&gate, &arena);

    // Rename through one handle: every edge sees the change.
    arena.borrow_mut().name = String::from("grand arena");

    for r in [&gate, &hall, &arena] {
        let r = r.borrow();
        let exits: Vec<String> = r.exits.iter().map(|e| e.borrow().name.clone()).collect();
        println!("{} -> {:?}", r.name, exits);
    }
}
gate -> ["hall", "grand arena"]
hall -> ["grand arena"]
grand arena -> []

A type alias (type RoomRef = Rc<RefCell<Room>>;) gives the long type a short name. This graph only has edges going "forward". If two rooms pointed at each other, the counts would never reach zero: that's the last step's topic.

Common mistakes

Your task

Players share one scoreboard. Implement:

Challenge 2

Write fn transfer(from: &AccountRef, to: &AccountRef, amount: u64) -> Result<(), String> for shared accounts:

Challenge 3

A tiny social network: users follow each other. Each User has a name, a list of users they follow, and a follower count. Write:

Previous: RefCell and Cell: changing data behind & Next: Weak<T>: parent links without leaks