Skip to content

← Ownership II: Borrowing and the Borrow Checker step 22 of 24

Medium Primitives

Share a log with RefCell and report borrow conflicts

Keep a string log shared by two closures: one appends a line and the other reads its length. Use RefCell<Vec<String>> so both closures can capture the same cell by shared reference.

pub fn observe(ops: &[String]) -> (Vec<String>, bool)

Process operations in order. Return the finished log and a flag recording whether a requested mutable borrow conflicted with a live shared borrow. The flag starts false and stays true once a conflict is detected.

Operation Effect
log <text> Append everything after the literal log prefix
count Read the length n, then append count=<n>
nested Hold a shared guard, read length n, try a mutable borrow, record its failure, release the shared guard, then append nested=<n>
Anything else Append ? followed by the original operation

Every operation adds exactly one entry, including count and nested. Lengths refer to the log before adding that operation's entry.

[]                          -> ([], false)
["log a", "count"]          -> (["a", "count=1"], false)
["nested", "count"]         -> (["nested=0", "count=1"], true)
["log x", "nested", "log y"] -> (["x", "nested=1", "y"], true)
["zzz"]                     -> (["? zzz"], false)

Matching is case-sensitive and does not trim whitespace. log appends an empty string; log a appends a space followed by a. Bare log, count and an empty operation are unknown operations, producing ? log, ? count and ? respectively. Preserve Unicode and whitespace in logged text.

Why the starter fails

The starter's write closure captures a mutable borrow of a plain Vec. The count closure then tries to borrow that Vec immutably while the write closure still needs its capture. The compiler rejects the overlap with E0502.

RefCell allows shared access to the container while checking borrows of its contents at runtime. This does not turn off Rust's static checks: references and guards still have lifetimes, and the closures still borrow the cell.

Keep each guard short-lived

borrow returns a shared Ref guard. borrow_mut returns a mutable RefMut guard. Multiple shared borrows can coexist, but a mutable borrow conflicts with either kind. Dropping guards releases their borrows. The ordinary methods panic on conflict; try_borrow and try_borrow_mut return an error instead.

For nested, actually attempt try_borrow_mut while the shared guard is live. Do not merely set the flag because the command is named nested. Drop the shared guard before calling the writer, so the subsequent append can borrow mutably. Normal reads and writes should finish their borrows within their closure calls. Finish with into_inner to recover the vector after the closures' last use.

This small example demonstrates the required guard order:

use std::cell::RefCell;

fn main() {
    let log = RefCell::new(vec![String::from("first")]);
    let guard = log.borrow();
    assert!(log.try_borrow_mut().is_err());
    drop(guard);
    log.borrow_mut().push(String::from("second"));
    assert_eq!(log.into_inner(), ["first", "second"]);
}

See the RefCell documentation. The deliberate conflict must be reported without panicking. Exact panic text is not part of the task, and a panic does not necessarily abort the process: that depends on the panic strategy and how unwinding is handled.

Choosing Cell or RefCell

Cell is useful when replacing or moving a whole value is convenient. Its get method copies a Copy value, while take can move out a Default value. It does not track dynamic borrows. RefCell is convenient here because the closures borrow the vector to inspect it or push in place.

Cell can also hold a Vec: a take/modify/set approach is possible, but does not demonstrate the borrow-conflict behavior this exercise asks you to observe. Cell is not an unconditional promise that no operation can panic, and it has get_mut for callers with exclusive access to the cell. See the Cell documentation.

Passing requires compilation, tests and Clippy with warnings denied.