Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 16 of 20

Medium Primitives

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…