We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 16 of 20
Ownership inside loops
Two loops. One gives a value away on every turn; the other must not give anything away at all.
pub fn fold_labels(prefix: String, items: Vec<String>) -> Vec<String>
pub fn survey(items: Vec<String>) -> (usize, Vec<String>)
The table you will use forever
for loops over a collection come in three flavours, and choosing the wrong one
is the single most common beginner mistake in Rust:
| You write | You get | Afterwards |
|---|---|---|
for x in v |
x: T — owned elements |
v is gone |
for x in &v |
x: &T — shared borrows |
v is untouched |
for x in &mut v |
x: &mut T — exclusive borrows |
v is untouched, elements may be edited |
for x in v desugars to v.into_iter(), which takes v by value. That is why
the vector is unusable afterwards, and why the compiler complains at whatever
line touches it next rather than at the loop itself.
This track has no & yet, so fold_labels lives entirely in row one. survey
is where row two becomes unavoidable — and that is the point: it is the first
problem in the course where the only solution is a borrow.
Part 1: fold_labels
The fixed helper eats both strings:
fn join_owned(a: String, b: String) -> String {
format!("{a}/{b}")
}
Return the sequence of running prefixes: start with prefix itself, then
prefix joined with the first item, then that joined with the second, and so
on. ("p", ["a", "b", "c"]) gives
["p", "p/a", "p/a/b", "p/a/b/c"]
and ("p", []) gives ["p"].
The starter’s failure is the loop-flavoured E0382 —
value moved here, in previous iteration of loop
— which most people misread as a compiler bug the first time. It is not. The loop body is checked once and must be valid on every iteration, so a body that consumes a variable declared outside the loop is rejected even if the loop would only run once.
::: question join_owned consumes the accumulator on every turn. What do you feed it on turn two?
The thing it handed back on turn one.
let mut acc = prefix;
for item in items {
acc = join_owned(acc, item);
out.push(acc.clone());
}
acc is moved out and immediately refilled, so the slot is never empty at the
top of the next iteration. This shape has a name — it is a fold — and
Iterator::fold is the same thing with the plumbing hidden:
items.into_iter().fold(prefix, join_owned)
though that only gives you the final value, not the running sequence, so it is not quite enough here.
The acc.clone() in the loop is real and unavoidable: the output needs a
snapshot of the accumulator, and the accumulator has to keep going. One
allocation per element. Notice that this is a different clone from the
starter’s temptation — it buys you an output value rather than papering over a
move error.
One more option to consider and reject: while let Some(x) = items.pop() is
lovely, works, and hands you the items backwards. The three-item test case
exists specifically to catch it.
:::
Part 2: survey
Return (number of non-empty items, the items themselves). The items must come
back to the caller unchanged.
That constraint is what forces the borrow. Any pass that consumes the vector —
into_iter, for x in items, pop in a loop — leaves you with nothing to
return. And indexing does not save you either:
error[E0507]: cannot move out of index of `Vec<String>`
|
| let s: String = items[i];
| ^^^^^^^^ move occurs because value has type `String`
items[i] is a place expression. Assigning it to a String binding means
moving the element out of the vector, which would leave a hole in a vector you
still own — the same objection as the previous problem, in a different costume.
items.iter() yields &String instead. Nothing moves, nothing is emptied, and
items walks out of the function whole:
let nonempty = items.iter().filter(|s| !s.is_empty()).count();
(nonempty, items)
That is a preview of Track 3, and the whole track is about how much this one capability buys you.
Two lints in the neighbourhood
needless_range_loop is on by default and fires on for i in 0..v.len()
when the index is only used to look up v[i]. It is right: the index is
bookkeeping, for x in &v says what you meant, and it cannot go out of bounds.
explicit_iter_loop is pedantic and off by default; it prefers for x in &v
over for x in v.iter(). The two are identical in meaning — &v in a for
loop calls IntoIterator for &Vec<T>, which calls iter() — so this is purely
about which one reads better, and reasonable people disagree.
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.