Updated Aug 5, 2026

17 - Interior Mutability

📋 Jump to Takeaways

🎁 The borrow checker says you can't mutate through a shared reference. But what if you need to? A cache that fills on first access, a counter inside a shared struct, multiple owners that all need to write. Rust has a way, and it doesn't require unsafe.

The Problem

The borrow checker enforces one rule: if you only have &T (a shared reference), you can't mutate. This comes up in two common situations:

  1. &self methods — you want to mutate a field (a cache, a counter, a log buffer) but the method signature only gives you shared access
  2. Through Rc<T>Rc only hands out &T, never &mut T, because multiple owners exist

In both cases, the compiler can't prove mutation is safe at compile time, so it rejects it. The types in this lesson are the escape hatch: they move the borrow check to runtime.

RefCell: Runtime Borrow Checking

RefCell<T> wraps a value and lets you get &mut T from a &T. The same borrowing rules apply (one mutable OR many shared), but they're checked when you call .borrow() or .borrow_mut() instead of at compile time. Violations panic instead of failing to compile.

use std::cell::RefCell;

struct Cache {
    data: RefCell<Vec<i32>>,
}

impl Cache {
    fn add(&self, val: i32) {          // &self, not &mut self
        self.data.borrow_mut().push(val);
    }

    fn get(&self) -> Vec<i32> {
        self.data.borrow().clone()
    }
}

fn main() {
    let cache = Cache { data: RefCell::new(vec![]) };
    cache.add(1);
    cache.add(2);
    println!("{:?}", cache.get()); // [1, 2]
}

.borrow() gives a shared reference, .borrow_mut() gives a mutable one. The same rules as always, just checked at runtime. Holding both at the same time panics:

let data = RefCell::new(vec![1, 2]);
let _a = data.borrow();
let _b = data.borrow_mut(); // panics: already borrowed as immutable

Use RefCell sparingly. If you find yourself reaching for it often, it usually means the struct's design can be improved to make the mutability explicit.

Cell: Copy-Based Interior Mutability

Cell<T> solves the same problem as RefCell<T> (mutating through &self) but for simple Copy types like integers and booleans. Instead of issuing references that need runtime borrow tracking, it just copies the value in and out with .get() and .set(). No borrow check, no panic possible.

use std::cell::Cell;

fn main() {
    let counter = Cell::new(0);

    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);

    println!("count: {}", counter.get()); // count: 2
}

A practical use is a struct that tracks internal state through &self methods, something the borrow checker would normally forbid:

use std::cell::Cell;

struct Button {
    label: String,
    click_count: Cell<u32>,
}

impl Button {
    fn click(&self) {                              // &self, not &mut self
        self.click_count.set(self.click_count.get() + 1);
    }

    fn clicks(&self) -> u32 {
        self.click_count.get()
    }
}

fn main() {
    let btn = Button { label: "Submit".to_string(), click_count: Cell::new(0) };
    btn.click();
    btn.click();
    println!("{} clicked {} times", btn.label, btn.clicks()); // Submit clicked 2 times
}

Cell<T> only works with Copy types (integers, booleans, char). For String, Vec, or anything heap-allocated, use RefCell<T> instead. It issues references rather than copying.

Cell vs RefCell

Cell<T> RefCell<T>
Works with Copy types only Any type
API .get() / .set() (copies value) .borrow() / .borrow_mut() (returns references)
Can panic? No Yes, if you violate borrow rules
Use when You need a simple counter or flag You need to mutate a Vec, String, or complex type

Threading Equivalents

Cell<T> is not thread-safe. It doesn't implement Sync, so the compiler rejects it at thread boundaries. Think of it as the single-threaded equivalent of an atomic variable. When you cross a thread boundary, swap it for the real thing:

Need Single-threaded Multi-threaded
Mutate an integer through &self Cell<i32> AtomicI32
Mutate a bool through &self Cell<bool> AtomicBool
Mutate a Vec or String through &self RefCell<T> Mutex<T>

Atomics use atomic CPU instructions for thread-safe access. Cell opts for no overhead at all (single-threaded only), while atomics use CPU-level synchronization so multiple threads can safely read and write without a lock.

Rc<RefCell>: Shared Mutable Data

Rc<T> gives you multiple owners. RefCell<T> gives you interior mutability. Combine them and you get data that multiple parts of the program can own and mutate.

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

fn main() {
    let shared = Rc::new(RefCell::new(0));
    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    *a.borrow_mut() += 10;
    *b.borrow_mut() += 5;

    println!("shared: {}", shared.borrow()); // shared: 15
}

Why does this need RefCell? Because Rc only gives you &T. Without RefCell, you'd have three read-only handles to the same integer. RefCell lets each handle call .borrow_mut() and get &mut T at runtime.

This pattern has a direct multi-threaded equivalent: Arc<Mutex<T>>. Arc is the thread-safe version of Rc, and Mutex is the thread-safe version of RefCell. For single-threaded code, Rc<RefCell<T>> is cheaper because neither type uses atomic operations or OS locks.

Key Takeaways

  • RefCell<T> defers borrow checks to runtime; violations panic instead of failing to compile
  • .borrow() gives a shared reference, .borrow_mut() gives a mutable one; overlapping them panics
  • RefCell is for when you only have &T but need &mut T (both &self methods and through Rc)
  • Cell<T> is interior mutability for Copy types; moves values in/out with .get()/.set(), no borrow check or panic possible
  • Cell<T> and RefCell<T> are single-threaded only; equivalents across threads are atomics and Mutex<T>
  • Rc<RefCell<T>> combines shared ownership with interior mutability for single-threaded code
  • The multi-threaded equivalent is Arc<Mutex<T>>

🎁 Your data is owned, shared, and mutated safely. But how do you split a growing codebase across multiple files and share it with the world? Next up: Rust's module system, crates, and how Cargo wires them all together.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy