Rust’s borrowing rules are strict: you can have either multiple immutable references (&T) or one mutable reference (&mut T), but not both. This is checked at compile time. However, sometimes these rules are too restrictive. For instance, in a mock object used for testing, you might need to modify an internal value to record that a method was called, even though the mock object itself is borrowed immutably.
This is where the interior mutability pattern comes in. It’s a design pattern in Rust that allows you to mutate data even when there are immutable references to it. Instead of enforcing the borrowing rules at compile time, types that provide interior mutability enforce them at runtime. If you violate the rules, your program will panic.
The primary type for this in single-threaded code is RefCell<T>.
RefCell<T>: Runtime Borrow Checking
RefCell<T> is a smart pointer that owns its data. Unlike Box<T>, it doesn’t affect whether its data is on the stack or heap. Its special power is that it moves Rust’s borrow checking from compile time to runtime.
With RefCell<T>, you use the borrow() and borrow_mut() methods to get smart pointers (Ref<T> and RefMut<T>, respectively) that implement Deref.
borrow(): Returns aRef<T>for an immutable borrow. You can have multiple of these.borrow_mut(): Returns aRefMut<T>for a mutable borrow. You can only have one of these.
If you try to get a mutable borrow while an immutable one is active, or get a second mutable borrow before the first is dropped, your program will panic at runtime.
Use Case: A Mock Object for Testing
Let’s create a simple Messenger trait and a mock object to test it. The mock object needs to track how many messages it has sent, which requires mutating its internal state.
use std::cell::RefCell;
pub trait Messenger {
fn send(&self, msg: &str);
}
struct MockMessenger {
// Use RefCell to allow mutation of sent_messages via an immutable reference.
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
// `send` takes an immutable reference to self.
fn send(&self, msg: &str) {
// We can still get a mutable reference to the data inside the RefCell.
self.sent_messages.borrow_mut().push(String::from(msg));
}
}
fn main() {
let mock_messenger = MockMessenger::new();
mock_messenger.send("Hello, world!");
// We can borrow it immutably to check the messages.
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
Here, send only takes &self, but we can still modify sent_messages because it’s wrapped in a RefCell. The call to borrow_mut() gives us a mutable reference that is checked at runtime.
Combining Rc<T> and RefCell<T>
A very common pattern is Rc<RefCell<T>>. This combination allows you to have a value with multiple owners (Rc<T>) that can also be mutated (RefCell<T>).
Let’s modify the Cons list example from the reference counting article to hold a mutable value.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
// Mutate the value through our first handle `value`.
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
When you run this, you’ll see that the value 5 has been changed to 15, and this change is visible through all three lists (a, b, and c) because they all share ownership of the same RefCell<i32> via Rc.
Cell<T> vs. RefCell<T>
Another type for interior mutability is Cell<T>. It’s similar but has different trade-offs:
Cell<T>:- The value is moved in and out. You use
get()to get a copy of the value andset()to replace it. - It only works for types that implement the
Copytrait. - There is no runtime checking and no panics; it’s always safe.
- The value is moved in and out. You use
RefCell<T>:- The value is borrowed, not moved.
borrow()andborrow_mut()return smart pointers. - It works for any type.
- It enforces borrowing rules at runtime and will panic if they are violated.
- The value is borrowed, not moved.
Thread-Safe Interior Mutability: Mutex<T> and RwLock<T>
Both RefCell<T> and Cell<T> are for single-threaded use only. If you try to share them across threads, you’ll get a compile-time error.
For thread-safe interior mutability, Rust provides Mutex<T> and RwLock<T>. They use locking mechanisms to ensure that only one thread can access the data at a time (or multiple readers for RwLock), making them safe for concurrent code. The common pattern for shared, mutable state in a multi-threaded context is Arc<Mutex<T>>.
Conclusion
Interior mutability is a powerful but advanced feature. It allows you to bend Rust’s strict compile-time borrowing rules by enforcing them at runtime, giving you more flexibility in certain situations.
- Use
Cell<T>forCopytypes where you just need to swap values. - Use
RefCell<T>for single-threaded scenarios where you need to borrow mutable access to data within an immutable context. - Use
Mutex<T>orRwLock<T>(often withArc<T>) for thread-safe interior mutability.