How it works
The explanation, with examples you can run.
The common ones
use std::cell::RefCell;use std::rc::Rc;use std::sync::Arc;use std::thread; #[derive(Debug)]enum List { Cons(i32, Box<List>), // Box gives the recursive type a known size Nil,} fn sum(list: &List) -> i32 { match list { List::Cons(v, rest) => v + sum(rest), List::Nil => 0, }} fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil)))); println!("{list:?} sum={}", sum(&list)); let shared = Rc::new(RefCell::new(vec![1])); let other = Rc::clone(&shared); // a second owner, not a copy of the Vec other.borrow_mut().push(2); // borrow rules checked at run time println!("{:?} owners={}", shared.borrow(), Rc::strong_count(&shared)); let config = Arc::new(String::from("prod")); let handles: Vec<_> = (0..2) .map(|i| { let config = Arc::clone(&config); // atomic count: safe across threads thread::spawn(move || format!("worker {i} on {config}")) }) .collect(); for h in handles { println!("{}", h.join().unwrap()); }}It prints Cons(1, Cons(2, Nil)) sum=3, [1, 2] owners=2, then worker 0 on prod and worker 1 on prod.
Box<T>: one owner, value on the heap. Use it for recursive types, for large values you want to move cheaply, and for trait objects (Box<dyn Trait>).Rc<T>: several owners on one thread.Rc::cloneincrements a counter; the value is dropped when the lastRcgoes. AnRconly gives shared (&T) access.Arc<T>: the same, with an atomic counter, so it can cross threads. The atomic operations cost a little more, which is whyRcexists.RefCell<T>: interior mutability.borrow()andborrow_mut()apply the shared-or-mutable rule at run time and panic on a violation.Rc<RefCell<T>>is the usual single-threaded "shared and mutable" combination; across threads it isArc<Mutex<T>>orArc<RwLock<T>>.Cell<T>swaps wholeCopyvalues in and out with no borrow tracking at all.
Costs and traps
Every one of these makes a cost visible in the type: a heap allocation, a counter update, a run-time borrow check. That is the trade Rust makes: you pay only where you ask for flexibility.
Reference counting can't free cycles. Two Rcs pointing at each other keep each other's count above zero forever. For parent links, trees and observer lists, make the back-pointer a Weak<T> (from Rc::downgrade), which doesn't keep the value alive; upgrade() returns None once it is gone.
A RefCell panic (on Rust 1.99 the message is RefCell already borrowed) is the run-time version of a borrow-checker error. If you hit them often, restructure so that ownership is clear rather than wrapping more in RefCell.
Heap or stack is mostly invisible in safe code: moving a Box moves only the pointer, and dereferencing works through Deref, so *b, b.method() and &*b all reach the value inside.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.