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
pub fn apply_ops(xs: &mut Vec<i64>, ops: &[String]) -> Vec<String>
Apply each op to the vector in order, and return one log line per op.
Recognised ops, exactly:
| op | effect |
|---|---|
"push <n>" |
append the integer n |
"pop" |
remove the last element, if any |
"double" |
multiply every element by 2 |
After a recognised op, log "<op> -> len=<new length>". Anything else — an
unknown word, or a "push " whose argument does not parse as an integer — is
logged as an error and does not panic: log "<op> -> error" and leave the
vector alone.
"pop" on an empty vector is not an error. It is a recognised op that
removes nothing, and it logs "pop -> len=0".
xs [1], ops ["push 5"] -> xs [1,5], log ["push 5 -> len=2"]
xs [1], ops ["frobnicate"] -> xs [1], log ["frobnicate -> error"]
xs [2], ops ["push x"] -> xs [2], log ["push x -> error"]
xs [], ops ["pop","push 3","double"]
-> xs [6], log ["pop -> len=0","push 3 -> len=1","double -> len=1"]
The starter does not compile.
The borrow you cannot see
let mut push = |n: i64| xs.push(n);
There is no &, no &mut, no type annotation anywhere on that line. It
reads like a definition. It is a borrow: the closure captured xs
exclusively, and holds that loan for its entire lifetime as a value — not for
the duration of a call, for as long as push is alive.
So every later use of xs collides:
error[E0501]: cannot borrow `*xs` as mutable because previous closure
requires unique access
Closures are where borrow errors are least legible, precisely because the
borrow is invisible. Learners routinely conclude that closures are broken, or
that Rust “doesn’t let you use closures”, when in fact the closure is doing
exactly what a let r = &mut xs; would do — with none of the syntax that
would have made it obvious.
The four codes you will meet around closures, and what each means:
- E0500 — a closure requires unique access to something already borrowed.
- E0501 — the reverse: you tried to borrow something a live closure already has uniquely. This is the starter’s error.
-
E0373 — a closure outlives the function but captures a local by
reference. The usual fix really is
move. - E0524 — two closures both want unique access to the same thing.
What a closure captures, and how
Rust picks the weakest capture that makes the body work, per variable:
-
reads only →
&T -
writes →
&mut T(and the closure must itself bemutto be called) - consumes the value → by move
move forces the last one for everything. That is why move is not the
universal fix people reach for: here it would take ownership of xs, and
xs is a &mut Vec<i64> parameter that the caller needs back. Moving a
&mut into a closure means nothing outside can ever touch the vector again —
which is worse, not better.
The fix
Stop capturing. Pass the data in as a parameter.
let apply = |v: &mut Vec<i64>, op: &str| -> bool { /* ... */ };
// ...
if apply(xs, op) { /* xs is free again the instant the call returns */ }
A parameter is borrowed for the duration of the call and not one instruction
longer. Between calls there is no outstanding loan and xs.len() is fine.
This is the lesson, and it comes with a teaching problem: a learner who starts by writing the parameter version never sees the error at all. Hence the fix-it shape — the broken closure is written for you so that you meet E0501 once, deliberately, and recognise it forever.
(Nothing forces you to keep a closure at all. Inlining the whole thing into the loop is perfectly good Rust. The parameter version is shown because “capture it” versus “pass it in” is the distinction worth carrying away.)
RFC 2229: closures capture fields, since edition 2021
One modernity check, because it changes what old answers mean.
Before edition 2021, a closure that touched a.y captured the whole of a.
Since 2021 (RFC 2229, “disjoint closure captures”) it captures the place
a.y — so this now compiles:
let mut a = Pair { x: vec![1], y: 0 };
let f = || println!("{}", a.y);
a.x.push(2); // fine in 2021+; E0502 before
f();
Your code compiles as edition 2024, so you get the new behaviour. Stack
Overflow answers written before 2021 may describe errors you will never see,
and workarounds (let y = &a.y; before the closure) that are now redundant.
Two migration caveats, since they are real and occasionally bite:
- Drop order changed. Capturing fewer things means the un-captured parts are dropped at a different time than they used to be.
-
Auto-trait impls can change. A closure that no longer captures a
non-
Sendfield is nowSend. Usually a gift; occasionally it changes which overload or bound applies.
Watch for
redundant_closure (a closure that just forwards to one function — pass the
function) and needless_borrow (&x where x already coerces).
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.