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::cloneis 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, andupgrade()returns anOptionbecause it may be gone.Dropruns on scope exit and while unwinding, which is why Rust needs nofinally.