The Rust Collections Guide
Iteration and Collecting
Last Updated: 2026-04-05
Why iteration and collecting matter
A large share of everyday Rust collection work follows the same broad shape. Start with some values. Iterate over them. Transform, filter, or flatten them. Then collect the result into a new collection. This pattern appears so often that understanding it changes how the rest of the standard library feels.
The key idea is that iterators separate sequence processing from concrete storage. You can map over values before deciding whether the result should become a Vec<T>, a HashSet<T>, a String, or something else. This makes code flexible and composable.
In practice, iteration is often the processing layer, and collection is the final storage decision.
What an iterator gives you
An iterator is a value that can yield a sequence of items one at a time. Many Rust collections can produce iterators, and many standard-library methods transform one iterator into another.
This matters because you can write logic in terms of sequence operations rather than immediate indexing or manual loops.
fn main() {
let values = vec![1, 2, 3, 4];
for value in values.iter() {
println!("{value}");
}
}That example is simple, but the same iterator idea extends into mapping, filtering, flattening, collecting, and fallible processing.
From collections to iterators
Most collection types can be turned into iterators in several ways. The exact method determines whether you borrow elements immutably, borrow them mutably, or move them out.
fn main() {
let mut values = vec![10, 20, 30];
for value in values.iter() {
println!("shared: {value}");
}
for value in values.iter_mut() {
*value += 1;
}
println!("after iter_mut = {:?}", values);
for value in values.into_iter() {
println!("moved: {value}");
}
}This distinction is important because iterator style is tied directly to ownership. Shared iteration reads. Mutable iteration edits in place. Consuming iteration transfers ownership of items.
The central role of `collect`
collect is one of the most important methods in Rust's iterator model. It takes the items yielded by an iterator and gathers them into a concrete result type. That result might be a vector, a string, a set, a map, or something else that knows how to be built from an iterator.
fn main() {
let doubled: Vec<i32> = (1..=5).map(|n| n * 2).collect();
println!("doubled = {:?}", doubled);
}This is a common Rust pattern: leave the data in iterator form while transforming it, then use collect once you are ready to materialize the result.
One reason collect can feel unusual at first is that the target type is not always obvious from the method call alone. The compiler often needs help through an explicit type annotation.
Type inference and turbofish with `collect`
Because collect can build many different collection types, you sometimes need to tell Rust what you want. This can be done through a variable type annotation or by using the so-called turbofish syntax.
fn main() {
let a: Vec<i32> = (1..=3).collect();
let b = (4..=6).collect::<Vec<i32>>();
println!("a = {:?}", a);
println!("b = {:?}", b);
}Both forms are common. The main lesson is that collect does not mean "make a vector" by itself. It means "build some collection from this iterator," and the target type must be known.
Collecting into different collection types
A major strength of iterator pipelines is that the same processing steps can feed different destination collections.
use std::collections::{BTreeSet, HashSet};
fn main() {
let values = ["pear", "apple", "pear", "banana"];
let as_vec: Vec<_> = values.into_iter().collect();
let as_hash_set: HashSet<_> = values.into_iter().collect();
let as_btree_set: BTreeSet<_> = values.into_iter().collect();
println!("vec = {:?}", as_vec);
println!("hash set = {:?}", as_hash_set);
println!("btree set = {:?}", as_btree_set);
}This is a powerful idea. The iterator describes the sequence of items. The destination collection determines how those items are stored and interpreted.
Transforming with `map` before collecting
map transforms each yielded item into a new one. This is one of the most common iterator operations and often appears directly before collect.
fn main() {
let words = ["rust", "collections", "guide"];
let lengths: Vec<usize> = words.iter().map(|word| word.len()).collect();
println!("lengths = {:?}", lengths);
}The conceptual flow is straightforward. Start with words, transform each word into its length, then collect the lengths into a vector.
Filtering with `filter` and `filter_map`
filter keeps only items that satisfy a predicate. filter_map combines filtering and mapping by letting you return Some(new_value) for items to keep and None for items to discard.
fn main() {
let values = [1, 2, 3, 4, 5, 6];
let evens: Vec<_> = values.into_iter().filter(|n| n % 2 == 0).collect();
println!("evens = {:?}", evens);
let raw = ["10", "x", "25", "nope", "7"];
let parsed: Vec<i32> = raw.into_iter().filter_map(|s| s.parse().ok()).collect();
println!("parsed = {:?}", parsed);
}filter_map is especially useful when converting partially valid input into a clean result sequence.
Flattening nested structure
Rust iterators also support flattening nested structures. This is useful when each item is itself a sequence, an Option, or another iterator-like thing.
fn main() {
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
println!("flat = {:?}", flat);
}This removes one level of nesting. Instead of collecting vectors inside vectors, you collect the yielded inner elements into one flat destination.
Flattening `Option`-like and `Result`-like layers
Flattening is also useful when iterating over values that may or may not be present. One common case is a sequence of Option<T> values.
fn main() {
let items = vec![Some(1), None, Some(3), Some(5), None];
let values: Vec<i32> = items.into_iter().flatten().collect();
println!("values = {:?}", values);
}This works because flattening a sequence of Option<T> values effectively keeps the present values and discards the absent ones. It often reads more directly than manual branching.
Using `flat_map` for one-to-many expansion
flat_map combines mapping and flattening. It is useful when each input item expands into zero, one, or many output items.
fn main() {
let lines = ["a b", "c d e", "f"];
let words: Vec<&str> = lines.into_iter().flat_map(|line| line.split_whitespace()).collect();
println!("words = {:?}", words);
}Here each input line becomes an iterator over its words, and flat_map joins those per-line iterators into one continuous sequence.
Building collections incrementally with `extend`
While collect builds a new collection from an iterator, extend adds iterator items into an existing collection. This is useful when the collection already exists or when accumulation happens in stages.
fn main() {
let mut values = vec![1, 2, 3];
values.extend(4..=6);
println!("values = {:?}", values);
}This is conceptually similar to repeated push, but it works with whole iterator sources. Many collection types support extend, not just vectors.
Extending maps and sets
extend is especially useful because it applies across collection kinds. You are not limited to vectors.
use std::collections::{HashMap, HashSet};
fn main() {
let mut set: HashSet<i32> = [1, 2].into_iter().collect();
set.extend([2, 3, 4]);
println!("set = {:?}", set);
let mut map = HashMap::new();
map.extend([("a", 1), ("b", 2)]);
println!("map = {:?}", map);
}This reinforces the idea that iterator-based building is a general pattern in Rust, not just a vector convenience.
Collecting strings from characters and fragments
Strings are also common collection targets. You can collect characters into a String, or join text fragments through iterator pipelines.
fn main() {
let chars = ['r', 'u', 's', 't'];
let word: String = chars.into_iter().collect();
println!("word = {}", word);
let uppercase_letters: String = "hello"
.chars()
.filter(|ch| ch.is_ascii_alphabetic())
.map(|ch| ch.to_ascii_uppercase())
.collect();
println!("uppercase_letters = {}", uppercase_letters);
}This is another example of the same model: process with iterators, then choose a concrete destination.
Fallible collection with `Result`
One of the most elegant Rust patterns is collecting an iterator of Result<T, E> into a single Result<Vec<T>, E>. If all items succeed, you get a collected vector. If any item fails, collection stops and the error is returned.
fn main() {
let raw = ["10", "20", "oops", "40"];
let parsed: Result<Vec<i32>, _> = raw.into_iter().map(|s| s.parse::<i32>()).collect();
println!("parsed = {:?}", parsed);
}This pattern is extremely useful because it lets iterator pipelines remain expressive even when parsing or validation can fail.
Fallible collection with `Option`
A similar pattern works with Option<T>. Collecting an iterator of Option<T> values gives Option<Vec<T>>. If every item is Some, the final result is Some(Vec<T>). If any item is None, the whole result becomes None.
fn main() {
let values = [Some(1), Some(2), Some(3)];
let all_present: Option<Vec<i32>> = values.into_iter().collect();
println!("all_present = {:?}", all_present);
let mixed = [Some(1), None, Some(3)];
let missing: Option<Vec<i32>> = mixed.into_iter().collect();
println!("missing = {:?}", missing);
}This is a clean way to express all-or-nothing collection logic.
Choosing between `filter_map` and fallible collection
These two patterns solve different problems. filter_map is useful when invalid or absent items should simply be skipped. Fallible collection is useful when any invalid or absent item should cause the whole operation to fail.
fn main() {
let raw = ["1", "x", "2"];
let skipped: Vec<i32> = raw.into_iter().filter_map(|s| s.parse().ok()).collect();
println!("skipped invalid = {:?}", skipped);
let strict: Result<Vec<i32>, _> = raw.into_iter().map(|s| s.parse::<i32>()).collect();
println!("strict parse = {:?}", strict);
}The choice depends on semantics, not style. Do you want best-effort extraction, or do you want validation to be strict?
Iterator-friendly function design
One of the most useful design habits is to make functions iterator-friendly. Often this means accepting slices or iterators as input rather than requiring a concrete collection type unnecessarily.
fn squared_evens(input: &[i32]) -> Vec<i32> {
input.iter().copied().filter(|n| n % 2 == 0).map(|n| n * n).collect()
}
fn main() {
let array = [1, 2, 3, 4, 5, 6];
let vector = vec![7, 8, 9, 10];
println!("{:?}", squared_evens(&array));
println!("{:?}", squared_evens(&vector));
}This is often better than taking &Vec<i32>, because it works with more caller-owned data without extra conversion.
Returning iterators versus returning collected results
Sometimes a function should return a concrete collected value such as Vec<T>. Other times it is better to return an iterator and let the caller decide how to consume it. The right choice depends on ownership, simplicity, and how much flexibility you want to expose.
fn positive_values<'a>(input: &'a [i32]) -> impl Iterator<Item = i32> + 'a {
input.iter().copied().filter(|n| *n > 0)
}
fn main() {
let values = [-2, 4, 0, 7, -1, 9];
let as_vec: Vec<i32> = positive_values(&values).collect();
println!("as_vec = {:?}", as_vec);
let sum: i32 = positive_values(&values).sum();
println!("sum = {}", sum);
}Returning an iterator can keep the function more reusable because callers can collect, sum, count, or continue transforming as needed.
When collecting is unnecessary
A common beginner pattern is to collect too early. Sometimes the program only needs a sum, count, predicate check, or another terminal result. In those cases, keeping the data as an iterator may be simpler and cheaper.
fn main() {
let values = [1, 2, 3, 4, 5, 6];
let count = values.iter().filter(|n| **n % 2 == 0).count();
let total: i32 = values.iter().copied().filter(|n| n % 2 == 0).sum();
println!("even count = {}", count);
println!("even total = {}", total);
}The lesson is not "always avoid collecting." It is "collect when you need a collection, not automatically."
A small decision guide
Use collect when an iterator pipeline has produced the right sequence of items and you now need a concrete owned result.
Use extend when you already have a collection and want to add more items from an iterator source.
Use filter_map when invalid or absent items should be skipped.
Use fallible collection into Result or Option when the whole operation should fail if any item fails.
Delay collecting when a terminal iterator method like sum, count, any, or all already gives the result you actually need.
A small sandbox project
A tiny Cargo project is enough to explore the main collecting patterns.
[package]
name = "iteration-collecting-guide"
version = "0.1.0"
edition = "2024"Create and run it like this.
cargo new iteration-collecting-guide
cd iteration-collecting-guide
cargo runA minimal src/main.rs could look like this.
use std::collections::{HashMap, HashSet};
fn parse_numbers(input: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
input.iter().map(|s| s.parse::<i32>()).collect()
}
fn main() {
let words = ["pear", "apple", "pear", "banana"];
let unique: HashSet<_> = words.into_iter().collect();
println!("unique = {:?}", unique);
let lengths: Vec<usize> = words.into_iter().map(|w| w.len()).collect();
println!("lengths = {:?}", lengths);
let parsed = parse_numbers(&["10", "20", "30"]);
println!("parsed = {:?}", parsed);
let grouped: HashMap<usize, Vec<&str>> = words.into_iter().fold(HashMap::new(), |mut acc, word| {
acc.entry(word.len()).or_default().push(word);
acc
});
println!("grouped = {:?}", grouped);
}This one small program shows several core patterns together: collecting into a set, transforming into a vector, strict fallible parsing into Result<Vec<T>, E>, and building a grouped collection from iteration.
