RustArena

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.

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:

Common mistakes

Your task

A ticket desk is shared through plain & references, so its methods take &self. Implement:

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