RefCell in Rust
Once Cell starts making sense, RefCell becomes much easier to understand.
Both exist for the same broad reason:
Sometimes you only have
&self, but some internal state still needs to change.
If you haven’t read the Cell article yet, it is worth starting there because RefCell solves a slightly more complicated version of the same problem.
The short version is:
Cell<T>lets you replace a value through&self.RefCell<T>lets you actually borrow and mutate a value through&self.
Start with the normal version
Let’s say we have a logger:
struct Logger {
messages: Vec<String>,
}
impl Logger {
fn log(&mut self, message: String) {
self.messages.push(message);
}
}Nothing unusual here.
Vec::push needs mutable access to the vector, so log needs:
&mut selfAnd this works fine:
let mut logger = Logger {
messages: Vec::new(),
};
logger.log("starting".into());If you naturally have mutable access to the logger, this is probably exactly how you should write it. But what if you only have a shared reference?
fn do_work(logger: &Logger) {
logger.log("starting".into());
}This doesn’t compile. logger is a &Logger, but log requires &mut Logger.
This is the same situation that led us to Cell.
The difference is that our state is now a Vec<String>, not something simple like a u32.
Could we use Cell?
We could technically write something like:
use std::cell::Cell;
struct Logger {
messages: Cell<Vec<String>>,
}But now we have a problem. What we actually want to do is:
messages.push(...)That requires:
&mut Vec<String>Cell deliberately doesn’t give us that. With Cell, the usual model is:
cell.get();
cell.set(...);
cell.replace(...);We replace or move the whole value. That works nicely for things like:
Cell<bool>
Cell<u32>
Cell<usize>But it becomes awkward for something like a vector, map, or larger struct. We don’t want to replace the entire vector just to add one item. We want to borrow it mutably.
That is what RefCell gives us. We can rewrite the logger like this:
use std::cell::RefCell;
struct Logger {
messages: RefCell<Vec<String>>,
}Now:
impl Logger {
fn log(&self, message: String) {
self.messages.borrow_mut().push(message);
}
}Notice that log now takes:
&selfnot:
&mut selfSo this works:
fn do_work(logger: &Logger) {
logger.log("starting".into());
}The interesting part is:
self.messages.borrow_mut()RefCell is allowing us to obtain mutable access to the value inside it even though we only have a shared reference to the outer object.
That is interior mutability.
What does borrow_mut() actually return?
Normally, mutable borrowing looks like this:
let value: &mut Vec<String> = ...;With RefCell, we write:
let mut value = self.messages.borrow_mut();borrow_mut() gives us a smart pointer called RefMut<T>.
For most practical purposes, you can think of it like an &mut T:
let mut messages = logger.messages.borrow_mut();
messages.push("hello".into());
messages.push("world".into());For immutable access, there is:
borrow()which gives you a Ref<T>:
let messages = logger.messages.borrow();
println!("{}", messages.len());So the basic API is:
borrow() // shared borrow
borrow_mut() // mutable borrowBut Rust already has borrowing rules
Exactly! Normally Rust enforces these rules at compile time:
many &Tor:
one &mut Tbut not both at the same time. For example, Rust will reject this:
let a = &mut value;
let b = &mut value;because two simultaneous mutable references are not allowed. RefCell does not remove this rule. It changes when the rule is checked. With normal references, Rust checks borrowing at compile time. With RefCell, borrowing is checked at runtime.
Runtime borrow checking
This is valid:
let a = data.borrow();
let b = data.borrow();Multiple shared borrows are allowed.
This is also valid:
let a = data.borrow_mut();One mutable borrow is allowed.
But this:
let a = data.borrow_mut();
let b = data.borrow_mut();will panic at runtime.
And so will this:
let a = data.borrow();
let b = data.borrow_mut();because an immutable borrow is still active when we try to create a mutable one.
So RefCell is effectively saying:
The compiler cannot prove these borrows are valid, so check them while the program is running.
That is the tradeoff.
You gain flexibility, but invalid borrowing becomes a runtime panic instead of a compile-time error.
Why would you ever want that?
Because sometimes the ownership structure of your program is valid, but too dynamic for the compiler to reason about easily.
A common example is shared mutable state.
Suppose multiple parts of the program need to own the same value.
We can use Rc:
use std::rc::Rc;
let users = Rc::new(Vec::<String>::new());
let a = Rc::clone(&users);
let b = Rc::clone(&users);Now a and b both own the same vector.
But Rc only gives shared access.
So this doesn’t work:
a.push("Alice".into());There may be multiple owners, so Rust cannot hand us an &mut Vec<String>.
This is where Rc<RefCell<T>> appears.
Rc<RefCell<T>>
We can write:
use std::{
cell::RefCell,
rc::Rc,
};
let users = Rc::new(RefCell::new(Vec::new()));
let a = Rc::clone(&users);
let b = Rc::clone(&users);Now both a and b share ownership of the same value.
And both can mutate it:
a.borrow_mut().push("Alice".into());
b.borrow_mut().push("Bob".into());The two types are solving separate problems:
Rc<T>gives us:
multiple owners
while:
RefCell<T>gives us:
mutable access through shared ownership
Together:
Rc<RefCell<T>>means:
multiple owners of one mutable value
This is one of the most common places you will see RefCell in Rust.
Cell vs RefCell
Use Cell when you just want to replace a small value:
Cell<bool>
Cell<u32>
Cell<Option<usize>>For example:
self.dirty.set(true);Use RefCell when you need to actually work with the value mutably:
RefCell<Vec<String>>
RefCell<HashMap<K, V>>
RefCell<MyStruct>For example:
self.messages.borrow_mut().push(message);You can think of it like this:
Cell<T>
replace the value
RefCell<T>
borrow the valueIf the Cell article explains why you might want mutation through &self, then RefCell is the next step:
What if I don’t just want to replace the field? What if I need an actual mutable borrow of it?
That’s what RefCell solves.
The downside
RefCell is useful, but it is not something to add everywhere.
Normal borrowing is better when possible because the compiler verifies it for you.
This:
fn update(&mut self)is generally easier to reason about than:
fn update(&self) {
self.data.borrow_mut();
}With &mut self, invalid borrowing cannot compile.
With RefCell, invalid borrowing can compile and then panic later.
So if ordinary mutable references fit your design, use them.
Reach for RefCell when shared ownership or API constraints mean you only have &self, but you still need real mutable access to an internal value.