Skip to content
Lesson 2 of 7

Step 1 of 4 · Reading · ~3 min

Learn

Smart Pointers and Interior Mutability

Cell, RefCell and Interior Mutability

After this lesson you can write &self methods that update state, pick the right wrapper for it, and avoid the one mistake that makes RefCell panic in real programs. In the intermediate tier you used RefCell and Mutex as tools; here you see what they share and where they bite.

One primitive underneath

A &T normally promises that nobody mutates the value. UnsafeCell<T> is the one type the compiler exempts from that promise, and every interior-mutability type wraps it and adds its own rule so that aliasing stays safe:

TypeThe ruleThread-safe
Cell<T>values are copied or moved in and out, a reference is never handed outno
RefCell<T>borrows are counted at run time, a violation panicsno
Mutex<T>, RwLock<T>a lock, other threads waityes
atomicssingle hardware operationsyes

Cell: values in, values out

A Cell never lends a reference, so there is no borrow to track and nothing can panic. get needs T: Copy; for anything else use take (leaves Default behind) or replace.

struct Sensor { level: i32, reads: Cell<u32> }
impl Sensor {
    fn read(&self) -> i32 {
        self.reads.set(self.reads.get() + 1);
        self.level
    }
}
// s.read(); s.read(); println!("{} {}", s.read(), s.reads.get());   // 7 3

let slot = Cell::new(Some(String::from("job-9")));
println!("{:?} {:?}", slot.take(), slot.take());   // Some("job-9") None

Cell::new(String::new()).get() is rejected: String : std::marker::Copy is not satisfied.

RefCell: a counter and a guard

borrow and borrow_mut return guards (Ref, RefMut) that decrement the cell's counter in Drop. The check can be asked without panicking:

let log = RefCell::new(vec![1, 2]);
let first = log.borrow();
println!("{}", log.try_borrow_mut().is_err());   // true
drop(first);
println!("{}", log.try_borrow_mut().is_ok());    // true

Ref::map narrows a guard to one field or element without cloning, and the cell stays borrowed for as long as the narrowed guard lives:

fn first(&self) -> Ref<'_, String> { Ref::map(self.names.borrow(), |v| &v[0]) }

The trap: a temporary guard that lives too long

A temporary created in the scrutinee of match, if let or for lives until the end of the whole statement, arms included. This memoizing get looks fine and panics:

match self.memo.borrow().get(&n) {            // the Ref lives to the closing brace
    Some(v) => *v,
    None => {
        let r = self.get(n - 1) + self.get(n - 2);
        self.memo.borrow_mut().insert(n, r);  // panics
        r
    }
}
thread 'main' panicked at 'already borrowed: BorrowMutError'

Copy the answer out first so the guard is gone before anything else runs:

let cached = self.memo.borrow().get(&n).copied();   // guard dropped at the `;`
match cached { Some(v) => v, None => { /* same arm, no panic */ } }

With that fix get(10) returns 55 after 19 calls, counted in a Cell<u32> on the same &self. The rule: never hold a guard across a call that might reach the same cell, and recursion always might.

The thread-safe siblings

Cell and RefCell update plain memory, so two threads sharing &Cell would race. They are Send but not Sync:

error[E0277]: `std::cell::Cell<u32>` cannot be shared between threads safely

Mutex and RwLock block instead of panicking, and a counter needs only an atomic: four threads each doing hits.fetch_add(1, Ordering::SeqCst) a thousand times on an Arc<AtomicUsize> end at 4000. Use SeqCst until you have a reason not to.

Common mistakes

  • Looping over self.rules.borrow().iter() and calling something that does borrow_mut() inside: it panics with already borrowed.
  • Re-reading the list by index on every iteration to avoid the guard: items added during the loop run in the same pass, so the output gains lines.
  • Raising a Cell counter on the way in and skipping the lower on an early return: the counter drifts and later calls misbehave.
  • Calling get on a Cell of a non-Copy type: compile error, use take or replace.

Your exercise

Write emit for an event bus whose handlers can register new rules while it runs. Everything else is in the starter. The likeliest mistake is keeping the rules guard alive while an action runs.

Up nextDeclarative Macros (macro_rules!)Macros and Unsafe

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…