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
self.ledger.borrow_mut(): the method call looks through theRc(Deref) to theRefCell, then borrows it mutably for this one statement.- All three owners (
ledger,alice.ledger,bob.ledger) see the same three entries.
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
- Borrowing the same RefCell twice in one operation. A
transfer(&acct, &acct, 10)that callsfrom.borrow_mut()andto.borrow_mut()panics when both are the same account. CheckRc::ptr_eq(from, to)first. - Holding a borrow across a call that borrows again, as in the previous step.
Rc<RefCell<T>>everywhere. In code review it's often a sign that ownership wasn't thought through. Many designs are simpler with one owner (say, aVec<Room>) and indexes instead of pointers. Use shared mutable state when the problem really has it.
Your task
Players share one scoreboard. Implement:
Player::join(name, board) -> Player: keeps anRcto the board and adds the name to it with 0 points.Player::score(&self, points): adds the points to this player's entry on the shared board.points_of(board, name) -> u32: the player's points, or 0 if they never joined.
Challenge 2
Write fn transfer(from: &AccountRef, to: &AccountRef, amount: u64) -> Result<(), String> for shared accounts:
- If
fromandtoare the same account (Rc::ptr_eq), returnErr("same account".to_string()). Without this check, borrowing it mutably twice would panic. - If
fromhas less thanamount, returnErr("insufficient funds".to_string())and change nothing. - Otherwise move the money and return
Ok(()).
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:
fn user(name: &str) -> UserRef: a new user with no follows and 0 followers.fn follow(who: &UserRef, target: &UserRef):whofollowstarget(push a clone oftargetintowho'sfollowing, add 1 totarget'sfollowers). Following yourself or someone you already follow does nothing.fn most_followed(users: &[UserRef]) -> Option<String>: the name of the user with the most followers (the first one listed wins a tie),Nonefor an empty slice.
Previous: RefCell and Cell: changing data behind & Next: Weak<T>: parent links without leaks