Post

Rust Notes: Box, Rc, RefCell and Interior Mutability

Quick reference for Box, Rc, RefCell, and interior mutability.

Rust Notes: Box, Rc, RefCell and Interior Mutability

Rust Notes: Box, Rc, RefCell and Interior Mutability

Mental model

  • Box<T> → Heap allocation + single ownership
  • Rc<T> → Shared ownership
  • RefCell<T> → Runtime borrow checking (interior mutability)

Box

Purpose

  • Allocate data on the heap.
  • Own the value.
  • Zero-cost abstraction (except heap allocation).
1
let x = Box::new(5);

Common use cases

  • Recursive data structures.
1
2
3
4
enum List {
    Cons(i32, Box<List>),
    Nil,
}
  • Large objects without copying ownership.
  • Trait objects (Box<dyn Trait>).

Rc

Purpose

Allow multiple owners of the same value.

1
2
3
4
use std::rc::Rc;

let a = Rc::new(String::from("Rust"));
let b = Rc::clone(&a);
1
2
3
4
5
6
7
8
a ----\
       \
      Rc(count=2)
         |
      "Rust"
         |
       /
b -----/

Important

Rc::clone() does not clone the data.

It only increments the reference count.

1
let b = Rc::clone(&a);

instead of

1
let b = (*a).clone();

Rc only provides immutable access

1
2
3
4
5
6
7
let value = Rc::new(5);

// OK
println!("{}", value);

// Not allowed
// *value = 10;

Reason:

Multiple owners may exist.


Rc is not thread-safe

For multi-threaded shared ownership, use:

1
Arc<T>

RefCell

Purpose

Move borrow checking from compile time to runtime.

1
2
3
4
5
use std::cell::RefCell;

let value = RefCell::new(5);

*value.borrow_mut() += 1;

Notice:

1
let value = ...

is not mutable.


Interior Mutability

Mutating the inside of an immutable object.

1
2
3
let counter = RefCell::new(0);

*counter.borrow_mut() += 1;

The outer object is immutable.

The inner value changes.


Borrow Rules

Exactly the same rules as normal references:

  • Many immutable borrows

OR

  • One mutable borrow

The difference is when they’re checked.

Normal referencesRefCell
Compile timeRuntime
Compile errorPanic

Why does RefCell exist?

Because sometimes the compiler cannot prove the code is safe.

The rules do not change.

Only the enforcement changes.

1
2
3
4
5
Compiler cannot prove safety
            ↓
Use RefCell
            ↓
Runtime checks borrowing

Classic Example: Mock Objects

Library API:

1
2
3
trait Messenger {
    fn send(&self, msg: &str);
}

Need to record messages:

1
2
3
struct MockMessenger {
    messages: RefCell<Vec<String>>,
}
1
2
3
fn send(&self, msg: &str) {
    self.messages.borrow_mut().push(msg.to_string());
}

The API requires &self.

Without RefCell, mutation is impossible.


Production Example: Cache

1
2
3
struct Calculator {
    cache: RefCell<Option<i32>>,
}
1
fn result(&self) -> i32

looks immutable,

but internally updates the cache.

This is interior mutability.


Rc + RefCell

Very common combination.

1
Rc<RefCell<T>>

Meaning:

  • shared ownership
  • mutable value
  • runtime borrow checking

Useful for:

  • Trees
  • Graphs
  • GUI state
  • Shared mutable data (single thread)

Comparison

FeatureBoxRcRefCell
OwnersOneManyOne
Heap allocation✅✅❌*
Mutable access✅❌✅
Immutable access✅✅✅
Borrow checksCompile timeCompile timeRuntime
Can panic❌❌✅

RefCell<T> itself doesn’t allocate on the heap. It can live on the stack or inside another allocation.


Compile-Time vs Runtime Borrow Checking

Compile Time

Pros

  • No runtime cost
  • Errors found before execution
  • No borrow panics

Cons

  • Compiler may reject safe programs.

Runtime (RefCell)

Pros

  • More flexible
  • Works when compile-time analysis is too restrictive

Cons

  • Runtime overhead
  • May panic if borrow rules are violated

Choosing the Right Smart Pointer

Need heap allocation?

→ Box<T>

Need multiple owners?

→ Rc<T>

Need mutation through &self?

→ RefCell<T>

Need shared ownership and mutation?

→ Rc<RefCell<T>>

Need shared ownership across threads?

→ Arc<T>

Need shared mutable ownership across threads?

→ Arc<Mutex<T>>


Mental Models

Box

One owner.

1
2
3
4
5
Owner
  |
 Box
  |
Value

Rc

Many owners.

1
2
3
4
5
A ----\
       \
        Rc ---> Value
       /
B ----/

RefCell

One owner.

Runtime borrow checking.

1
2
3
4
5
Owner
  |
RefCell
  |
Value

Key Takeaways

  • Box<T> solves where the value lives (heap).
  • Rc<T> solves who owns the value (shared ownership).
  • RefCell<T> solves when borrow rules are checked (runtime instead of compile time).
  • RefCell does not weaken Rust’s borrowing rules; it enforces the same rules at runtime.
  • Prefer compile-time borrowing (&, &mut) whenever possible. Use RefCell only when compile-time analysis is too restrictive.
This post is licensed under CC BY 4.0 by the author.