Skip to main content
  1. Rust/

Interior Mutability in Rust: Pattern Philosophy, Cell, and RefCell

1410 words·7 mins· loading · loading · · ·Rust-middle
About Rust - This article is part of a series.

Introduction
#

Usually, the borrow checker acts strictly: if a variable is declared without mut, the compiler will never allow modifying its fields through an immutable reference &T. This is standard Inherited Mutability.

However, in real-world code, situations arise where a component needs to store a cached response, record an error log, or update an internal identifier while holding only an immutable reference &self.

For these cases, the Rust standard library provides the Interior Mutability pattern. In this article, I explore how Cell and RefCell work, where they apply in practice, and how their runtime checks operate.


1. The Cell Container: Mutability Without References
#

The simplest tool for interior mutability is std::cell::Cell<T>.

The key property of Cell<T> is that it never hands out direct references &T or &mut T to the value inside it.

Instead of references, Cell operates by reading, writing, or moving values as a whole:

  • get() — returns a copy of the inner value (requires T: Copy).
  • set(val) — completely replaces the inner value with val.
  • replace(val) — stores val inside the container and returns the previous value.
  • take() — extracts the value, leaving Default::default() in its place.
use std::cell::Cell;

struct Counter {
    count: Cell<u32>,
}

fn main() {
    let c = Counter { count: Cell::new(0) };
    
    // The variable 'c' is NOT mutable! But we can freely modify count:
    c.count.set(c.count.get() + 1);
    
    println!("Current count: {}", c.count.get()); // Prints 1
}
Note

Note: For non-Copy types (like String or Vec), calling .get() will fail to compile. However, non-Copy types in Cell can still be updated via .set(), .replace(), and .take().

Tip

Because Cell<T> never issues references to its inner data, it does not need to track borrow counts. It has zero runtime overhead (Zero-cost abstraction) and zero risk of panicking! It is the ideal choice for flags, counters, and Copy types.


2. The RefCell Container: Dynamic Borrow Checking
#

When storing a complex data structure (such as Vec<String>, HashMap, or a custom object) inside a container, copying it as a whole via Cell::get() is not an option.

For complex types, Rust provides std::cell::RefCell<T>.

RefCell<T> lets you request references to the inner value:

  • .borrow() — returns a Ref<T> wrapper that acts like an immutable reference &T.
  • .borrow_mut() — returns a RefMut<T> wrapper that acts like a mutable reference &mut T.
  • .try_borrow() / .try_borrow_mut() — non-panicking variants returning Result, allowing safe checks when borrow conflicts might occur.

Runtime Borrow Checking
#

The standard Rust compiler enforces the rule “either multiple & or exactly one &mut at compile time.

RefCell defers this borrow counter check to runtime.

use std::cell::RefCell;

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

    let mut b1 = data.borrow_mut();
    b1.push(4);

    // WARNING: b1 is still active! Attempting a second borrow_mut will PANIC:
    // let mut b2 = data.borrow_mut(); // ALREADY BORROWED PANIC!
}

Thanks to the RAII idiom, the Ref and RefMut smart pointers automatically decrement the borrow counter of RefCell in their Drop destructor when going out of scope. Therefore, calling drop(b1) explicitly allows the next data.borrow_mut() call to succeed smoothly.

Caution

Remember: RefCell does not bypass Rust borrow rules; it merely defers their enforcement to runtime. Architectural design flaws result in runtime panics rather than compile errors!


3. Real-World Use Cases
#

Now that we understand how Cell and RefCell operate, let’s explore why changing data through an immutable reference &self is necessary when &mut self isn’t feasible.

In real-world code, using &mut self is not always an option. Here are three common scenarios:

1. Button Click Counters
#

Consider a standard UI button component. Let’s try updating a click counter inside a &self method:

Comparison: Compilation Error vs Solution with Cell
Step 1/2
 1// ?compile_fail
 2
 3struct Button {
 4    label: String,
 5    click_count: u32,
 6}
 7
 8impl Button {
 9    fn on_click(&self) {
10        println!("Button '{}' clicked!", self.label);
11        self.click_count += 1;
12    }
13}
14
15fn main() {
16    let btn = Button {
17        label: String::from("Submit"),
18        click_count: 0,
19    };
20    btn.on_click();
21}
22

Without Cell: Compilation Error

  • The compiler prohibits modifying click_count through an immutable reference &self.
  • Declaring the method as &mut self is often impossible in UI components, as buttons are accessed from multiple places simultaneously.

In UI frameworks, event handler methods usually accept &self because button instances are invoked from multiple parts of the application. Cell enables updating the internal click counter directly through &self without propagating &mut self across the entire call chain.

2. Log History Accumulation in Services
#

Another scenario involves a shared application context App, accessed across components via &App to read settings. With RefCell, any function can record warnings into a shared log vector on the fly:

struct App {
    name: String,
    // Log history stored inside the application struct
    logs: RefCell<Vec<String>>,
}

impl App {
    fn log_error(&self, msg: &str) {
        // Appends an entry to the vector holding only &self
        self.logs.borrow_mut().push(msg.to_string());
    }
}

3. State Flags (“Already Initialized”)
#

The same pattern applies to initialization flags in background audio players or loaders:

struct Player {
    is_playing: Cell<bool>,
}

impl Player {
    fn play(&self) {
        if !self.is_playing.get() {
            println!("Starting playback...");
            self.is_playing.set(true);
        }
    }
}

4. Multithreading Constraints
#

An essential caveat: Cell and RefCell containers are designed strictly for single-threaded execution.

Neither type implements the Sync marker trait, meaning the compiler prevents passing references to them across thread boundaries at build time.

A detailed walkthrough of multithreaded interior mutability (Mutex, RwLock) and the Send / Sync traits will be covered in a separate dedicated article.


TypeIssues References?Borrow CheckingType & Trait RequirementsOverhead
Cell<T>No (get/set only)Not requiredRequires T: Copy for .get()Zero-cost
RefCell<T>Yes (Ref / RefMut)Runtime (Panics on violation)Works with any TSmall runtime counter

Let’s explore how these types operate in practical interactive code slides:

Comparing Cell<T> and RefCell<T> Behavior
Step 1/2
 1use std::cell::Cell;
 2
 3struct Logger {
 4    log_count: Cell<usize>,
 5}
 6
 7impl Logger {
 8    fn new() -> Self {
 9        Logger {
10            log_count: Cell::new(0),
11        }
12    }
13
14    // Note: The method takes &self (immutable reference!), but mutates log_count!
15    fn log(&self, message: &str) {
16        println!("[LOG]: {message}");
17        let current = self.log_count.get();
18        self.log_count.set(current + 1);
19    }
20}
21
22fn main() {
23    let logger = Logger::new();
24
25    logger.log("First system log message");
26    logger.log("Second system log message");
27
28    println!("Total log entries: {}", logger.log_count.get());
29
30    // The replace method replaces the value and returns the old one
31    let old_val = logger.log_count.replace(100);
32    println!("Old count: {old_val}, new count: {}", logger.log_count.get());
33}
34

Interior Mutability with Cell

  • Cell<T> allows modifying values inside an object holding only an immutable reference &self.
  • Writing via .set() and reading via .get() works by copying or moving values without issuing direct references &T.
  • Ideal for simple scalar (Copy) types, flags, and counters.

Test Your Knowledge!
#

Take a short interactive quiz on interior mutability:

Article read
Please rate how helpful and clear this article was to you
Article series
About Rust - This article is part of a series.

Related