C.W.K.
Stream
Lesson 04 of 05 · published

RefCell & Interior Mutability

~12 min · smart-pointers, refcell, interior-mutability

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

Rc gives shared ownership but only immutable access. RefCell<T> supplies the missing half: the ability to mutate through a shared reference, by moving the borrow check from compile time to runtime. This is interior mutability.

Borrow rules, checked at runtime

Normally the borrow checker enforces aliasing XOR mutation at compile time. RefCell lets you opt into runtime checking instead: .borrow() gives a shared reference, .borrow_mut() a mutable one, and it tracks them at runtime. Break the rule — two mutable borrows at once — and it panics instead of failing to compile. Same guarantee, enforced later.

Why move the check to runtime?

Some valid patterns can't be proven safe at compile time — a value mutated through a shared structure, a mock object accumulating calls in a test, a graph node updated from elsewhere. RefCell says 'trust me, I'll uphold the borrow rules; check me at runtime.' You trade a compile-time guarantee for flexibility, accepting a possible panic if you break the rule.

RefCell moves borrow errors from compile time to runtime. The safety is the same — aliasing XOR mutation still holds — but a violation panics at runtime instead of failing to compile. Use it only when you genuinely can't satisfy the compile-time checker; reach for normal references first.

Rc + RefCell: shared and mutable

The classic combo is Rc<RefCell<T>>: Rc for multiple owners, RefCell for mutating the shared value. It's how you build a tree whose nodes can be updated, or shared mutable state in single-threaded code. (In multithreaded code the equivalent is Arc<Mutex<T>> — note the exact parallel.)

Code

Rc<RefCell<T>>: shared and mutable·rust
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    // Shared AND mutable: Rc for owners, RefCell for mutation
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

    let a = Rc::clone(&shared);
    a.borrow_mut().push(4); // mutate through a shared handle

    println!("{:?}", shared.borrow()); // [1, 2, 3, 4]

    // Breaking the borrow rule panics at RUNTIME, not compile time:
    // let _b1 = shared.borrow_mut();
    // let _b2 = shared.borrow_mut(); // panic: already mutably borrowed
}

External links

Exercise

Build an Rc<RefCell<Vec<String>>> log shared by two handles. Push a message through each handle, then read the combined log through a third clone. Then deliberately hold two borrow_mut()s alive at once and observe the runtime panic. Why does RefCell panic rather than refuse to compile?
Hint
RefCell's whole purpose is to defer the borrow check to runtime, enabling patterns the compile-time checker can't prove safe. The cost of that flexibility is that a violated rule surfaces as a runtime panic instead of a compile error.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.