Interior Mutability (Rc, RefCell, Cell, Weak)
This file is Tier 2 ā the Rust Book Chapter 15 literacy set. After
01_smart_pointers.md you know Box (unique heap ownership),
Deref, and Arc<Mutex> (shared mutable state across threads). This file covers the
single-threaded counterparts:
Rc<T>ā shared ownership on one thread (andRc::strong_count).RefCell<T>ā move the borrow check from compile time to runtime.Rc<RefCell<T>>ā the shared-and-mutable combo.Cell<T>ā interior mutability forCopyvalues, no borrowing.Weak<T>ā a non-owning reference that breaks cycles.
Read this first: Rc and RefCell are single-threaded only (they are not Send/
Sync, so the compiler refuses to move them across threads). In day-to-day async web
code ā Axum handlers, background tasks, anything tokio ā you'll almost always use
Arc<Mutex<T>> / Arc<RwLock<T>> from Tier 1 instead, because handlers run on a thread
pool. Rc/RefCell shine in single-threaded tree/graph structures, interpreters, and
tight inner data structures. Learn them for literacy; reach for Arc in servers.
Rc<T> ā shared ownership, single thread
Box<T> has exactly one owner. Sometimes a single-threaded data structure needs several
owners of the same value ā e.g. two lists that share a tail, or many tree nodes pointing at
one shared resource. Rc<T> (Reference Counted) allows that: each Rc::clone hands
out another owning handle to the same allocation and increments a (non-atomic) count. The
value is dropped only when the last Rc goes away.
use std::rc::Rc;
let a = Rc::new(String::from("shared"));
println!("count = {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a); // not a deep copy ā same allocation, count -> 2
let c = Rc::clone(&a); // count -> 3
println!("count = {}", Rc::strong_count(&a)); // 3
drop(c); // count -> 2
println!("count = {}", Rc::strong_count(&a)); // 2
// a and b still point at the same "shared" String.Rc::clone(&a) is the idiomatic spelling (cheap refcount bump), not a.clone() ā it makes
"this is a refcount increment, not a deep copy" obvious to readers. Rc::strong_count
returns the current number of owners, which is handy for understanding (and testing)
ownership ā Task 2 prints it as state evolves.
In C#: Rc<T> ā an ordinary reference to a heap object. When you write
var b = a; for a class, both variables reference the same instance and the GC frees it
when no references remain ā that's exactly Rc minus the manual clone and with a tracing
GC instead of a refcount. Rc::strong_count has no everyday C# equivalent because the
runtime hides the count from you.
RefCell<T> ā the borrow check, moved to runtime
Rc<T> gives shared ownership but only immutable access ā you can't get &mut out of
an Rc, just like you can't have a shared reference and a mutable one at compile time.
RefCell<T> is the way out: it provides interior mutability ā the ability to mutate
data through a shared (&) reference ā by enforcing Rust's borrow rules at runtime
instead of compile time.
You call:
.borrow()ā aRef<T>(acts like&T); you may have many at once..borrow_mut()ā aRefMut<T>(acts like&mut T); only one, and not alongside any shared borrow.
The rules are identical to compile-time borrowing (many readers XOR one writer); the difference is when they're checked. Break them and you get a panic at runtime rather than a compile error:
use std::cell::RefCell;
let cell = RefCell::new(5);
*cell.borrow_mut() += 10; // mutate through a shared &cell
println!("{}", cell.borrow()); // 15
// This compiles but PANICS at runtime: "RefCell already borrowed"
// (a BorrowMutError ā the same rule the compiler enforces statically for &mut,
// just checked at runtime here)
let r = cell.borrow_mut();
let _r2 = cell.borrow_mut(); // second active borrow -> panicKeep borrows short (don't hold a Ref/RefMut longer than needed) to avoid accidental
overlaps. The borrow is released when the guard drops, so a *cell.borrow_mut() += 1;
one-liner is safe because the temporary guard drops at the ;.
In C#: there is no analogue, because the borrow check itself has no analogue ā C# lets
you mutate any field through any reference at any time. RefCell exists precisely to recover
that "just mutate it" freedom in the rare spots where the compile-time checker is too
strict, at the price of a runtime check. Think of a BorrowMutError panic as Rust's version
of the bug C# would simply let happen (e.g. mutating a collection while enumerating it,
which C# catches at runtime as InvalidOperationException ā same idea: a runtime guard
against aliased mutation).
Rc<RefCell<T>> ā shared and mutable (single thread)
Combine them and you get the single-threaded equivalent of a plain C# object: multiple
owners (Rc) that can all mutate the shared value (RefCell). This is the idiom for
mutable graphs, trees with shared children, and observer-style structures on one thread.
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(0));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
*a.borrow_mut() += 10; // mutate via handle a
*b.borrow_mut() += 5; // mutate via handle b
println!("{}", shared.borrow()); // 15 ā all three see the same valueThe mental model: Rc = "who owns it" (sharing), RefCell = "who may mutate it right now"
(interior mutability). It's the exact single-threaded mirror of Arc<Mutex<T>> from Tier 1
ā swap atomic refcounting for plain, and a runtime-checked lock for a borrow flag.
In C#: Rc<RefCell<T>> is essentially what every shared mutable class instance
already is ā several variables referencing one object that any of them can mutate. Rust
makes you spell out both halves (shared ownership + interior mutability) that C# bundles into
the default reference. Task 2 builds exactly this.
Cell<T> ā interior mutability without borrowing (Copy types)
Cell<T> is a lighter-weight interior-mutability tool for small Copy values (numbers,
bools, small enums). Instead of handing out borrows, it moves whole values in and out:
.get() returns a copy, .set(v) replaces the contents, .replace(v) swaps and returns
the old value. Because it never lends out a reference, there's nothing to conflict ā so no
runtime borrow check and no possible BorrowError.
use std::cell::Cell;
let counter = Cell::new(0);
counter.set(counter.get() + 1);
counter.set(counter.get() + 1);
println!("{}", counter.get()); // 2 ā mutated through a shared &counterRule of thumb: Cell<T> for Copy values you read/replace wholesale; RefCell<T> when
you need to borrow the inner value (e.g. to call &mut methods or mutate a field of a
struct in place).
In C#: like RefCell, there's no direct analogue ā it's just "a mutable field," which
C# gives you unconditionally. Cell is what you use when even a non-mut binding needs an
internally mutable scalar (think a cached/memoized field on an otherwise-shared value).
Weak<T> ā non-owning references that break cycles
Rc has one failure mode: reference cycles leak. If node A holds an Rc to B and B
holds an Rc back to A, neither count ever reaches zero, so neither is freed ā a memory
leak the refcount can't escape. The fix is Weak<T>: a non-owning handle that does
not keep the value alive.
Rc::downgrade(&rc)produces aWeak<T>(bumps a separate weak count, not the strong count).weak.upgrade()returnsOption<Rc<T>>:Someif the value is still alive,Noneif it has already been dropped ā so you can never dereference a dangling pointer.
The standard pattern: parent owns children with Rc; each child points back to its parent
with Weak. The strong edges form a tree (no cycle), so everything frees correctly; the
weak back-edges let you navigate upward without keeping the parent alive.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // back-edge: weak, doesn't own
children: RefCell<Vec<Rc<Node>>>, // forward-edges: strong, owns
}
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
*child.parent.borrow_mut() = Rc::downgrade(&parent); // weak back-edge
parent.children.borrow_mut().push(Rc::clone(&child)); // strong forward-edge
// Navigate up via upgrade(); None if the parent were already gone.
let up = child.parent.borrow().upgrade();
println!("{}", up.map(|p| p.value).unwrap_or(-1)); // 1In C#: Weak<T> ā System.WeakReference / WeakReference<T> ā a reference that lets
the GC reclaim the target, with TryGetTarget/upgrade() returning whether it's still
alive. The motivation differs slightly: in C# the tracing GC actually collects cycles, so
you reach for WeakReference for caches and event-handler leaks; in Rust Rc cycles never
collect at all, so Weak is the required tool for any back-pointer, not just an
optimization.
When do I actually use these?
| Tool | Use it for | Avoid it for |
|---|---|---|
Rc<T> | single-threaded shared ownership (trees/graphs/interpreters) | anything multithreaded ā use Arc |
RefCell<T> | interior mutability of borrowed inner data, single thread | shared-across-threads ā use Mutex |
Rc<RefCell<T>> | shared, mutable, single-threaded structure | async/web handlers ā use Arc<Mutex<T>> |
Cell<T> | a mutable Copy scalar behind a shared handle | borrowing the inner value (use RefCell) |
Weak<T> | back-pointers to break Rc cycles | ownership (Weak doesn't keep data alive) |
Note again: in async web code (Axum, tokio) you'll reach for Arc<Mutex<T>> /
Arc<RwLock<T>> from Tier 1 far more often than anything on this page, because handlers run
across a thread pool and Rc/RefCell aren't Send. Rc<RefCell<T>> is mainly for
single-threaded data structures.
Key takeaway
Interior mutability is how Rust lets you mutate through a shared (&) handle when the
compile-time borrow checker is too strict ā by moving the check to runtime. Use Rc<T>
for single-threaded shared ownership (and Rc::strong_count to watch owners come and go),
RefCell<T> to borrow-and-mutate with the rules enforced at runtime (overlapping
borrows panic ā BorrowMutError), and Rc<RefCell<T>> together as the single-thread
mirror of Arc<Mutex<T>>. Use Cell<T> for cheap, borrow-free mutation of Copy
scalars, and Weak<T> (via Rc::downgrade/upgrade) to add back-edges without leaking
Rc cycles ā Rust's WeakReference. All of these are single-threaded; in async/web code
prefer the Arc<Mutex>/Arc<RwLock> tools from Tier 1.