🎓 A1Learner
🔍

Rust Interior Mutability

Interior mutability is the pattern that lets a value change behind a shared reference. The borrow rules are checked as the program runs.

The pain: a &self method that must record

send takes &self, a shared borrow. The push needs a mutable borrow, so the compiler refuses:

struct Mock {
    sent: Vec<String>,
}
impl Mock {
    fn send(&self, msg: &str) {
        self.sent.push(String::from(msg));
    }
}
// error[E0596]: cannot borrow `self.sent` as mutable, as it is behind a `&` reference

Change through a shared reference

The cell sits behind a plain shared reference, yet the inside changes: borrow_mut() opens the one allowed write borrow. Two live writers would panic at run time instead of failing to compile.

use std::cell::RefCell;
let cell = RefCell::new(0);
*cell.borrow_mut() += 1;
*cell.borrow_mut() += 1;
println!("value = {}", cell.borrow());

A clash you can handle

try_borrow() and try_borrow_mut() hand back a Result instead of panicking, for when a clash should be handled, not crash:

let cell = RefCell::new(5);
let _writer = cell.borrow_mut();
println!("try_borrow while writing: {}", if cell.try_borrow().is_err() { "busy" } else { "free" });

Swap the value inside

replace swaps the value and hands the old one back. take takes the value out and leaves the type's Default value inside.

let cell = RefCell::new(5);
let old = cell.replace(9);
println!("old = {} new = {}", old, cell.borrow());
let took = cell.take();
println!("took {} left {}", took, cell.borrow());

Set once, lazily: OnceCell

OnceCell is the set-once relative — its thread-safe twin is OnceLock. get_or_init computes the value on first use and keeps it: lazy, one-time setup behind a shared reference.

let cell: OnceCell<i32> = OnceCell::new();
let v = cell.get_or_init(|| 40 + 2);
println!("once = {}", v);

Example

use std::cell::RefCell;
trait Messenger { fn send(&self, msg: &str); }
struct Mock { sent: RefCell<Vec<String>> }
impl Messenger for Mock { fn send(&self, msg: &str) { self.sent.borrow_mut().push(String::from(msg)); } }
fn main() {
    let mock = Mock { sent: RefCell::new(vec![]) };
    mock.send("Warning: over 75%");
    mock.send("Urgent: over 90%");
    println!("recorded = {}", mock.sent.borrow().len());
    println!("first = {}", mock.sent.borrow()[0]);
}

Key points

  • borrow() gives a Ref<T> and borrow_mut() gives a RefMut<T>: both are smart pointers that act like normal references
  • RefCell counts its live borrows.
  • It allows many Ref readers at once, or one RefMut writer.
  • Each count falls when a guard is dropped.
  • Use Rc<RefCell<T>> when you need both at once: many owners, and a value those owners can change
  • It moves the checking from compile time to run time.
  • Same rules, later verdict.
  • There is a small run-time cost for the counting.
  • A value can change behind a shared reference when its cell checks the borrows at run time.

In this example, Mock is a test double: a stand-in messenger that records messages instead of sending them. Its send method takes &self — a shared borrow — yet it must change the list. The list lives in a RefCell, so borrow_mut() opens the one allowed write borrow and push adds the message. After two sends, borrow() opens a read borrow. The count is 2. The first recorded message is the warning. Without the RefCell, the push inside send(&self) would not compile at all.

Note

RefCell does not remove the borrow rules.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A trait method takes &self, but your test double must record each call. What makes that possible?