How to Use Iterators in Rust

The Complete Guide

A walk-through of Rust's iterator system: lazy adapters like map/filter, the iter/iter_mut/into_iter distinction, common consumers, and why iterator chains are often as fast as hand-written loops.

You have a vector of numbers, and you want to double each one and add them up. In Python you'd write sum(x*2 for x in numbers) and move on with your day. In Rust, the equivalent is numbers.iter().map(|x| x*2).sum(). It looks similar. It is similar. But underneath, Rust's iterators do something genuinely surprising: they're often faster than the hand-written loop you'd reach for instead.

That sounds backwards. Hand-rolled loops should be the speed champion, right? Iterators sound like extra layers of objects and method calls. The compiler does some of its best work here, though, and once you understand what's happening, iterators become the default tool for almost every "do something to each element" task you'll meet.

The mental model: a lazy pipeline

An iterator is a small object that knows how to produce one value at a time, on demand. You ask it for the next value, it gives you one, and eventually it tells you it's out. That's the whole protocol.

The interesting part is what you build on top. Methods like .map(), .filter(), and .take() don't actually do work when you call them. They just wrap the iterator in another iterator that, when asked for a value, will lazily fetch from the inner one and transform it. So this:

let v = vec![1, 2, 3, 4, 5];

// Each chained call returns a new iterator. Nothing has run yet.
let pipeline = v.iter()
    .map(|x| x * 2)      // doubles each value
    .filter(|x| *x > 4); // keeps only those greater than 4

doesn't doubles or filter anything. pipeline is a value of some elaborate type like Filter<Map<Iter<i32>, ...>, ...>. Until something asks it to actually produce values, it sits there idle.

To make work happen, you need a consumer. Calling .collect(), .sum(), .count(), for x in ..., .next() directly, or any of a dozen others kicks the pipeline into motion. That's the moment the values flow.

// Now we consume; this triggers the actual computation.
let result: Vec<i32> = pipeline.collect();
println!("{:?}", result); // [6, 8, 10]

Once you've internalized this lazy/eager split, a lot of Rust idioms suddenly make sense. The compiler can fuse the whole chain into a single tight loop because it sees the pipeline as one expression. There's no intermediate Vec getting allocated between .map and .filter. That's where the surprising speed comes from.

Three ways to start an iterator from a collection

This trips up almost every learner. Vectors and slices give you three different starting methods, and they hand out three different things:

let v = vec![10, 20, 30];

// .iter() yields &T (shared references). Original v is still usable after.
for x in v.iter() {
    println!("{}", x);   // x is &i32
}

// .iter_mut() yields &mut T. Lets you modify in place.
let mut v2 = vec![1, 2, 3];
for x in v2.iter_mut() {
    *x *= 10;            // dereference to write through the &mut
}

// .into_iter() yields T (owned). It consumes the collection.
let v3 = vec![100, 200, 300];
for x in v3.into_iter() {
    println!("{}", x);   // x is i32, by value
}
// v3 is gone here; you can't use it again.

Pick the one that matches what you need. Most of the time you want .iter(): you read each element, the collection lives on, no surprises. You only reach for .iter_mut() when you actually want to mutate; you only reach for .into_iter() when you're done with the collection and want to take ownership of its contents (often because you're transforming it into something else).

There's a shortcut: writing for x in &v is the same as for x in v.iter(). Writing for x in &mut v is iter_mut(). Writing for x in v consumes (into_iter()). The Rust Book uses these everywhere; just remember they're the same idea wearing a different syntactic hat.

The adapters you'll actually use

.map(f) runs f on each element and yields the result. Use it for "transform every value the same way."

.filter(p) keeps only elements where the predicate p returns true. Note: the predicate gets a reference, so for an iterator over &i32 it gets &&i32, which is why you'll often see *x > 4 or |&x| x > 4. Both work; the second pattern destructures the outer reference for you.

.take(n) stops after n elements. Useful for limiting infinite iterators (yes, those exist; (0..).take(10) gives you 0 through 9).

.skip(n) throws away the first n and yields the rest.

.enumerate() pairs each element with its index, yielding (usize, T). Saves you from the let mut i = 0; ... i += 1; dance.

