We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 21 of 24
Closures hold their borrows
Modify a vector by applying operations in order, and return one log entry for each operation.
pub fn apply_ops(xs: &mut Vec<i64>, ops: &[String]) -> Vec<String>
| Operation | Effect |
|---|---|
push <n> |
Append the parsed i64 value. |
pop |
Remove the last element if there is one. |
double |
Multiply each element by two. |
Recognize pop and double exactly. For a push, require the exact prefix
push followed by a signed decimal integer accepted by parse::<i64>():
an optional + or -, then one or more ASCII digits, with a value in range.
Leading zeros are valid. Do not trim whitespace, accept underscores or change
letter case. Thus push +03 is valid, while push 3, push 3 and Push 3
are errors. An out-of-range integer is also an error.
After a recognized operation, append "<op> -> len=<new length>". For an
invalid operation, append "<op> -> error" and leave the vector unchanged.
In both cases, <op> is the original string, including its spaces.
Popping an empty vector succeeds and logs pop -> len=0. Empty operations
leave the vector unchanged and return an empty log. Inputs ensure that every
multiplication performed by double fits in i64; arithmetic overflow is
not an additional operation-error case.
For example, starting with an empty vector and applying
["pop", "push 3", "double", "push x"] leaves [6] and returns:
pop -> len=0
push 3 -> len=1
double -> len=1
push x -> error
Why the starter's capture conflicts
The starter defines let mut push = |n: i64| xs.push(n);. This closure
captures access to xs so it can mutate the vector. The loop calls the
closure, then also uses xs directly to pop, double or read its length.
Because the closure may be called again on a later iteration, the exclusive
borrow overlaps those direct accesses. Rust reports E0501.
A captured borrow does not necessarily last until the end of the closure binding's scope. When no later use needs the borrow, it can end earlier:
fn main() {
let mut values = vec![1];
let mut push = |n| values.push(n);
push(2);
// No more uses of push, so its borrow no longer blocks this access.
values.push(3);
assert_eq!(values, [1, 2, 3]);
}
The starter cannot use that reasoning between iterations: its closure still needs the vector for future calls.
Pass the vector into the operation
Give the closure a vector parameter instead of capturing the vector. Here is a smaller example of the same pattern:
fn main() {
let push = |values: &mut Vec<i64>, number| values.push(number);
let mut values = vec![1];
push(&mut values, 2);
assert_eq!(values.len(), 2);
push(&mut values, 3);
assert_eq!(values, [1, 2, 3]);
}
This closure neither stores nor returns the borrowed vector. Each call can therefore finish its borrow before the next direct access. A function or closure that returns a borrowed value could keep a borrow needed for longer; passing a parameter alone does not guarantee that every borrow ends at return.
In your solution, an operation helper can accept &mut Vec<i64> and &str
and return a bool indicating whether it recognized a valid operation.
After the call, use that result and xs.len() to build the log entry.
A named helper function or inline loop logic is also valid.
What move changes
Adding move is not a repair for the starter's competing accesses. It would
move the &mut Vec<i64> reference into the closure, leaving the local xs
binding unavailable for the loop's other branches. It does not move the
caller's vector allocation or prevent the caller from using its vector once
this function returns.
Capture ownership also differs from the callable traits. The starter's closure
needs mutable access when called, so its binding is mut. The parameter-based
closure above captures nothing and can mutate its argument without a mutable
closure binding. A move closure may still implement Fn if calling it only
reads its captured state.
Separate fields can be borrowed separately
In edition 2021 and later, closure capture can be precise enough to borrow one field while leaving another available:
struct Pair {
values: Vec<i64>,
count: usize,
}
fn main() {
let mut pair = Pair { values: vec![1], count: 7 };
let read_count = || pair.count;
pair.values.push(2);
assert_eq!(read_count(), 7);
assert_eq!(pair.values, [1, 2]);
}
This does not mean every field access results in a separate capture. Using the whole value or an array index, for example, can require a broader capture. The starter's closure and direct accesses all need the same vector, so field precision does not resolve that conflict.
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.