195. Chain Iterator Adapters — Don't collect() Between Every Step

Every collect::<Vec<_>>() in the middle of a pipeline is a heap allocation and a full pass over your data. Adapters like map and filter are lazy and fuse together — chain them and the whole transformation runs in one pass with zero temporary Vecs.

A Vec between every step

It’s tempting to do one transformation at a time, binding each result to a variable. Every collect() allocates a throwaway Vec and walks the entire sequence before the next step even starts:

1
2
3
4
5
6
7
let nums = [1, 2, 3, 4, 5, 6];

let doubled: Vec<i32> = nums.iter().map(|&n| n * 2).collect();
let big: Vec<i32> = doubled.into_iter().filter(|&n| n % 4 == 0).collect();
let sum: i32 = big.iter().sum();

assert_eq!(sum, 24);

Two intermediate Vecs, two extra allocations, three separate passes — all to compute a single number.

One chain, one pass, no temporaries

The adapters compose directly. Nothing is materialized until the final consumer (sum) pulls values through, so there are no intermediate collections at all:

1
2
3
4
5
6
7
8
9
let nums = [1, 2, 3, 4, 5, 6];

let sum: i32 = nums
    .iter()
    .map(|&n| n * 2)        // 2, 4, 6, 8, 10, 12
    .filter(|&n| n % 4 == 0) // 4, 8, 12
    .sum();                  // 24

assert_eq!(sum, 24);

Each element flows through map then filter then into the sum, one at a time. No buffer is ever allocated.

Laziness means short-circuiting works

Because nothing runs until pulled, a chain only does the work it needs. Add a take(2) and the pipeline stops after producing two results — the elements past that point are never touched:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

let visited = Cell::new(0);
let nums = [1, 2, 3, 4, 5, 6, 7, 8];

let first_two: Vec<i32> = nums
    .iter()
    .inspect(|_| visited.set(visited.get() + 1))
    .map(|&n| n * 10)
    .filter(|&n| n > 20)
    .take(2)
    .collect();

assert_eq!(first_two, [30, 40]);
assert_eq!(visited.get(), 4); // stopped early — never looked at 5..8

The intermediate-collect version can’t do this: collect() always drains the whole iterator, so it would have visited all eight elements before take ever saw one.

When you genuinely do need a Vec

The point isn’t “never collect” — it’s “don’t collect between steps.” Collect once, at the end, when you actually need an owned, reusable collection:

1
2
3
4
5
6
7
8
9
let words = ["fast", "lazy", "fused", "iter"];

let shouted: Vec<String> = words
    .iter()
    .filter(|w| w.len() == 4)
    .map(|w| w.to_uppercase())
    .collect();

assert_eq!(shouted, ["FAST", "LAZY", "ITER"]);

One collect, at the end, when the Vec is the actual result. Everything before it stays lazy and allocation-free.

← Previous 194. Reuse One Buffer with .clear() — Allocate Once, Loop Many Times