We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 15 of 26
`Rc<RefCell<T>>`: the workhorse, and when it is a smell
Build the same event bus twice — once with shared ownership, once with an arena — and get identical answers from both.
pub fn event_bus(subscribers: Vec<String>, events: Vec<String>) -> Vec<(String, Vec<String>)>
pub fn event_bus_arena(subscribers: Vec<String>, events: Vec<String>) -> Vec<(String, Vec<String>)>
Each subscriber name gets one inbox. A name listed twice registers a second route to that same inbox — so a matching event lands in it twice. That duplication is deliberate: it is what proves the two registrations share one inbox rather than owning separate ones.
An event is "<topic> <text>", split at the first space; an event with no
space has an empty text. A topic of * matches everyone. A topic matching no
subscriber is dropped.
Return, sorted by subscriber name, one (name, messages) pair per
distinct name, messages in delivery order.
Version one: Rc<RefCell<Vec<String>>>
This is the pattern you will meet in real Rust more than any other in this track, and it composes two things you already know:
-
Rc<T>— several owners of one value, but only&Tout of it; -
RefCell<T>— mutation through&T, checked at runtime.
Together: shared ownership of one mutable cell. The dispatcher and the subscriber both own the inbox; either can write to it; it lives exactly as long as the last of them.
The nesting order matters and is not arbitrary. Rc<RefCell<T>> is “several
owners of one mutable thing”. RefCell<Rc<T>> is “one mutable slot that
currently holds a shared handle” — occasionally what you want (a re-pointable
link), almost never what you meant.
Two neighbours in the same family, for completeness: Arc<RefCell<T>> is
what people write when they first need threads, and it is wrong — RefCell
is Send but never Sync, so the Arc buys nothing. Clippy’s default-on
arc_with_non_send_sync catches it at the construction site, which is exactly
where the starter fails:
error: usage of an `Arc` that is not `Send` and `Sync`
= note: `Arc<RefCell<Vec<String>>>` is not `Send` and `Sync`
as `RefCell<Vec<String>>` is not `Sync`
= help: if the `Arc` will not be used across threads replace it with an `Rc`
= help: otherwise … consider a wrapper type such as `Mutex`
And Rc<Mutex<T>> is the mirror-image nonsense: an atomic lock guarding data
that a non-atomic refcount already proves is single-threaded. Clippy has
rc_mutex for it, allow-by-default.
Version two: the arena
Now write it again with Rc and RefCell banned. Keep the inboxes in one
Vec<Vec<String>>, keep the names in a parallel Vec<String>, and make a
route a plain usize. Two registrations of the same name push the same index
twice.
Compare the two when you are done. The arena version is shorter, has no
runtime borrow checks, cannot panic, cannot leak a cycle, is Send, and its
inboxes are contiguous in memory. The Rc<RefCell<_>> version has one thing
the arena does not: the inbox handle can be carried away by a subscriber
that has no idea the bus exists.
That is the whole trade, and it is worth being able to state it in one sentence.
When Rc<RefCell<T>> is right, and when it is a crutch
This is a live disagreement in the Rust community, not settled doctrine, so here is the honest version of both sides.
Legitimate: observer and callback graphs where handlers outlive the dispatch loop; GUI widget trees where a child needs a handle to something it does not own; interpreter environments with genuine multi-owner scoping; caches with real shared ownership. In all of these the sharing is not a workaround — it is the design.
A smell: using it to dodge the borrow checker; building linked lists and
general graphs where Vec<Node> plus usize handles is simpler, faster,
cycle-safe and Send; reaching for it because a function signature was
awkward.
The decision criterion that actually works:
RefCellpanics are a runtime failure mode you are choosing to accept. Accept it when the borrow pattern is genuinely dynamic. Refuse it when it is standing in for thinking about ownership.
And remember that every .borrow_mut() composes badly: two independent
modules, each correct in isolation, can borrow the same cell in a combination
neither author tested, and the failure appears only on that path.
Two hazards worth naming
Type complexity. Rc<RefCell<Vec<Rc<RefCell<Node>>>>> is a type people
genuinely write, and clippy’s default-on type_complexity will tell you so. A
type alias is the minimum fix. A redesign is usually the real one — this
problem uses type Inbox = Rc<RefCell<Vec<String>>>; for exactly that reason.
Never #[derive(Debug)] on a cyclic Rc<RefCell<Node>>. The derived
Debug follows every Rc it can reach, and a cycle makes it recurse forever
— unbounded output, no error, no stack trace, just a process that never comes
back. When you want to inspect shared structure, print Rc::strong_count, not
{:?}.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.