Interior Mutability in Structs

Change data behind a shared reference with Cell and RefCell, share it with Rc<RefCell<T>>, and know when runtime borrow checks can panic.

Sometimes a struct needs to change a field even though you only have &self. A cache that fills itself on first read, a counter of how many times a method was called, a node in a graph that several owners point at. The normal rules say a &T is read-only, so this looks impossible.

Interior mutability is the escape hatch. Types like Cell<T> and RefCell<T> let you change what's inside them through a shared reference. The borrowing rules still apply. Cell sidesteps them by never handing out references, and RefCell checks them at runtime instead of at compile time.

The problem

struct Counter {
    hits: u32,
}

impl Counter {
    fn record(&self) {
        self.hits += 1; // error[E0594]: cannot assign to `self.hits`, which is behind a `&` reference
    }
}

You could change record to take &mut self, and often you should. But if the struct is shared (for example through Rc), you can't get a &mut at all.

Cell<T>: for small Copy values

Cell<T> lets you get a copy of the value and set a new one. It never gives you a reference to the inside, so there is nothing to borrow-check.

use std::cell::Cell;

struct Counter {
    hits: Cell<u32>,
}

impl Counter {
    fn record(&self) {
        self.hits.set(self.hits.get() + 1);
    }
}

fn main() {
    let c = Counter { hits: Cell::new(0) };
    c.record();
    c.record();
    println!("{}", c.hits.get()); // 2
}

Use Cell for numbers, bool, small enums and other Copy types. For non-Copy values it still offers replace and take, but you can't look at the value in place.

RefCell<T>: borrow checking at runtime

When the value isn't Copy, or you need a reference to it (to push into a Vec, for example), use RefCell<T>.

  • borrow() returns a Ref<T>, which acts like &T.
  • borrow_mut() returns a RefMut<T>, which acts like &mut T.
use std::cell::RefCell;

struct Logger {
    lines: RefCell<Vec<String>>,
}

impl Logger {
    fn log(&self, msg: &str) {
        self.lines.borrow_mut().push(msg.to_string());
    }

    fn count(&self) -> usize {
        self.lines.borrow().len()
    }
}

fn main() {
    let logger = Logger { lines: RefCell::new(Vec::new()) };
    logger.log("started");
    logger.log("loaded config");
    println!("{} lines", logger.count()); // 2 lines
}

RefCell keeps a counter of active borrows. The rules are the same as normal references: many borrow()s or one borrow_mut(). The difference is when you find out you broke them.

Breaking the rules panics

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    let first = data.borrow();       // shared borrow is still alive...
    data.borrow_mut().push(4);       // ...so this panics: already borrowed: BorrowMutError
    println!("{first:?}");
}

This compiles fine and crashes at runtime. Two habits avoid it:

  1. Keep borrows short. Call borrow_mut() inside a small block or a single expression so the guard drops right away.
  2. Use try_borrow_mut() when a conflict is possible and you'd rather handle it than panic.
if let Ok(mut v) = data.try_borrow_mut() {
    v.push(4);
}

Rc<RefCell<T>>: shared and mutable

Rc<T> gives you several owners of one value, but only shared access. Wrap the value in a RefCell and every owner can change it:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Account {
    balance: i64,
}

fn main() {
    let shared = Rc::new(RefCell::new(Account { balance: 100 }));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    a.borrow_mut().balance -= 30;
    b.borrow_mut().balance += 5;

    println!("{:?}", shared.borrow()); // Account { balance: 75 }
}

This is common for trees, graphs and UI state. Watch for reference cycles: two Rcs that point at each other are never freed. Use Weak<T> for back-pointers such as a child's link to its parent.

Threads need different types

Cell and RefCell are not thread-safe (!Sync), and Rc is not Send. The compiler stops you from sharing them across threads. The thread-safe versions are:

Single threadAcross threads
Cell<T> (numbers)AtomicU32, AtomicBool...
RefCell<T>Mutex<T> or RwLock<T>
Rc<RefCell<T>>Arc<Mutex<T>>

Which one to pick

  • You can use &mut self? Do that. No interior mutability needed.
  • Small Copy value behind &self? Cell<T>.
  • Anything else behind &self? RefCell<T>, with short borrows.
  • Several owners that all change it? Rc<RefCell<T>>.
  • Several threads? Arc<Mutex<T>> or atomics.

Interior mutability isn't a way around the borrow checker. The same rules hold, and you take responsibility for checking them at runtime. Use it where shared mutation really is the design, and keep it inside the struct so callers see a simple API.