ProgrammingSystems Programmer

What is interior mutability in Rust, and how do Cell and RefCell allow modifying data within immutable structures?

Pass interviews with Hintsage AI assistant

Answer.

Background

One of Rust's primary goals is to prevent the mutation of immutable data and avoid data races at compile time. Under normal circumstances, Rust does not allow changing data through an immutable reference. However, in systems with caching, lazy computations, or logic that requires modifying internal state through a reference, this can become necessary. This is where the interior mutability pattern comes into play.

The Problem

Without interior mutability, implementing caches, lazy initialization, and many other idioms can be difficult or impossible while maintaining safe ownership and references. A classic example is a cache inside a function that provides the outside world with only an immutable reference of itself.

The Solution

Rust has special types — Cell and RefCell (and their multi-threaded counterparts) that allow modifying the internal value even through an immutable reference, controlling safety at runtime rather than compile time.

  • Cell<T> is a copy primitive that allows modifying or reading a value without using references, but only for types that implement Copy.
  • RefCell<T> allows obtaining a mutable reference "on the fly" even with only an immutable external reference, but if an attempt is made to obtain two mutable references or one mutable and several immutable references simultaneously, it will panic at runtime.

Code example:

use std::cell::RefCell; struct Foo { cache: RefCell<Option<u32>>, } impl Foo { fn get_or_compute(&self) -> u32 { if let Some(val) = *self.cache.borrow() { return val; } let computed = 42; *self.cache.borrow_mut() = Some(computed); computed } }

Key features:

  • Allows modifying data within an object, even if everything around it is declared as immutable
  • Violations of ownership and reference rules can only happen at runtime (via panic), so caution is needed
  • Main types: Cell, RefCell (single-threaded environment), Mutex, RwLock (multi-threaded)

Tricky Questions.

Can we safely use RefCell for multi-threaded structures?

No, RefCell is not thread-safe. For multi-threaded environments, use Mutex or RwLock.

Can a reference to the contents of Cell<T> be returned?

No, Cell does not yield any references, only copies or updates the value. It only works with Copy types; for all others, use RefCell.

What happens if borrow_mut is called twice in a row on the same RefCell?

It will cause a panic at runtime, as RefCell tracks the number of active references. The second attempt to gain mutable access while an existing reference is present will lead to a panic.

Common Mistakes and Anti-patterns

  • Storing RefCell in a global or static context and forgetting about its single-threaded nature
  • Unjustified use of RefCell instead of mutable ownership and references, making the code more complex and slower
  • Neglecting panic handling inside RefCell

Real-life Example

Negative Case

In a project, RefCell is always used to store any mutable state within a structure even when mutable ownership could be utilized. The code becomes verbose, runtime panics occur, and testing becomes challenging.

Pros: Can quickly bypass compiler restrictions to achieve the desired behavior.

Cons: High risk of errors at runtime, application crashes, difficult debugging of logic.

Positive Case

RefCell is used only for implementing lazy caches in large structures, while for everything else, classic ownership and references are maintained. All values are cleared correctly, no panics occur.

Pros: Transparent logic, minimal use of interior mutability, the code is robust and predictable.

Cons: Requires careful attention to the boundaries of data modification: reading the cache is always safe, writing should only occur as necessary and in narrowly defined places.