Lesson 58 of 94
Cell & RefCell: Interior Mutability
The borrow rules say: many &T or one &mut T. Occasionally you need to change something that is only reachable through a shared reference, for example a counter inside an object that many parts of the program read, or shared state behind an Rc. Interior mutability types allow that, while keeping Rust's guarantees:
Cell<T>: replace or copy the value in and out. No references to the inside are ever handed out, so it's always safe. Best for smallCopyvalues.RefCell<T>: hands outRefandRefMutguards, and enforces the borrow rules at runtime instead of compile time. Breaking them panics.
Both are single-threaded. Their thread-safe counterparts are atomics and Mutex/RwLock (Shared State).
| Concept | Rust |
|---|---|
Changing a field through &self |
Only fields in a Cell/RefCell change through &self |
| Breaking the borrow rules at runtime | RefCell panics "already borrowed" at runtime |
| Mutable value shared by several owners | Rc<RefCell<T>> |
| Lazy field initialization | OnceCell<T> / LazyCell<T> |
1. The problem: changing state through &self
A method that only needs &self can't update a field, even something as harmless as a counter.
struct Api {
calls: u32,
}
impl Api {
fn get(&self, path: &str) -> String {
self.calls += 1;
format!("GET {path}")
}
}
fn main() {
let api = Api { calls: 0 };
println!("{}", api.get("/health"));
}error[E0594]: cannot assign to `self.calls`, which is behind a `&` reference
--> src/main.rs:7:9
|
7 | self.calls += 1;
| ^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to
|
help: consider changing this to be a mutable reference
|
6 | fn get(&mut self, path: &str) -> String {
| +++Changing the method to &mut self would force every caller to have exclusive access, which may not be possible if the Api is shared.
2. Cell<T>: a mutable slot for Copy values
Cell::get copies the value out and Cell::set replaces it. Because no reference to the inside ever exists, this can't break any borrow rule.
use std::cell::Cell;
struct Api {
calls: Cell<u32>,
}
impl Api {
fn get(&self, path: &str) -> String {
self.calls.set(self.calls.get() + 1);
format!("GET {path}")
}
}
fn main() {
let api = Api { calls: Cell::new(0) };
let shared_a = &api;
let shared_b = &api;
println!("{}", shared_a.get("/health"));
println!("{}", shared_b.get("/users"));
println!("calls: {}", api.calls.get());
}GET /health
GET /users
calls: 23. More Cell operations
use std::cell::Cell;
fn main() {
let flag = Cell::new(false);
let old = flag.replace(true);
println!("was {old}, now {}", flag.get());
let counter = Cell::new(10);
counter.update(|n| n * 2);
println!("{}", counter.get());
let name = Cell::new(String::from("draft"));
let taken = name.take(); // leaves String::default() behind
println!("took {taken:?}, left {:?}", name.into_inner());
}was false, now true
20
took "draft", left ""Cell<T> works with non-Copy types too, through replace, take, and into_inner; only get needs Copy.
4. RefCell<T>: borrowing checked at runtime
borrow() returns a guard that acts like &T; borrow_mut() returns one that acts like &mut T. The guards end the borrow when they're dropped.
use std::cell::RefCell;
fn main() {
let log = RefCell::new(Vec::new());
log.borrow_mut().push("started");
log.borrow_mut().push("running");
println!("{} entries: {:?}", log.borrow().len(), log.borrow());
}2 entries: ["started", "running"]5. Breaking the rules panics
Holding a borrow() while calling borrow_mut() is exactly the aliasing the compiler normally prevents. With RefCell, it's caught at runtime instead, with a panic.
use std::cell::RefCell;
fn main() {
let queue = RefCell::new(vec![1, 2, 3]);
let first = queue.borrow(); // shared borrow held...
queue.borrow_mut().push(4); // ...while asking for an exclusive one
println!("{:?}", first);
}thread 'main' panicked at src/main.rs:6:11:
RefCell already borrowedThis is RefCell's trade-off: flexibility in exchange for a runtime check that can fail. Keep borrows short to make conflicts impossible.
6. Keeping borrows short
Most RefCell panics come from a guard living longer than intended. Scope guards tightly, or copy out what you need.
use std::cell::RefCell;
fn main() {
let queue = RefCell::new(vec![1, 2, 3]);
let first = *queue.borrow().first().unwrap(); // guard dropped at end of statement
queue.borrow_mut().push(first * 10);
{
let q = queue.borrow();
println!("{:?} (len {})", *q, q.len());
} // guard dropped here
queue.borrow_mut().clear();
println!("{:?}", queue.borrow());
}[1, 2, 3, 10] (len 4)
[]7. try_borrow_mut: checking instead of panicking
When a conflict is possible and you want to handle it, try_borrow and try_borrow_mut return a Result.
use std::cell::RefCell;
fn main() {
let cache = RefCell::new(vec![String::from("warm")]);
let reader = cache.borrow();
match cache.try_borrow_mut() {
Ok(mut c) => c.push(String::from("more")),
Err(_) => println!("cache is being read; skipping update"),
}
drop(reader);
if let Ok(mut c) = cache.try_borrow_mut() {
c.push(String::from("more"));
}
println!("{:?}", cache.borrow());
}cache is being read; skipping update
["warm", "more"]8. Rc<RefCell<T>>: shared *and* mutable
Rc gives several owners; RefCell lets them change the value. Together they model an object reachable from several places, mutable through any of them.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Account {
balance: i64,
}
fn main() {
let account = Rc::new(RefCell::new(Account { balance: 100 }));
let mobile_app = Rc::clone(&account);
let web_app = Rc::clone(&account);
mobile_app.borrow_mut().balance -= 30;
web_app.borrow_mut().balance += 50;
println!("{:?}", account.borrow());
}Account { balance: 120 }Use this combination sparingly. It trades compile-time checking for runtime checks, and it tends to spread through a codebase. Often a clear owner that passes &mut down is simpler.
9. A shared counter updated by closures
Closures that each hold an Rc<Cell<_>> can update one shared counter, which is handy for callbacks.
use std::cell::Cell;
use std::rc::Rc;
fn main() {
let clicks = Rc::new(Cell::new(0));
let handlers: Vec<Box<dyn Fn()>> = (0..3)
.map(|_| {
let c = Rc::clone(&clicks);
Box::new(move || c.set(c.get() + 1)) as Box<dyn Fn()>
})
.collect();
for h in &handlers {
h();
h();
}
println!("total clicks: {}", clicks.get());
}total clicks: 6The closures are Fn (not FnMut) because Cell::set only needs &self.
10. Caching inside a &self method
A computed value can be cached lazily in a RefCell<Option<T>>, keeping the method signature &self.
use std::cell::RefCell;
struct Report {
rows: Vec<u64>,
cached_total: RefCell<Option<u64>>,
}
impl Report {
fn total(&self) -> u64 {
if let Some(t) = *self.cached_total.borrow() {
return t;
}
println!(" computing total...");
let t = self.rows.iter().sum();
*self.cached_total.borrow_mut() = Some(t);
t
}
}
fn main() {
let r = Report { rows: vec![120, 340, 90], cached_total: RefCell::new(None) };
println!("{}", r.total());
println!("{}", r.total());
} computing total...
550
55011. OnceCell: set exactly once
std::cell::OnceCell holds a value that is set once and then only read. get_or_init initializes it on first use. It's simpler than RefCell<Option<T>> for lazy fields.
use std::cell::OnceCell;
struct Page {
markdown: String,
html: OnceCell<String>,
}
impl Page {
fn html(&self) -> &str {
self.html.get_or_init(|| {
println!(" rendering...");
format!("<p>{}</p>", self.markdown)
})
}
}
fn main() {
let page = Page { markdown: String::from("Hello, Rust"), html: OnceCell::new() };
println!("{}", page.html());
println!("{}", page.html());
} rendering...
<p>Hello, Rust</p>
<p>Hello, Rust</p>OnceCell::get_or_init returns a plain &T tied to the cell, so there are no guards to manage.
12. LazyCell: initialization defined up front
LazyCell bundles the initializer with the cell, and runs it on first access.
use std::cell::LazyCell;
fn main() {
let expensive_table = LazyCell::new(|| {
println!(" building lookup table");
(0..10).map(|i| i * i).collect::<Vec<u32>>()
});
println!("before first use");
println!("{}", expensive_table[3]);
println!("{}", expensive_table[9]);
}before first use
building lookup table
9
81For process-wide lazy globals shared across threads, the equivalents are std::sync::OnceLock and LazyLock (Atomics, Send & Sync).
13. A test double that records calls
A trait method takes &self, but a mock implementation wants to record every call. RefCell lets the mock remember calls without changing the trait. This is one of the most common real uses of RefCell.
use std::cell::RefCell;
trait Mailer {
fn send(&self, to: &str, subject: &str);
}
struct FakeMailer {
sent: RefCell<Vec<(String, String)>>,
}
impl Mailer for FakeMailer {
fn send(&self, to: &str, subject: &str) {
self.sent.borrow_mut().push((to.to_string(), subject.to_string()));
}
}
fn notify_overdue(mailer: &dyn Mailer, invoices: &[(&str, u32)]) {
for (customer, days) in invoices {
if *days > 30 {
mailer.send(customer, &format!("Invoice {days} days overdue"));
}
}
}
fn main() {
let fake = FakeMailer { sent: RefCell::new(Vec::new()) };
notify_overdue(&fake, &[("a@x.com", 10), ("b@x.com", 45), ("c@x.com", 31)]);
let sent = fake.sent.borrow();
println!("{} mails: {:?}", sent.len(), *sent);
assert_eq!(sent[0].0, "b@x.com");
println!("assertions passed");
}2 mails: [("b@x.com", "Invoice 45 days overdue"), ("c@x.com", "Invoice 31 days overdue")]
assertions passed14. A shared, mutable graph
Nodes that point to each other and must be updated after creation are typically Rc<RefCell<Node>>.
use std::cell::RefCell;
use std::rc::Rc;
struct User {
name: &'static str,
following: Vec<Rc<RefCell<User>>>,
followers: u32,
}
fn follow(from: &Rc<RefCell<User>>, to: &Rc<RefCell<User>>) {
from.borrow_mut().following.push(Rc::clone(to));
to.borrow_mut().followers += 1;
}
fn main() {
let ana = Rc::new(RefCell::new(User { name: "ana", following: vec![], followers: 0 }));
let ben = Rc::new(RefCell::new(User { name: "ben", following: vec![], followers: 0 }));
let cy = Rc::new(RefCell::new(User { name: "cy", following: vec![], followers: 0 }));
follow(&ana, &ben);
follow(&cy, &ben);
follow(&ben, &cy);
for u in [&ana, &ben, &cy] {
let u = u.borrow();
let names: Vec<&str> = u.following.iter().map(|f| f.borrow().name).collect();
println!("{} follows {:?}, has {} follower(s)", u.name, names, u.followers);
}
}ana follows ["ben"], has 0 follower(s)
ben follows ["cy"], has 2 follower(s)
cy follows ["ben"], has 1 follower(s)Mutual follows like ben ↔ cy form Rc cycles and leak when dropped. In a long-running program you'd use Weak for one direction, or store users in a Vec and refer to them by index, which is often the simplest design of all.
15. The index-based alternative
Storing nodes in a Vec and using indices instead of pointers avoids both Rc and RefCell. Mutation is ordinary &mut access. Many Rust graph libraries work this way.
struct User {
name: &'static str,
following: Vec<usize>,
followers: u32,
}
struct Network {
users: Vec<User>,
}
impl Network {
fn add(&mut self, name: &'static str) -> usize {
self.users.push(User { name, following: vec![], followers: 0 });
self.users.len() - 1
}
fn follow(&mut self, from: usize, to: usize) {
self.users[from].following.push(to);
self.users[to].followers += 1;
}
}
fn main() {
let mut net = Network { users: Vec::new() };
let (ana, ben, cy) = (net.add("ana"), net.add("ben"), net.add("cy"));
net.follow(ana, ben);
net.follow(cy, ben);
net.follow(ben, cy);
for u in &net.users {
let names: Vec<&str> = u.following.iter().map(|&i| net.users[i].name).collect();
println!("{} follows {:?}, {} follower(s)", u.name, names, u.followers);
}
}ana follows ["ben"], 0 follower(s)
ben follows ["cy"], 2 follower(s)
cy follows ["ben"], 1 follower(s)16. RefCell borrows are released at the end of the statement
A temporary guard (one not stored in a variable) is dropped at the end of its statement, so these calls never conflict.
use std::cell::RefCell;
fn main() {
let items = RefCell::new(vec![3, 1, 2]);
items.borrow_mut().sort();
let len = items.borrow().len();
items.borrow_mut().push(len as i32);
println!("{:?}", items.borrow());
}[1, 2, 3, 3]17. A classic RefCell trap: calling back into yourself
A method holding borrow_mut() that calls code which borrows the same cell again panics. Collect what you need, release the borrow, then call out.
use std::cell::RefCell;
struct Store {
items: RefCell<Vec<String>>,
}
impl Store {
fn count(&self) -> usize {
self.items.borrow().len()
}
fn add_and_report(&self, item: &str) {
let mut items = self.items.borrow_mut();
items.push(item.to_string());
println!("now {} items", self.count()); // borrow() while borrow_mut() is alive
}
}
fn main() {
let s = Store { items: RefCell::new(vec![]) };
s.add_and_report("a");
}thread 'main' panicked at src/main.rs:9:20:
RefCell already mutably borrowedThe fix: end the mutable borrow before calling self.count(), for example by putting the push in its own statement: self.items.borrow_mut().push(...).
18. Cell for flags in a non-mutable struct
use std::cell::Cell;
struct Connection {
host: &'static str,
used: Cell<bool>,
bytes_sent: Cell<u64>,
}
impl Connection {
fn send(&self, payload: &[u8]) {
self.used.set(true);
self.bytes_sent.set(self.bytes_sent.get() + payload.len() as u64);
}
}
fn main() {
let pool = [
Connection { host: "db-1", used: Cell::new(false), bytes_sent: Cell::new(0) },
Connection { host: "db-2", used: Cell::new(false), bytes_sent: Cell::new(0) },
];
pool[0].send(b"SELECT 1");
pool[0].send(b"SELECT 2");
for c in &pool {
println!("{}: used={} bytes={}", c.host, c.used.get(), c.bytes_sent.get());
}
}db-1: used=true bytes=16
db-2: used=false bytes=0pool isn't even declared mut, yet its connections track usage.
19. RefCell::replace and take
use std::cell::RefCell;
fn main() {
let batch = RefCell::new(vec!["e1", "e2", "e3"]);
let flushed = batch.take(); // leaves an empty Vec behind
println!("flushed {:?}, batch now {:?}", flushed, batch.borrow());
let old = batch.replace(vec!["e4"]);
println!("old {:?}, new {:?}", old, batch.borrow());
}flushed ["e1", "e2", "e3"], batch now []
old [], new ["e4"]20. Realistic example: a shopping cart shared by two views
Two UI components hold the same cart. One adds items, the other reads totals; both see every change.
use std::cell::RefCell;
use std::rc::Rc;
struct Cart {
items: Vec<(&'static str, u32)>,
}
struct ProductPage {
cart: Rc<RefCell<Cart>>,
}
struct HeaderBadge {
cart: Rc<RefCell<Cart>>,
}
impl ProductPage {
fn add_to_cart(&self, name: &'static str, cents: u32) {
self.cart.borrow_mut().items.push((name, cents));
}
}
impl HeaderBadge {
fn render(&self) -> String {
let cart = self.cart.borrow();
let total: u32 = cart.items.iter().map(|(_, c)| c).sum();
format!("[cart: {} items, {:.2}]", cart.items.len(), total as f64 / 100.0)
}
}
fn main() {
let cart = Rc::new(RefCell::new(Cart { items: Vec::new() }));
let page = ProductPage { cart: Rc::clone(&cart) };
let badge = HeaderBadge { cart: Rc::clone(&cart) };
println!("{}", badge.render());
page.add_to_cart("keyboard", 12_900);
page.add_to_cart("mouse", 3_900);
println!("{}", badge.render());
}[cart: 0 items, 0.00]
[cart: 2 items, 168.00]21. Realistic example: an in-memory event store with subscribers
use std::cell::RefCell;
use std::rc::Rc;
type Handler = Box<dyn Fn(&str)>;
struct EventStore {
events: RefCell<Vec<String>>,
subscribers: RefCell<Vec<Handler>>,
}
impl EventStore {
fn subscribe(&self, h: Handler) {
self.subscribers.borrow_mut().push(h);
}
fn append(&self, event: &str) {
self.events.borrow_mut().push(event.to_string()); // borrow ends here
for s in self.subscribers.borrow().iter() {
s(event);
}
}
}
fn main() {
let store = EventStore { events: RefCell::new(vec![]), subscribers: RefCell::new(vec![]) };
let audit = Rc::new(RefCell::new(Vec::new()));
let audit_handle = Rc::clone(&audit);
store.subscribe(Box::new(move |e| audit_handle.borrow_mut().push(format!("audit: {e}"))));
store.subscribe(Box::new(|e| {
if e.starts_with("payment") {
println!("notify finance: {e}");
}
}));
store.append("user_signed_up");
store.append("payment_received");
println!("{} events, audit {:?}", store.events.borrow().len(), audit.borrow());
}notify finance: payment_received
2 events, audit ["audit: user_signed_up", "audit: payment_received"]22. Realistic example: memoized recursive function
A RefCell<HashMap> cache lets a recursive function that takes &self memoize its results.
use std::cell::RefCell;
use std::collections::HashMap;
struct PathCounter {
blocked: Vec<(usize, usize)>,
memo: RefCell<HashMap<(usize, usize), u64>>,
}
impl PathCounter {
// number of right/down paths from (r, c) to (0, 0)
fn paths(&self, r: usize, c: usize) -> u64 {
if self.blocked.contains(&(r, c)) {
return 0;
}
if r == 0 || c == 0 {
return 1;
}
if let Some(&v) = self.memo.borrow().get(&(r, c)) {
return v;
}
let v = self.paths(r - 1, c) + self.paths(r, c - 1);
self.memo.borrow_mut().insert((r, c), v);
v
}
}
fn main() {
let grid = PathCounter { blocked: vec![(1, 1)], memo: RefCell::new(HashMap::new()) };
println!("{}", grid.paths(3, 3));
let big = PathCounter { blocked: vec![], memo: RefCell::new(HashMap::new()) };
println!("{}", big.paths(16, 16));
println!("memo entries: {}", big.memo.borrow().len());
}8
601080390
memo entries: 256Notice the borrow() in the if let is released before the recursive calls, and borrow_mut() happens only after them, so borrows never overlap.
23. Realistic example: rate-limit bookkeeping behind &self
A client object shared across a program tracks how many requests it has made in the current window, without requiring &mut everywhere.
use std::cell::Cell;
struct RateLimitedClient {
limit: u32,
window_start: Cell<u64>,
used: Cell<u32>,
}
impl RateLimitedClient {
fn call(&self, now_secs: u64, endpoint: &str) -> Result<String, String> {
if now_secs - self.window_start.get() >= 60 {
self.window_start.set(now_secs);
self.used.set(0);
}
if self.used.get() >= self.limit {
return Err(format!("rate limited, retry after {}s", 60 - (now_secs - self.window_start.get())));
}
self.used.set(self.used.get() + 1);
Ok(format!("200 {endpoint}"))
}
}
fn main() {
let client = RateLimitedClient { limit: 2, window_start: Cell::new(0), used: Cell::new(0) };
for (t, ep) in [(1, "/a"), (5, "/b"), (10, "/c"), (61, "/d")] {
println!("t={t:>2} {:?}", client.call(t, ep));
}
}t= 1 Ok("200 /a")
t= 5 Ok("200 /b")
t=10 Err("rate limited, retry after 50s")
t=61 Ok("200 /d")24. When *not* to use interior mutability
Reach for Cell/RefCell only when a shared reference is truly unavoidable (trait methods with &self, callbacks, shared graphs, caches). If you can restructure so that one owner passes &mut down, do that: the compiler then checks everything, and there are no runtime panics.
struct Inventory {
stock: Vec<(&'static str, u32)>,
}
impl Inventory {
fn reserve(&mut self, sku: &str, qty: u32) -> bool {
match self.stock.iter_mut().find(|(s, _)| *s == sku) {
Some((_, have)) if *have >= qty => {
*have -= qty;
true
}
_ => false,
}
}
}
fn checkout(inv: &mut Inventory, cart: &[(&str, u32)]) -> Vec<bool> {
cart.iter().map(|(sku, q)| inv.reserve(sku, *q)).collect()
}
fn main() {
let mut inv = Inventory { stock: vec![("pen", 5), ("pad", 1)] };
println!("{:?}", checkout(&mut inv, &[("pen", 2), ("pad", 2), ("pen", 3)]));
println!("{:?}", inv.stock);
}[true, false, true]
[("pen", 0), ("pad", 1)]Summary
- Interior mutability lets you change data through
&T. It moves some borrow checking from compile time to runtime. Cell<T>:get/set/replace/take/update; never hands out references, never panics. Best for smallCopyvalues like counters and flags.RefCell<T>:borrow()/borrow_mut()guards, checked at runtime; conflicting borrows panic. Keep guards short; usetry_borrow_mutwhen a conflict is possible.Rc<RefCell<T>>is the single-threaded "shared, mutable object". Prefer indices or a single owner when you can.OnceCellandLazyCellhandle lazy, set-once values.- Multi-threaded code uses atomics,
Mutex,RwLock,OnceLock, andLazyLockinstead.
Next: Drop, Deref & Cow.
