We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 22 of 24
Interior mutability: moving the check from compile time to run time
pub fn observe(ops: &[String]) -> (Vec<String>, bool)
Keep a log of strings. Two closures share it: one writes to it, one
reads its length. Process each op in order and return
(the finished log, whether an overlapping borrow was ever detected).
The ops, exactly:
| op | effect |
|---|---|
"log <t>" |
append the text <t> |
"count" |
read the current length n, then append "count=<n>" |
"nested" |
take a shared guard on the log, read its length n, ask for an exclusive one while still holding it, record whether that failed, release the shared guard, then append "nested=<n>" |
| anything else |
append "? <op>" |
[] -> ([], false)
["log a","log b"] -> (["a","b"], false)
["log a","count"] -> (["a","count=1"], false)
["nested"] -> (["nested=0"], true)
["log x","nested","log y"]-> (["x","nested=1","y"], true)
["zzz"] -> (["? zzz"], false)
The "nested" op must report the conflict as true, not panic. That
distinction is the whole point.
The starter tries to do this with two ordinary closures over a plain Vec and
does not compile:
error[E0502]: cannot borrow `log` as immutable because it is also
borrowed as mutable
which is the previous item’s lesson: two closures, one holding &mut log and
one wanting &log, cannot coexist.
The trade you are about to make
Everything so far has been “the compiler proves it at compile time, for free”. This item is the first time you give that up, deliberately. It should feel like a trade, not an upgrade.
RefCell<T> implements the same rule — shared XOR exclusive — but enforces it
with a runtime counter instead of a static proof:
-
.borrow()→Ref<T>, incrementing a shared count. -
.borrow_mut()→RefMut<T>, taking the exclusive slot. - If the request conflicts with what is already out, it panics.
-
Guards decrement on
Drop.
What you gain: shapes the checker cannot express — two closures both touching
one value, a graph node reachable from several places, a cache inside an &self
method.
What you pay:
- A counter incremented and checked on every borrow. Small, not zero.
-
A panic path in production.
borrow_mut()while aRefis out will abort your program at runtime, on the machine of whoever is unlucky. The compiler will not warn you. Nothing will, until it happens. - Errors move from your desk to your users. A borrow bug that was a red squiggle is now a 3am page.
That is a real trade and it is not always the wrong one. But “use RefCell“
is never the first answer to a borrow error. Work items 3.6, 3.15 and 3.21
first.
try_borrow_mut — the version that does not panic
match log.try_borrow_mut() {
Ok(mut guard) => guard.push(line),
Err(_) => { /* somebody else has it */ }
}
try_borrow and try_borrow_mut return Result instead of panicking. In
library code, or anywhere a conflict is possible rather than impossible,
they are the honest choice. This problem requires them: the "nested" op
deliberately creates a conflict, and you must report it as a bool.
Cell versus RefCell
Both give you interior mutability. They do it in completely different ways, and picking the right one matters.
Cell<T> never hands out a reference at all. Its whole API is get
(requires T: Copy), set, replace, take, update. Values go in and
come out by copy or by move. Since no reference ever exists, there is nothing
to alias, so there is no counter and no panic — ever. For a Cell<u32>
counter or a Cell<bool> flag, this is strictly better.
RefCell<T> hands out Ref/RefMut guards, so it must count them at
runtime, so it can panic. You need it when you want to operate on the value
in place — pushing to a Vec, mutating a struct field — rather than swapping
it wholesale.
Rule of thumb: if Cell can do the job, use Cell.
The panic messages, verified on 1.95
If you do trip a real panic, the two directions produce two different messages:
-
a failing
borrow_mut()saysRefCell already borrowed -
a failing
borrow()saysRefCell already mutably borrowed
Most tutorials, and the Book, still quote the older
already borrowed: BorrowMutError text. That is stale. So is the belief that
both directions print the same thing. If you ever assert on this in a test,
assert the message that matches the call that actually failed.
Two lints worth knowing
await_holding_refcell_ref — holding a Ref across an .await is a
deadlock-by-construction in async code, because the task can be suspended with
the guard still out. readonly_write_lock — you took a write lock and only
read through it.
(significant_drop_tightening, which suggests dropping a guard earlier than
scope end, is a nursery lint and allow-by-default; it will not fire on this
gate.)
Implementation notes
Write the log as RefCell<Vec<String>> and let both closures capture it by
shared reference — that is the point: &RefCell<T> gives you mutation, so
two closures can share one. For the "nested" op, hold the Ref in a local,
call try_borrow_mut(), and drop(guard) before you write, or your own
subsequent write will conflict too. Finish with into_inner() to take the
Vec back out.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.