Skip to content

← Ownership II: Borrowing and the Borrow Checker step 13 of 24

Medium Primitives

Indexing in a loop is usually a borrow-checker workaround — and clippy knows

pub fn subtract_min(xs: &mut [i64]) -> i64

Find the smallest element, subtract it from every element in place, and return the value that was subtracted. An empty slice is untouched and returns 0.

[]          -> 0,   []
[5]         -> 5,   [0]
[3,3,3]     -> 3,   [0,0,0]
[-2,4,0]    -> -2,  [0,6,2]
[0,7]       -> 0,   [0,7]

The starter compiles and passes every test. It fails the grade. Read the clippy output before reading any further here.

Two lessons that turn out to be the same lesson

This item sits at the junction of “satisfy the borrow checker” and “write good Rust”, and the point is that they converge. The iterator rewrite is simultaneously safer, usually faster, and makes borrow errors disappear. That is not a coincidence, and it is worth understanding why.

The borrow lesson, hiding inside

The natural draft is:

let m = xs.iter().min();        // Option<&i64> — a SHARED loan of xs
for i in 0..xs.len() {
    xs[i] -= m.unwrap();        // needs an EXCLUSIVE loan -> E0502
}

The shared loan created by .iter().min() is still alive inside the loop, because m is a reference into the slice and you keep using it. So the mutation is rejected.

What actually fixes it is not the loop at all: it is getting the i64 out of the Option<&i64>. Whether you write *v, .copied(), .cloned(), or destructure with Some(&m), the effect is the same — you end up holding an independent integer, the loan dies on that line, and the mutable loop is free to start.

That is worth saying explicitly because it is easy to miss: the dereference is the fix, not the loop rewrite. The starter already does it (Some(v) => *v), which is exactly why the starter compiles.

The clippy lesson, sitting on top

error: the loop variable `i` is only used to index `xs`
  --> src/lib.rs:6:14
   |
 6 |     for i in 0..xs.len() {
   |              ^^^^^^^^^^^
   = note: `-D clippy::needless-range-loop` implied by `-D warnings`
help: consider using an iterator
   |
 6 |     for <item> in xs.iter_mut() {

needless_range_loop is the gate here. Its argument is not aesthetic:

  • xs[i] is a bounds-checked access. Every iteration pays a compare and a branch. LLVM often eliminates it for a simple 0..len loop, but “often” is doing real work in that sentence — put anything non-obvious in the loop and the check stays.
  • iter_mut() cannot be out of bounds by construction, so there is nothing to check and nothing to eliminate.
  • The iterator version cannot have an off-by-one. It is not that off-by-ones are unlikely; it is that the expression is not there to be wrong.

The idiomatic form is for x in xs.iter_mut() { *x -= m; }.

Related lints in the same family: manual_memcpy (a loop copying src[i] into dst[i] — use copy_from_slice), explicit_counter_loop (a hand-rolled i += 1 alongside a for — use .enumerate()), and mut_range_bound (you changed n inside for i in 0..n, which does nothing because the range was built before the loop began). indexing_slicing is a stricter, allow-by-default restriction lint that bans xs[i] outright in codebases that want no panicking paths at all; it is not part of this gate.

Being honest: when indexing is right

Clippy is a linter, not an oracle, and this particular lint has a history of suggesting code that does not compile (rust-clippy issues #16344, #5625, #2036, #15596 among others). #[allow(clippy::needless_range_loop)] with a one-line comment explaining why is a legitimate professional outcome, not a defeat. This gate will not accept it, which is why this problem was chosen to be one where the iterator form genuinely wins.

The honest list of cases where the index is the right tool:

  • Two independent indices. while i < n && j < m merging two sequences. Two iterators would need peekable and be less clear.
  • Non-unit stride. for i in (0..n).step_by(3) is fine, but if the pattern is irregular, indices win. (chunks/windows cover the regular cases.)
  • Wraparound. v[(i + 1) % n] — a neighbour relation that an iterator does not express. windows(2) handles the non-wrapping version.
  • The index is the subject. Binary search, dynamic-programming tables, graph adjacency by node id. Here i is not a way to reach the data; it is the data.

Rule of thumb, and the exact thing clippy’s heuristic is approximating: if the loop variable appears only inside [...], it is a workaround and the iterator is better. If the loop variable appears anywhere else — in arithmetic, in a comparison, in the output — it is real and indexing is honest.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…