Smart Pointers · about 40 min

RefCell and Interior Mutability

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]

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:

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

Recap

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.

Exercises

Exercise 1 · write code

A shared, mutable log

Several Components write to one shared log. Define struct Component { name: String, log: Rc<RefCell<Vec<String>>> } with fn act(&self, what: &str) that pushes "NAME: WHAT" into the shared log (note: &self, not &mut self).

The given main reads lines component action from stdin, routes each to the component with that name (ui or db), and prints the log at the end with its length:

ui: click
db: query
ui: scroll
3 entries

File: main.rs

Starting point

use std::cell::RefCell;
use std::io;
use std::rc::Rc;

fn main() {
    let log = Rc::new(RefCell::new(Vec::new()));
    let ui = Component { name: String::from("ui"), log: Rc::clone(&log) };
    let db = Component { name: String::from("db"), log: Rc::clone(&log) };
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let (who, what) = line.trim().split_once(' ').unwrap();
        match who {
            "ui" => ui.act(what),
            _ => db.act(what),
        }
    }
    for entry in log.borrow().iter() {
        println!("{entry}");
    }
    println!("{} entries", log.borrow().len());
}

Example

input

ui click
db query
ui scroll

expected output

ui: click
db: query
ui: scroll
3 entries
Hint 1

self.log.borrow_mut().push(...) gets a temporary mutable borrow of the Vec inside the RefCell.

Reference solution
use std::cell::RefCell;
use std::io;
use std::rc::Rc;

struct Component {
    name: String,
    log: Rc<RefCell<Vec<String>>>,
}

impl Component {
    fn act(&self, what: &str) {
        self.log.borrow_mut().push(format!("{}: {}", self.name, what));
    }
}

fn main() {
    let log = Rc::new(RefCell::new(Vec::new()));
    let ui = Component { name: String::from("ui"), log: Rc::clone(&log) };
    let db = Component { name: String::from("db"), log: Rc::clone(&log) };
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let (who, what) = line.trim().split_once(' ').unwrap();
        match who {
            "ui" => ui.act(what),
            _ => db.act(what),
        }
    }
    for entry in log.borrow().iter() {
        println!("{entry}");
    }
    println!("{} entries", log.borrow().len());
}

Exercise 2 · write code

Fix the run-time borrow panic

This program compiles, but panics at run time with already borrowed: BorrowMutError: it holds a borrow() of the RefCell while calling borrow_mut(). Fix it so that the shared reference is released before the mutation (for example by copying the value you need out first). It must print:

before: [1, 2, 3]
after: [1, 2, 3, 6]

File: main.rs

Starting point

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    println!("before: {:?}", data.borrow());
    let items = data.borrow();
    let total: i32 = items.iter().sum();
    data.borrow_mut().push(total);
    println!("after: {:?}", data.borrow());
}

Example

expected output

before: [1, 2, 3]
after: [1, 2, 3, 6]
Hint 1

items keeps the shared borrow alive until the end of main, so borrow_mut fails.

Hint 2

Compute the sum in one expression, let total: i32 = data.borrow().iter().sum();, so the temporary borrow ends at the semicolon; or put items in an inner block, or drop(items) before borrow_mut.

Reference solution
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    println!("before: {:?}", data.borrow());
    let total: i32 = data.borrow().iter().sum();
    data.borrow_mut().push(total);
    println!("after: {:?}", data.borrow());
}

Exercise 3 · quiz

Compile time or run time?

How does RefCell<T> differ from ordinary borrowing? Select all that apply.

Answer
  • It enforces the same rule (many readers or one writer), but at run time instead of compile time

  • Breaking the rule with RefCell causes a panic

  • RefCell lets you mutate the inner value through a shared reference (&RefCell<T>)

RefCell moves the borrow check to run time: it counts active borrows, and borrow_mut panics if any borrow is alive (try_borrow_mut returns an error instead). That is what allows mutation through &self: interior mutability. It is single-threaded; the thread-safe counterparts are Mutex and RwLock.