S10: Index-based loops instead of iterators
What it looks like
Section titled “What it looks like”pub fn total(prices: &[u64]) -> u64 { let mut sum = 0; for i in 0..prices.len() { sum += prices[i]; } sum}
/// The price change of each product between two price lists.pub fn price_changes(old: &[u64], new: &[u64]) -> Vec<i64> { let mut changes = Vec::new(); for i in 0..old.len() { let before = i64::try_from(old[i]).unwrap_or(i64::MAX); let after = i64::try_from(new[i]).unwrap_or(i64::MAX); // panics if `new` is shorter changes.push(after - before); } changes}
pub fn total_for_customer(customers: &[&str], totals: &[u64], name: &str) -> u64 { let mut sum = 0; for i in 0..customers.len() { if customers[i] == name { sum += totals[i]; } } sum}Why AI agents produce it
Section titled “Why AI agents produce it”- It is the C and Java loop.
for (int i = 0; i < n; i++)is in a large part of all code ever written, so the translationfor i in 0..v.len()comes first. - Two lists “in step”. When the code needs items from two lists at the same position, an index looks like the obvious way. Rust’s
zipis less known. - It avoids borrow errors. An agent that gets an error about borrowing
vinside a loop overvsometimes switches to indexes, which hides the problem instead of fixing the design.
Why it is a problem
Section titled “Why it is a problem”- It can panic.
new[i]panics ifnewis shorter thanold. The testworse_panics_on_a_shorter_listshows it.zipstops at the shorter list, so the “better” code cannot go out of range. See S24: Indexing (v[i],s[a..b]) on untrusted input. - It hides the intent.
for i in 0..customers.len()says “count from 0”.customers.iter().zip(totals).filter(...)says “pair each customer with its total, and keep the matching ones”. - It can be slower. Each
v[i]has a bounds check. The compiler removes many of them, but not always, and not when two slices are indexed. - Parallel lists are a design smell too. Two
Vecs that must stay in step are often oneVecof structs.
The better version
Section titled “The better version”WorseIndex loops over one list and over two lists. A shorter second list panics.
pub fn total(prices: &[u64]) -> u64 {let mut sum = 0;for i in 0..prices.len() {sum += prices[i];}sum}/// The price change of each product between two price lists.pub fn price_changes(old: &[u64], new: &[u64]) -> Vec<i64> {let mut changes = Vec::new();for i in 0..old.len() {let before = i64::try_from(old[i]).unwrap_or(i64::MAX);let after = i64::try_from(new[i]).unwrap_or(i64::MAX); // panics if `new` is shorterchanges.push(after - before);}changes}pub fn total_for_customer(customers: &[&str], totals: &[u64], name: &str) -> u64 {let mut sum = 0;for i in 0..customers.len() {if customers[i] == name {sum += totals[i];}}sum}Better
iter().sum(), andzipfor two lists. No index, no panic.pub fn total(prices: &[u64]) -> u64 {prices.iter().sum()}pub fn price_changes(old: &[u64], new: &[u64]) -> Vec<i64> {old.iter().zip(new) // stops at the shorter list: no index, no panic.map(|(&before, &after)| {i64::try_from(after).unwrap_or(i64::MAX) - i64::try_from(before).unwrap_or(i64::MAX)}).collect()}pub fn total_for_customer(customers: &[&str], totals: &[u64], name: &str) -> u64 {customers.iter().zip(totals).filter(|(c, _)| **c == name).map(|(_, t)| t).sum()}
| Index loop | Iterator |
|---|---|
for i in 0..v.len() { v[i] } |
for x in &v or v.iter() |
v[i] and w[i] |
v.iter().zip(&w) |
i and v[i] |
v.iter().enumerate() |
v[i - 1] and v[i] |
v.windows(2) |
for i in (0..v.len()).rev() |
v.iter().rev() |
When it is acceptable
Section titled “When it is acceptable”When the algorithm is about positions, indexes can be the clearest code:
/// Reverse in place: the algorithm is about positions, so indexes are the clearest code.pub fn reverse_in_place(items: &mut [u64]) { let n = items.len(); for i in 0..n / 2 { items.swap(i, n - 1 - i); }}- Swaps and in-place algorithms that touch two positions at once.
- Matrix and grid code, where
grid[row][col]is the natural notation. - When the index itself is the output, though
enumerateorpositionoften still works.
How to detect it
Section titled “How to detect it”- Clippy
needless_range_loop(style group, on by default) reports a range loop whose index is only used to index one slice: the first function in the example. It does not report the loops over two lists. Read those yourself. - Search for
in 0..followed by.len(). - In review, look for
[i]in loop bodies.
What to tell your agent
Section titled “What to tell your agent”Replace the index loops with iterators. Use for x in &items or items.iter()for one list, zip for two lists that go together, enumerate where the indexis needed, and windows(2) for neighbors. Keep an index only where thealgorithm needs positions, such as a swap. If two Vecs must stay in step,consider one Vec of a struct instead. Run cargo clippy and the tests.Related: S24: Indexing (v[i], s[a..b]) on untrusted input, S11: collect() then iterate again, and Iterators vs streams.