We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 10 of 26
`Cell<T>`: mutation by moving values in and out
Count arrivals during a fixed graph walk.
pub fn visit_counts(edges: Vec<(usize, usize)>, start: usize) -> Vec<u32>
The node count is 1 + max(start, every index appearing in edges). Edges are
directed and kept in input order, which is what makes the walk
deterministic.
The walk is a depth-first traversal from start with one twist:
- every arrival at a node increments its counter, revisits included;
- a node recurses into its neighbours only on its first arrival.
Return the counter of every node in index order. Nodes never reached stay at
0. Self-loops and cycles are allowed and must terminate — the “recurse only
on first arrival” rule is what guarantees that.
The constraint that makes this a Cell problem
The traversal function is handed &[Node] — a shared reference to the
whole graph — and it recurses into itself while that reference is live. You
cannot change it to &mut [Node], because the recursive call would then need
a second exclusive borrow of the same slice, and the borrow checker will not
give you one. That is not a puzzle to route around; it is the shape of every
graph traversal ever written.
So the starter’s field is a plain u32, and the starter does not compile:
error[E0594]: cannot assign to `nodes[_].visits`, which is behind a `&` reference
| nodes[i].visits += 1;
| ^^^^^^^^^^^^^^^^^^^^ `nodes` is a `&` reference, so it cannot be written to
help: consider changing this to be a mutable reference
Ignore that help: line. It is the compiler’s usual first guess and it is
wrong here — following it just moves the error to the recursive call. The
fix is to change the type of the field, not the type of the reference.
Cell<T>: the one that cannot panic
Cell<T> is the simplest interior-mutability type in the language, and it is
sound with zero ceremony for one reason that is worth stating precisely:
Cellnever hands out a reference to its contents at all.
There is no &T to the inside, ever. You put a value in, you take a value
out, you swap one for another. Since no reference to the interior can escape,
there is nothing for a second borrow to conflict with, so there is nothing to
check — no flags, no counters, no branch, and no way to panic. A
Cell<u32> is exactly as big as a u32 and exactly as fast.
The API follows from that design:
| method | needs | does |
|---|---|---|
get() |
T: Copy |
returns a copy |
set(v) |
— | overwrites, dropping the old value |
replace(v) |
— | overwrites and returns the old value |
take() |
T: Default |
replaces with the default and returns the old |
swap(&other) |
— | exchanges two cells’ contents |
update(f) |
T: Copy |
reads, applies f, writes back |
into_inner() |
consumes the cell | returns the value |
Note that get requires Copy — and note which error you get if you forget
that, because it is not the one you would guess:
error[E0599]: the method `get` exists for struct `Cell<String>`,
but its trait bounds were not satisfied
= note: the following trait bounds were not satisfied:
`String: Copy`
That is E0599 — method not found — not E0277. get is defined in an
impl<T: Copy> Cell<T> block, so for a non-Copy T the method genuinely
does not exist on that type. Getting used to reading E0599 as “you are inside
the wrong impl block” will save you a lot of time.
update is stable as of Rust 1.88, and c.update(|v| v + 1) says what a
counter increment means more clearly than c.set(c.get() + 1).
When Cell is the right answer — which is more often than you think
Counters. Flags. Dirty bits. Memoised scalars. Generation numbers. Anything
small and Copy that gets swapped wholesale rather than edited in place.
For all of those, Cell is strictly better than RefCell: same
expressive power for this shape of data, smaller, faster, and — the part that
matters — it removes an entire class of runtime failure. Learners who meet
RefCell first tend to use it for everything, then get a BorrowMutError on
a counter increment. Meeting Cell first makes RefCell‘s runtime
bookkeeping feel like something you buy when you need it.
Two facts about Cell that folklore gets wrong
You will read that “Cell doesn’t implement the comparison traits”. Half of
that is true and the half people repeat is the wrong half.
-
Cell<T>does not implementHash. So it cannot be aHashMapkey, and the attempt fails with a trait error long before clippy’smutable_key_typegets involved. That restriction is real and it is the right call — a key whose hash can change is a key you can never find again. -
Cell<T>does implementOrdandPartialOrdwhenT: Ord + Copy/T: PartialOrd + Copy.Cell::new(1) < Cell::new(2)istrue, andvec![Cell::new(3), Cell::new(1)].sort()compiles and works.
And one genuine hazard: you cannot mutate a Cell<Vec<T>> in place. There
is no &mut to the interior, so the only route is take(), mutate the owned
vector, set() it back — and between those two calls the cell holds an
empty vector. Any re-entrant code that reads it in that window sees nothing.
That is a real bug shape, and it is the point at which RefCell stops being
optional.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.