Skip to content

S10: Index-based loops instead of iterators

S10Severity: Low

for i in 0..v.len() with v[i], where an iterator is clearer and has no bounds checks.

Clippy: needless_range_loop

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
}
  • 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 translation for 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 zip is less known.
  • It avoids borrow errors. An agent that gets an error about borrowing v inside a loop over v sometimes switches to indexes, which hides the problem instead of fixing the design.
  • It can panic. new[i] panics if new is shorter than old. The test worse_panics_on_a_shorter_list shows it. zip stops 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 one Vec of structs.
  1. 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 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
    }
  2. Betteriter().sum(), and zip for 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 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 enumerate or position often still works.
  • 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.
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 index
is needed, and windows(2) for neighbors. Keep an index only where the
algorithm 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.