Iterators Over Loops
Rust's iterator system is one of the most expressive parts of the language. Once you learn to read and write iterator chains fluently, you will find that most explicit for loops can be replaced with something shorter, cleaner, and just as fast. Iterators are lazy and compile down to the same machine code as hand-written loops.
This tutorial covers when to reach for iterators instead of loops, the patterns you will use most, and the traps to avoid.
Scenario 1 - Transforming a collection
The Loop Version
let numbers = vec![1, 2, 3, 4, 5];
let mut doubled = Vec::new();
for n in &numbers {
doubled.push(n * 2);
}
The Iterator Version
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
.map() describes what to do with each element. .collect() drives the iteration and builds the result. The type annotation on doubled tells collect() what to produce. Without it, the compiler cannot infer which collection you want.
Tip: If you find yourself writing
let mut result = Vec::new()followed by aforloop with only.push()calls, that is almost always a.map().collect()in disguise.
Scenario 2 - Filtering elements
The Loop Version
let scores = vec![45, 82, 91, 34, 67];
let mut passing = Vec::new();
for &s in &scores {
if s >= 60 {
passing.push(s);
}
}
The Iterator Version
let scores = vec![45, 82, 91, 34, 67];
let passing: Vec<i32> = scores.iter().filter(|&&s| s >= 60).copied().collect();
Or, more clearly when the source is owned:
let passing: Vec<i32> = scores.into_iter().filter(|&s| s >= 60).collect();
.into_iter() vs .iter()
| Method | Produces | Ownership |
|---|---|---|
.iter() | &T | Borrows, source still usable |
.iter_mut() | &mut T | Mutably borrows each element |
.into_iter() | T | Consumes the collection |
Use .into_iter() when you are done with the source and want to avoid copying. Use .iter() when you need to keep the original.
Scenario 3 - Chaining map and filter together
Suppose you want to find all passing scores and double them for a leaderboard display.
let leaderboard: Vec<i32> = scores
.into_iter()
.filter(|&s| s >= 60)
.map(|s| s * 2)
.collect();
Each step in the chain is lazy: nothing runs until .collect() is called. The entire chain processes one element at a time, never building an intermediate Vec. Iterator chains are not slower than loops. They produce the same sequential code after compilation.
Tip: Order matters. Put
.filter()before.map()when the filter is cheap and the map is expensive. This skips the map work on elements that would be discarded.
Scenario 4 - Reducing to a single value
Sum, count, and max
let total: i32 = numbers.iter().sum();
let count = numbers.iter().filter(|&&n| n > 2).count();
let largest = numbers.iter().max(); // returns Option<&i32>
Custom fold
When the built-in reducers do not fit, .fold() lets you accumulate any value:
// Build a comma-separated string
let csv = numbers.iter().fold(String::new(), |mut acc, n| {
if !acc.is_empty() { acc.push(','); }
acc.push_str(&n.to_string());
acc
});
// "1,2,3,4,5"
.fold(initial, closure) is the general form. Many standard methods (.sum(), .product(), .count()) are implemented in terms of it.
Tip: For building strings from iterators, prefer
.join()viaitertoolsor collect into aVec<String>first.foldfor string building allocates on every step. The example above is instructive, not the recommended approach for production string joining.
Scenario 5 - Flattening nested structures
You have a Vec<Vec<T>> or a Vec<Option<T>> and want a flat collection.
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
// [1, 2, 3, 4, 5]
let maybe_values: Vec<Option<i32>> = vec![Some(1), None, Some(3), None, Some(5)];
let values: Vec<i32> = maybe_values.into_iter().flatten().collect();
// [1, 3, 5]
.flatten() works on any iterator of iterables, including Option and Result (which both implement IntoIterator).
.flat_map(): map then flatten in one step:
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter().flat_map(|s| s.split_whitespace()).collect();
// ["hello", "world", "foo", "bar"]
Scenario 6 - Working with indices
Sometimes you need the index alongside the value. Use .enumerate():
let items = vec!["a", "b", "c"];
for (i, item) in items.iter().enumerate() {
println!("{}: {}", i, item);
}
In an iterator chain:
let indexed: Vec<(usize, &&str)> = items.iter().enumerate().filter(|(i, _)| i % 2 == 0).collect();
Tip: If you reach for a range-based
for i in 0..vec.len()loop just to access both index and value,.enumerate()is cleaner and avoids the indexing overhead.
Scenario 7 - Short-circuiting: find and any
When you want to stop as soon as a condition is met, short-circuit methods avoid processing the rest of the collection.
let has_negative = numbers.iter().any(|&n| n < 0); // stops at first negative
let first_even = numbers.iter().find(|&&n| n % 2 == 0); // returns Option<&i32>
let first_even_index = numbers.iter().position(|&n| n % 2 == 0); // returns Option<usize>
These are more expressive than an equivalent loop with a break, and the intent is immediately clear to the reader.
When to Keep the Loop
Iterator chains are not always the right choice.
Keep a loop when:
- The body has side effects that depend on external mutable state that cannot easily be passed through a closure.
- The logic involves
breakwith a value that changes meaning partway through..fold()may obscure this. - The chain would require more than 3–4 methods to read clearly.
- You need early returns based on errors: a
forloop with?is usually cleaner than.try_fold().
// This is clearer as a loop
fn find_first_valid(items: &[Item]) -> Option<&Item> {
for item in items {
if item.is_ready() && item.has_required_fields() && !item.is_expired() {
return Some(item);
}
}
None
}
Forcing this into a .find() chain is possible but does not make it easier to read.
Key Takeaways
- Reach for
.map().collect()any time you seeVec::new()+ loop +.push(). - Use
.into_iter()when consuming the source;.iter()when borrowing. - Iterator chains are lazy: nothing runs until a consuming method (
.collect(),.sum(),.for_each(), etc.) is called. - Order your chain to filter before mapping when the filter is cheap and the map is not.
.enumerate()replaces index-basedforloops..any(),.find(),.position()replace loops withbreak.- Keep explicit loops when the chain would become harder to read than the loop it replaces.