Box, Rc, RefCell and Interior Mutability

Article · 15:00

Ownership says one owner and borrowing says one mutable reference, and most programs fit those rules. For the ones that do not, the standard library moves the check elsewhere: Box to the heap, Rc to a count, RefCell to runtime - and Weak to break a cycle.

Box, Rc, RefCell and Interior Mutability

Ownership says one owner, and borrowing says one mutable reference. Most programs fit those rules. For the ones that do not, the standard library provides types that move the check somewhere else - to the heap, to a reference count, or to runtime.

Box: one owner, on the heap

fn main() {
    let boxed = Box::new(42);
    println!("{boxed} {}", *boxed + 1);      // Deref makes it feel like the value

    // The two real uses. First: a type whose size is not known at compile time,
    // because it contains itself.
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("{}", sum(&list));

    // Second: a trait object, when the concrete type varies.
    let shapes: Vec<Box<dyn Fn(f64) -> f64>> = vec![
        Box::new(|x| x * 2.0),
        Box::new(|x| x * x),
    ];
    for f in &shapes {
        print!("{} ", f(3.0));
    }
    println!();
}

enum List {
    Cons(i32, Box<List>),        // without Box this type would be infinite
    Nil,
}

fn sum(list: &List) -> i32 {
    match list {
        List::Cons(value, rest) => value + sum(rest),
        List::Nil => 0,
    }
}

Rc: several owners, single-threaded

Rc counts references. The value is dropped when the last one goes. It gives you shared ownership, not shared mutation - everything through an Rc is immutable.

use std::rc::Rc;

#[derive(Debug)]
struct Course {
    title: String,
}

fn main() {
    let course = Rc::new(Course { title: String::from("Rust") });
    println!("count: {}", Rc::strong_count(&course));

    let in_catalogue = Rc::clone(&course);      // cheap: a count bump, not a copy
    let in_search = Rc::clone(&course);
    println!("count: {}", Rc::strong_count(&course));

    println!("{} {} {}", course.title, in_catalogue.title, in_search.title);

    drop(in_catalogue);
    println!("count after one drop: {}", Rc::strong_count(&course));

    // Rc::clone is deliberately not .clone() by convention: it makes the
    // cheapness visible at the call site.
}

RefCell: the borrow check, moved to runtime

RefCell lets you mutate through a shared reference. The rule is still one writer or many readers - it is simply enforced at runtime, and breaking it panics instead of failing to compile.

use std::cell::RefCell;

fn main() {
    let log = RefCell::new(Vec::<String>::new());

    // Note: `log` is not mut, yet this works.
    log.borrow_mut().push(String::from("first"));
    log.borrow_mut().push(String::from("second"));

    println!("{:?}", log.borrow());
    println!("{} entries", log.borrow().len());

    // The rule is checked at runtime. This is the panic it produces:
    let first = log.borrow();
    let second = log.try_borrow_mut();
    println!("a mutable borrow while a shared one is alive: {:?}", second.is_err());
    drop(first);
    println!("once the shared borrow is dropped: {:?}", log.try_borrow_mut().is_ok());
}

Use it when you genuinely need interior mutability - a cache inside an otherwise immutable object, a counter in a callback - and not as a way around the borrow checker. A RefCell panic is the same bug the compiler would have caught, found later and by a user.

Rc<RefCell>: shared, mutable, single-threaded

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

#[derive(Debug)]
struct Basket {
    items: Vec<String>,
}

fn main() {
    let basket = Rc::new(RefCell::new(Basket { items: Vec::new() }));

    let from_web = Rc::clone(&basket);
    let from_api = Rc::clone(&basket);

    from_web.borrow_mut().items.push(String::from("a course"));
    from_api.borrow_mut().items.push(String::from("another"));

    println!("{:?}", basket.borrow());
    println!("owners: {}", Rc::strong_count(&basket));
}

This combination is common in tree and graph structures, and in single-threaded event systems. For anything across threads the equivalent is Arc<Mutex<T>>, which is the next lesson.

Weak, and the cycle that would leak

Rc is a reference count, and two Rcs pointing at each other never reach zero. Weak is a reference that does not count.

use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,      // Weak: does not keep the parent alive
    children: RefCell<Vec<Rc<Node>>>, // Rc: the parent owns its children
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent: {:?}", leaf.parent.borrow().upgrade().map(|n| n.value));

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent: {:?}", leaf.parent.borrow().upgrade().map(|n| n.value));
    println!("branch strong={} weak={}", Rc::strong_count(&branch), Rc::weak_count(&branch));
    println!("leaf   strong={} weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));

    println!("{}", branch.children.borrow().len());
}

upgrade() returns an Option because the target may be gone - which is the whole point, and is how a child can reach its parent without keeping it alive.

Drop: the destructor that always runs

struct Connection {
    name: &'static str,
}

impl Drop for Connection {
    fn drop(&mut self) {
        println!("  closing {}", self.name);
    }
}

fn main() {
    println!("opening");
    {
        let _first = Connection { name: "db" };
        let _second = Connection { name: "cache" };
        println!("  both open");
    }   // dropped here, in reverse order of declaration

    // drop() moves the value, which is how you close one early.
    let early = Connection { name: "early" };
    drop(early);
    println!("after the early drop");

    // A panic still runs Drop while unwinding, which is what makes RAII
    // reliable: there is no `finally` because there is no need for one.
    let _outer = Connection { name: "outer" };
    println!("main is returning");
}

What to take away

  • Box<T> is one owner on the heap: for recursive types and trait objects.
  • Rc<T> is shared ownership by counting; Rc::clone is a count bump, not a copy.
  • RefCell<T> moves the borrow check to runtime - and breaking it panics.
  • Rc<RefCell<T>> is shared mutable state in one thread; Arc<Mutex<T>> across threads.
  • Weak<T> breaks reference cycles, and upgrade() returns an Option because it may be gone.
  • Drop runs on scope exit and while unwinding, which is why Rust needs no finally.