Rust Interior Mutability: Cell and RefCell
Rust's ownership system enforces a fundamental rule: you can have either multiple immutable references or one mutable reference to data, but never both simultaneously. This prevents data races at...
Key Insights
- Interior mutability allows mutation through shared references by moving borrow checking from compile-time to runtime, providing a controlled escape hatch from Rust’s strict ownership rules
Cell<T>works by copying values and is zero-cost for Copy types, whileRefCell<T>uses runtime borrow checking and works with any type but can panic if borrow rules are violated- Use interior mutability sparingly for specific patterns like caching, mock objects, and graph structures—not as a general workaround for fighting the borrow checker
Introduction to Interior Mutability
Rust’s ownership system enforces a fundamental rule: you can have either multiple immutable references or one mutable reference to data, but never both simultaneously. This prevents data races at compile time, but it also creates situations where you legitimately need to mutate data that’s behind a shared reference.
Consider this common scenario that won’t compile:
struct Counter {
count: i32,
}
impl Counter {
fn increment(&self) {
self.count += 1; // Error: cannot mutate through &self
}
}
The compiler rejects this because &self is an immutable reference, yet we’re trying to mutate count. The obvious solution is to use &mut self, but what if you have multiple references to the same Counter? Or what if you’re implementing a trait that requires &self?
Interior mutability solves this by providing types that allow mutation through shared references while maintaining Rust’s safety guarantees. Instead of compile-time checks, these types enforce borrowing rules at runtime or through other mechanisms. The two fundamental types for interior mutability are Cell<T> and RefCell<T>.
Understanding Cell
Cell<T> provides interior mutability for types that implement the Copy trait. It works by replacing the entire value rather than providing direct access to the inner data. This approach is completely safe because Copy types can be duplicated freely without ownership concerns.
Here’s how Cell solves our counter problem:
use std::cell::Cell;
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn new() -> Self {
Counter { count: Cell::new(0) }
}
fn increment(&self) {
let current = self.count.get();
self.count.set(current + 1);
}
fn get(&self) -> i32 {
self.count.get()
}
}
fn main() {
let counter = Counter::new();
counter.increment();
counter.increment();
println!("Count: {}", counter.get()); // Prints: Count: 2
}
The get() method returns a copy of the inner value, and set() replaces it entirely. This works perfectly for small Copy types like integers, booleans, and simple structs.
A practical example demonstrates Cell’s utility when you need shared mutable state:
use std::cell::Cell;
struct Cache {
hits: Cell<u64>,
misses: Cell<u64>,
}
impl Cache {
fn new() -> Self {
Cache {
hits: Cell::new(0),
misses: Cell::new(0),
}
}
fn record_hit(&self) {
self.hits.set(self.hits.get() + 1);
}
fn record_miss(&self) {
self.misses.set(self.misses.get() + 1);
}
fn stats(&self) -> (u64, u64) {
(self.hits.get(), self.misses.get())
}
}
Cell has zero runtime overhead for Copy types—it’s as efficient as direct mutation. The trade-off is that you can’t get references to the inner value; you must copy it out and set it back.
Understanding RefCell
RefCell<T> provides interior mutability for any type, not just Copy types. It enforces Rust’s borrowing rules at runtime rather than compile time. When you borrow the contents, RefCell tracks whether borrows are active and panics if you violate the rules.
Here’s basic RefCell usage:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow
{
let borrowed = data.borrow();
println!("Length: {}", borrowed.len());
} // borrowed dropped here
// Mutable borrow
{
let mut borrowed_mut = data.borrow_mut();
borrowed_mut.push(4);
} // borrowed_mut dropped here
println!("{:?}", data.borrow()); // Prints: [1, 2, 3, 4]
}
The borrow() method returns a Ref<T> (similar to &T), and borrow_mut() returns a RefMut<T> (similar to &mut T). These smart pointers track active borrows.
Here’s what happens when you violate borrowing rules:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(String::from("hello"));
let borrow1 = data.borrow();
let borrow2 = data.borrow_mut(); // Panics! Already borrowed immutably
println!("{}", borrow1);
}
This code panics at runtime with “already borrowed: BorrowMutError”. The panic occurs because you can’t have a mutable borrow while immutable borrows exist.
Common Use Cases
Interior mutability shines in specific patterns where Rust’s standard borrowing rules are too restrictive.
Caching and Memoization: Computing expensive values once and storing them:
use std::cell::RefCell;
use std::collections::HashMap;
struct Fibonacci {
cache: RefCell<HashMap<u64, u64>>,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci {
cache: RefCell::new(HashMap::new()),
}
}
fn calculate(&self, n: u64) -> u64 {
if n <= 1 {
return n;
}
// Check cache first
if let Some(&result) = self.cache.borrow().get(&n) {
return result;
}
// Calculate and cache
let result = self.calculate(n - 1) + self.calculate(n - 2);
self.cache.borrow_mut().insert(n, result);
result
}
}
fn main() {
let fib = Fibonacci::new();
println!("fib(10) = {}", fib.calculate(10)); // 55
println!("fib(20) = {}", fib.calculate(20)); // 6765
}
Graph and Tree Structures: Nodes that reference each other:
use std::cell::RefCell;
use std::rc::Rc;
type NodeRef = Rc<RefCell<Node>>;
struct Node {
value: i32,
children: Vec<NodeRef>,
}
impl Node {
fn new(value: i32) -> NodeRef {
Rc::new(RefCell::new(Node {
value,
children: Vec::new(),
}))
}
fn add_child(parent: &NodeRef, child: NodeRef) {
parent.borrow_mut().children.push(child);
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
Node::add_child(&root, child1);
Node::add_child(&root, child2);
println!("Root has {} children", root.borrow().children.len());
}
Cell vs RefCell: When to Use Which
Choose Cell<T> when:
- Your type implements
Copy - You need zero-cost abstraction
- You’re storing simple values like counters or flags
Choose RefCell<T> when:
- Your type doesn’t implement
Copy - You need references to the inner data
- You want compile-time-like ergonomics with
&and&mut
use std::cell::{Cell, RefCell};
// Good use of Cell: simple counter
struct Metrics {
request_count: Cell<u64>,
}
// Good use of RefCell: complex data
struct Cache {
entries: RefCell<Vec<String>>,
}
// Bad: using RefCell for Copy types (unnecessary overhead)
struct BadCounter {
count: RefCell<i32>, // Use Cell<i32> instead
}
Performance-wise, Cell has zero overhead, while RefCell adds runtime checks. For most applications, RefCell’s overhead is negligible, but in hot paths with millions of borrows per second, it matters.
Pitfalls and Best Practices
The biggest danger with RefCell is runtime panics. Always consider whether you can structure your code to avoid interior mutability first.
To handle borrows safely, use try_borrow() and try_borrow_mut():
use std::cell::RefCell;
fn safe_borrow_example() {
let data = RefCell::new(vec![1, 2, 3]);
let borrow1 = data.borrow();
// This won't panic, it returns Result
match data.try_borrow_mut() {
Ok(_) => println!("Got mutable borrow"),
Err(_) => println!("Already borrowed, skipping mutation"),
}
}
Never use Cell or RefCell for thread-safe code. They’re explicitly not Sync, meaning they can’t be shared between threads. For concurrent scenarios, use Mutex, RwLock, or atomic types instead.
Avoid overusing interior mutability as a crutch. If you find yourself wrapping everything in RefCell, you’re probably fighting the borrow checker instead of working with it. Restructure your code to use standard borrowing patterns when possible.
Conclusion
Interior mutability is a powerful tool for specific scenarios where Rust’s borrowing rules are legitimately too restrictive. Cell<T> provides zero-cost mutation for Copy types, while RefCell<T> offers runtime-checked borrowing for any type.
Use these types judiciously for patterns like caching, mock objects in tests, and complex data structures like graphs. They’re not a general solution for avoiding the borrow checker—they’re specialized tools for specialized problems. When you do need interior mutability, understand the trade-offs: Cell for simplicity and performance, RefCell for flexibility and ergonomics, and always prefer standard Rust patterns when they suffice.