Step 5 of 7
RefCell and Cell: changing data behind &
The borrow checker enforces "many readers or one writer" at compile time. Sometimes your code follows that rule, but in a way the compiler can't see: a cache filled inside a method that takes &self, a counter of how many times something was looked up, or (next step) data shared through an Rc. For those cases Rust has interior mutability: types that let you change their contents through a shared & reference, checking the rules another way.
RefCell<T>:.borrow()gives you aRef<T>(acts like&T),.borrow_mut()gives you aRefMut<T>(acts like&mut T). The same rule applies, but it's checked at run time: RefCell counts the borrows that are alive, and breaking the rule panics.Cell<T>: for smallCopyvalues like numbers and bools..get()returns a copy,.set(v)replaces the value. It never hands out a reference into itself, so it can never panic.
use std::cell::{Cell, RefCell};
struct Inventory {
items: RefCell<Vec<String>>,
lookups: Cell<u32>,
}
impl Inventory {
fn add(&self, item: &str) {
// &self, not &mut self: the RefCell allows the change
self.items.borrow_mut().push(item.to_string());
}
fn has(&self, item: &str) -> bool {
self.lookups.set(self.lookups.get() + 1);
self.items.borrow().iter().any(|i| i == item)
}
}
fn main() {
let inv = Inventory { items: RefCell::new(Vec::new()), lookups: Cell::new(0) };
inv.add("sword");
inv.add("shield");
println!("{}", inv.has("sword"));
println!("{}", inv.has("bow"));
println!("{} items, {} lookups", inv.items.borrow().len(), inv.lookups.get());
}
true
false
2 items, 2 lookups
Notice inv isn't even declared mut. Each borrow_mut() lasts only for its statement, so the borrows never overlap and nothing panics.
When the runtime check fails
use std::cell::RefCell;
fn main() {
let log = RefCell::new(vec![String::from("start")]);
let first = log.borrow(); // a shared borrow, still alive below...
println!("first entry: {}", first[0]);
log.borrow_mut().push(String::from("next")); // ...so this panics
println!("never printed");
}
first entry: start
The program stops with:
thread 'main' (4321) panicked at src/main.rs:7:9:
RefCell already borrowed
first holds a Ref, and a Ref stays alive until the end of its scope (it has to run code when it's dropped, to lower the borrow count), not just until its last use. So borrow_mut() finds a reader and panics. The error behind it is called BorrowMutError (older Rust versions print already borrowed: BorrowMutError). Fixes:
- End the first borrow before mutating: put it in an inner
{ }block, calldrop(first), or copy out what you need (let first = log.borrow()[0].clone();). - Or ask instead of panicking:
log.try_borrow_mut()returns aResult,Err(BorrowMutError)when the value is busy.
Common mistakes
- Holding a
borrow()while something else callsborrow_mut(), often hidden in another method. Keep borrows short: borrow, use, let go. Cellwith a non-Copytype:cell.get()on aCell<String>doesn't compile becausegetneedsT: Copy. UseRefCell(orCell::take/Cell::replace).- Using RefCell where
&mut selfwould do. Compile-time checks are free and can't panic. Reach for RefCell only when the compiler can't see that your borrows are fine. - RefCell and Cell are single-threaded. The thread-safe equivalents are
Mutexand atomics.
Your task
A ticket desk is shared through plain & references, so its methods take &self. Implement:
TicketDesk::new(): no tickets yet; the first ticket will get id 1.open(&self, title: &str) -> u32: gives the ticket the next id (1, 2, 3, ...), records#<id> <title>in the log, and returns the id.log_len(&self) -> usizeandlast_entry(&self) -> Option<String>(a copy of the newest log line).
Use a Cell<u32> for the next id and a RefCell<Vec<String>> for the log.
Challenge 2
This program panics with RefCell already borrowed: it pushes to the queue while it's still iterating over it. It's meant to add, for every job greater than 1, a new job worth 10 times as much at the end of the queue, then print the queue:
[3, 1, 2, 30, 20]
Keep the RefCell. Collect the new jobs first, and push them with queue.borrow_mut() once the read borrow has ended.
Challenge 3
A Report computes its total lazily: the first call to total() adds up the numbers and caches the result, later calls return the cached value. total takes &self, so use a Cell<Option<u32>> for the cache (Option<u32> is Copy, so Cell works).
Also count how many times the total was actually computed in computations: Cell<u32>, so the tests can check the caching works. Write Report::new(data) and total(&self) -> u32.
Previous: Rc<T>: shared ownership Next: Rc<RefCell<T>>: shared state you can change