Why this matters
Rc lets several owners share a value, but only for reading. Some designs need shared data that changes: a log written by several components, a graph whose nodes are updated, a cache filled lazily behind a &self method. Rust's borrow rules forbid mutation through a shared reference at compile time. Interior mutability types relax that by checking the rule at run time instead. RefCell<T> is the main one; Cell<T> is a simpler cousin for Copy values. Combined with Rc, they give you shared, mutable data, with a different set of trade-offs.
RefCell: borrowing checked at run time
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(vec![1, 2, 3]);
{
let r1 = cell.borrow();
let r2 = cell.borrow();
println!("two readers: {:?} {:?}", *r1, *r2);
}
cell.borrow_mut().push(4);
println!("after push: {:?}", cell.borrow());
let shared: &RefCell<Vec<i32>> = &cell;
shared.borrow_mut().push(5);
println!("mutated through a shared reference: {:?}", cell.borrow());
}
two readers: [1, 2, 3] [1, 2, 3]
after push: [1, 2, 3, 4]
mutated through a shared reference: [1, 2, 3, 4, 5]
RefCell::new(value)wraps a value..borrow()returns aRef<T>(acts like&T);.borrow_mut()returns aRefMut<T>(acts like&mut T). Both are guards: the borrow lasts until the guard is dropped.- Both methods take
&self. That is the point: you can get mutable access through a shared reference to the cell.
The RefCell keeps a counter of active borrows. The rule is the same as the compiler's: many borrows, or one borrow_mut, never both. The difference is when a violation is detected:
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(String::from("hi"));
let reader = cell.borrow();
println!("reading {}", *reader);
let mut writer = cell.borrow_mut();
writer.push('!');
}
reading hi
thread 'main' panicked at main.rs:7:27:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
It compiles, and panics when run. The compiler cannot see the problem (both calls take &self), so the RefCell catches it as it happens. The usual fix is the same as for compile-time borrow errors: shorten the first borrow, for example by not storing the guard in a variable (cell.borrow().len() ends its borrow at the end of the statement). try_borrow_mut() returns a Result instead of panicking, when you want to handle contention.
Cell: for Copy values
Cell<T> never hands out references at all: you get a copy out and set a new value in. So there is nothing to check, no counter and no panic, but it only works well for Copy types like counters and flags:
use std::cell::Cell;
struct Page {
text: String,
views: Cell<u32>,
}
impl Page {
fn show(&self) -> &str {
self.views.set(self.views.get() + 1);
&self.text
}
}
fn main() {
let p = Page { text: String::from("hello"), views: Cell::new(0) };
p.show();
p.show();
println!("{} viewed {} times", p.show(), p.views.get());
}
hello viewed 3 times
show takes &self, yet it updates the counter. That is interior mutability used for an implementation detail (counting, caching) that callers should not have to care about.
Rc<RefCell<T>>: shared and mutable
Rc provides multiple owners; RefCell provides mutation behind shared access. Together:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Account {
balance: i64,
}
fn main() {
let acct = Rc::new(RefCell::new(Account { balance: 100 }));
let alice = Rc::clone(&acct);
let bob = Rc::clone(&acct);
alice.borrow_mut().balance -= 30;
bob.borrow_mut().balance += 5;
println!("{:?}, {} owners", acct.borrow(), Rc::strong_count(&acct));
}
Account { balance: 75 }, 3 owners
alice ---+
bob ---+--> Rc allocation [ strong: 3 | RefCell [ borrow flag | Account { balance } ] ]
acct ---+
This pattern is how you model graphs, observers and other genuinely shared mutable state in single-threaded code. Its multi-threaded equivalent is Arc<Mutex<T>> (module 14).
The trade-off
Interior mutability is not a loophole; the aliasing rule still holds. What you give up:
- Errors move from compile time to run time. A mistake becomes a panic in some code path, possibly one your tests never ran.
- A small run-time cost for updating the borrow counter.
- Less local reasoning: any holder of an
Rc<RefCell<T>>might change the value.
So prefer ordinary ownership and borrowing, and use RefCell where the design truly needs shared mutation. Keep borrows short: borrow, do one thing, let the guard drop.
Common mistakes
- Holding a
borrow()guard in a variable across aborrow_mut():already borrowed: BorrowMutErrorpanic. Use the borrow in a single expression or an inner block. - Borrowing inside a loop over the same RefCell:
for x in cell.borrow().iter() { cell.borrow_mut()... }panics. Collect what you need first. - Using RefCell across threads: E0277 "
RefCell<...>cannot be shared between threads safely". UseMutex. - Wrapping everything in
Rc<RefCell<...>>out of habit: it hides ownership structure and turns compile errors into panics.
Recap
- Interior mutability allows mutation through
&T; the aliasing rule is checked at run time. RefCell<T>:borrow()andborrow_mut()return guards; violations panic (try_borrow_mutreports them instead).Cell<T>:get/setfor Copy values, no references, no panics.Rc<RefCell<T>>gives shared, mutable data in single-threaded code.- Keep borrows short; prefer compile-time ownership when it fits.
Going further: read the Rust Book chapter 15.5. Try RefCell::replace and Cell::take, and look at std::cell::OnceCell for values that are initialized once on first use.