.zip(other) walks two iterators in lockstep, yielding pairs. Stops when either runs out.

.chain(other) glues two iterators together end-to-end.

A small example that mixes several:

let words = ["alpha", "beta", "gamma", "delta"];

// Index, uppercase, keep only those starting with vowels after upcasing
let result: Vec<(usize, String)> = words
    .iter()
    .enumerate()                                  // (idx, &str)
    .map(|(i, w)| (i, w.to_uppercase()))          // own the String now
    .filter(|(_, w)| {
        // first char is one of A, E, I, O, U
        matches!(w.chars().next(), Some('A' | 'E' | 'I' | 'O' | 'U'))
    })
    .collect();

println!("{:?}", result);
// [(0, "ALPHA")]

Read that bottom-up if it feels dense: start with the source, each line is a small step, and .collect() at the end gathers everything into a Vec.

Consumers do the work

The most common consumers:

// .collect::<Vec<_>>() pulls every value into a new Vec.
// The turbofish (or a type annotation) tells collect what shape to build.
let evens: Vec<i32> = (1..10).filter(|x| x % 2 == 0).collect();

// .sum() and .product() reduce a numeric iterator to a single value.
// Type annotation needed so the compiler picks the right numeric type.
let total: i32 = (1..=100).sum();          // 5050

// .count() walks the whole thing and returns how many items came out.
let n = "hello world".chars().filter(|c| !c.is_whitespace()).count();

// .find(p) returns Option<T>: the first matching item, or None.
let first_big = (1..).find(|x| x * x > 1000);   // Some(32)

// .fold(init, f) is the general-purpose reducer.
let concatenated = ["a", "b", "c"].iter()
    .fold(String::new(), |acc, s| acc + s);     // "abc"

.fold is the swiss army knife: any reduction can be expressed as a fold. The other consumers are specialized shortcuts. Reach for them when they fit; reach for .fold when nothing else does.

A more realistic example

Suppose you've got a list of customer orders and you want the total revenue from orders that shipped, grouped per customer. The iterator way:

struct Order {
    customer_id: u32,
    amount_cents: u64,
    shipped: bool,
}

// Returns a map customer_id -> total cents from shipped orders only.
fn revenue_by_customer(orders: &[Order]) -> std::collections::HashMap<u32, u64> {
    use std::collections::HashMap;

    orders
        .iter()
        .filter(|o| o.shipped)                  // skip un-shipped orders
        .fold(HashMap::new(), |mut acc, o| {
            // entry() gets the slot for this customer, inserting 0 if absent
            *acc.entry(o.customer_id).or_insert(0) += o.amount_cents;
            acc                                 // pass the map to the next fold step
        })
}

No mutable index counter, no manual loop. The intent is right there in the chain: take orders, keep shipped ones, accumulate per-customer totals. Add a println! and you've got a working report.

Common pitfalls

Forgetting to consume the iterator. If you write a chain and don't call a consumer, the compiler will warn you that nothing happens. Adapters are lazy. No collect, no for-loop, no sum: no work.

warning: unused `Map` that must be used
  --> src/main.rs:5:5
   |
5  |     v.iter().map(|x| x * 2);
   |     ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: iterators are lazy and do nothing unless consumed

Calling .iter() when you wanted ownership. If you write let s: Vec<String> = v.iter().map(|x| x.to_string()).collect(); on a Vec<i32>, you're fine, because to_string() produces an owned String from &i32. But if you have a Vec<String> and write v.iter().collect::<Vec<String>>(), the compiler will reject it because .iter() produces &String, not String. Use .into_iter() (and accept that v is consumed), or .iter().cloned() to clone each item.

The && confusion in filter. As mentioned: .iter() on Vec<i32> gives you &i32. .filter passes a reference to the predicate, so the closure sees &&i32. Either write |x| **x > 4, or destructure: |&&x| x > 4. The second is idiomatic.

Building a Vec just to iterate it once. If you only need to walk values once, .iter().map(...).for_each(...) is leaner than collecting into a Vec and iterating that. Allocate only when you need to keep the result around.

Where to go next