We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 15 of 24
Split borrows: the checker reasons about places, not variables
This exercise gives you a struct with three fields and a method that reads one field while writing the others. As written it does not compile. Your job is to make it compile without changing what it does.
pub struct Inventory {
pub items: Vec<String>,
pub counts: Vec<i64>,
pub log: Vec<String>,
}
pub fn run(items: Vec<String>, counts: Vec<i64>, ops: Vec<(String, i64)>)
-> (Vec<i64>, Vec<String>)
The contract
run builds an Inventory from items and counts with an empty log,
applies each (name, n) op in order through Inventory::restock, and returns
(counts, log).
restock(name, n) scans items from index 0 upward. For every index i
where items[i] == name, it adds n to counts[i] and appends one log line
that shows the new count. If no index matched, it appends one miss line
instead. Names are compared exactly: the comparison is case sensitive and
byte for byte, with no trimming.
Inputs you can rely on:
itemsandcountshave the same length. Either may be empty.itemsmay contain the same name more than once.opsmay be empty, in which casecountscomes back unchanged andlogis empty.- Every running total stays within the range of
i64, so you do not need to handle overflow.
The log format is exact, character for character:
hit: "<name> <signed n> -> <new count>" e.g. "bolt +3 -> 8"
"bolt -2 -> 3"
"w +0 -> 3"
miss: "<name> miss" e.g. "gizmo miss"
The sign of n is always written, including +0 for zero. The {:+} format
specifier does this: format!("{name} {n:+} -> {total}"). The new count is
printed without a forced sign, so a negative total prints as, for example,
-4.
items ["bolt","nut"], counts [5,1], ops [("bolt",3)]
-> counts [8,1], log ["bolt +3 -> 8"]
items ["a","a"], counts [1,2], ops [("a",1)]
-> counts [2,3], log ["a +1 -> 2", "a +1 -> 3"] <- every match, not just the first
The error
The starter code routes every log line through a small helper,
fn note(&mut self, line: String). Compiling it gives:
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
|
| for (i, item) in self.items.iter().enumerate() {
| -----------------------------
| |
| immutable borrow occurs here
| immutable borrow later used here
...
| self.note(format!("{item} {n:+} -> {}", self.counts[i]));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
The loop holds a shared borrow of self.items for as long as it runs,
because item points into that vector. Calling self.note(...) inside the
loop needs an exclusive (&mut) borrow, and the error says that exclusive
borrow conflicts with the shared one.
Why inlining the helper compiles
Replace the call with the helper's body and the same logic compiles:
for (i, item) in self.items.iter().enumerate() {
if item == name {
self.counts[i] += n;
self.log.push(format!("{item} {n:+} -> {}", self.counts[i]));
}
}
The program touches the same memory as before. The difference is what the
borrow checker can see. The borrow checker tracks borrows of places, not
just of whole variables. A place is a path to a piece of memory, such as
self.items or self.log. Inside one function body, the checker knows that
self.items, self.counts and self.log are different fields, so a shared
borrow of self.items can exist at the same time as exclusive borrows of
self.counts and self.log. Borrowing different fields of one struct at the
same time like this is called a split borrow. The article on
aliasing and mutability
covers places in more detail.
A method call hides that detail. The borrow checker checks each function on
its own and, at a call site, looks only at the callee's signature, not its
body. note's signature says &mut self, which means exclusive access to the
whole Inventory. Rust has no stable syntax for a parameter type that means
"exclusive access to only the log field of self". So at the call site the
checker must assume note may touch any field, including items, and that
conflicts with the loop's borrow of self.items.
In this example, then, the rejection comes from what a method signature can express, not from the code touching overlapping data.
Fix: destructure self
At the top of restock, split self into its fields:
let Inventory { items, counts, log } = self;
self has type &mut Inventory. When a pattern for a struct is matched
against a reference to that struct, each field is bound by reference (this is
called a binding mode; see
binding modes).
Here every field is bound by ref mut, giving three local variables of types
&mut Vec<String>, &mut Vec<i64> and &mut Vec<String>. Each one borrows a
different field, so the checker lets you read items while you write to
counts and log. In the loop you then write log.push(...) instead of
calling the helper.
Two other common fixes:
- Make the helper take only what it needs. A free function
fn note(log: &mut Vec<String>, line: String)called asnote(&mut self.log, line)borrows only thelogfield. - Compute first, then mutate. Collect the new log lines in a local
Vecwhile scanning, and append them toself.logafter the loop has finished withself.items. The copy out of the loan exercise uses the same idea.
If you stop calling note, delete it. An unused private method produces a
dead_code warning, and the grade treats warnings as errors.
Possible future language support
Rust language designers have discussed extensions, sometimes called view types, that would let a signature name the fields a method uses. None of them is part of stable Rust, and this exercise does not depend on them. The fixes above work on stable Rust today.
Your submission must compile, pass the tests and pass
cargo clippy -- -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.