Can you write this from memory?
Create an empty Vec of i32 values named `numbers`.
Rust's standard library provides powerful collections: Vec for dynamic arrays, HashMap for key-value storage, and more. But the real power comes from iterators. They're lazy, composable transformations that chain together elegantly.
The iterator ecosystem can be confusing at first. When do you use iter() vs into_iter()? Why does collect() need a type annotation? How do you chain map, filter, and fold without getting lost?
This page focuses on collection and iterator patterns until they become natural. You'll practice creating collections, transforming them with iterators, and collecting results.
collect() can build many collection types (Vec, HashSet, String, HashMap, Result<Vec<_>, _>), so the compiler often cannot infer which one you want:
let v = vec![1, 2, 3];
// WRONG: No type hint
// let result = v.iter().map(|x| x * 2).collect();
// error[E0282]: type annotations needed
// RIGHT: Annotate the variable
let result: Vec<i32> = v.iter().map(|x| x * 2).collect();
// RIGHT: Use turbofish
let result = v.iter().map(|x| x * 2).collect::<Vec<_>>();
Turbofish is useful mid-chain. Type annotation is clearer for standalone bindings. Common collect targets:
// Vec
let v = iter.collect::<Vec<_>>();
// HashMap from tuples
let m = pairs.collect::<HashMap<_, _>>();
// Result — collect stops at first Err
let results = items.map(|x| x.parse::<i32>()).collect::<Result<Vec<_>, _>>();
// String — join chars or &strs
let s = chars.collect::<String>();
.iter() yields references. If you collect into a type that expects owned values, the compiler rejects it:
let names = vec![String::from("alice"), String::from("bob")];
// OK: map produces new owned Strings (to_uppercase allocates)
let upper: Vec<String> = names.iter().map(|s| s.to_uppercase()).collect();
// WRONG: collecting &String directly into Vec<String>
// let cloned: Vec<String> = names.iter().collect();
// error[E0277]: a value of type Vec<String> cannot be built from &String
// RIGHT: .cloned() converts &T to T (calls Clone)
let cloned: Vec<String> = names.iter().cloned().collect();
// RIGHT: .into_iter() yields owned values directly
let owned: Vec<String> = names.into_iter().collect();
// names is consumed and can no longer be used
Both convert &T to T, but they work differently:
| Adapter | Requires | Does |
|---|---|---|
.copied() | T: Copy | Bitwise copy (no heap allocation) |
.cloned() | T: Clone | Calls .clone() (may allocate) |
// copied() — for Copy types (i32, f64, bool, char, etc.)
let nums = vec![1, 2, 3];
let owned: Vec<i32> = nums.iter().copied().collect();
// cloned() — for Clone types that may allocate (String, Vec, etc.)
let names = vec![String::from("alice"), String::from("bob")];
let owned: Vec<String> = names.iter().cloned().collect();
Prefer .copied() when the type is Copy — it documents that the operation is cheap.
The first question to answer when iterating:
| Method | Yields | Collection after? |
|---|---|---|
.iter() | &T | Still usable |
.iter_mut() | &mut T | Still usable (mutated) |
.into_iter() | T | Consumed, gone |
let mut v = vec![1, 2, 3];
// Borrow: read only, v survives
for x in v.iter() { println!("{x}"); }
// Mutable borrow: modify in place, v survives
for x in v.iter_mut() { *x += 10; }
// Consume: take ownership, v is gone
for x in v.into_iter() { println!("{x}"); }
// v cannot be used after this point
Start with .iter(). Switch to .into_iter() only when you need ownership or won't use the collection again. Use .iter_mut() when modifying elements in place. Understanding these choices requires a solid grasp of ownership and borrowing -- the same rules that govern &T vs &mut T vs T apply to iterator output.
This appears when you try to take ownership through a borrow:
let names = vec![String::from("alice"), String::from("bob")];
// WRONG: iter() yields &String, but you try to move
// for name in names.iter() {
// let owned: String = *name; // error[E0507]
// }
// RIGHT: use into_iter() to take ownership
for name in names.into_iter() {
let owned: String = name; // fine — already owned
}
// RIGHT: clone if you need the collection afterward
let names = vec![String::from("alice"), String::from("bob")];
for name in names.iter() {
let owned: String = name.clone();
}
Iterator adapters are lazy — nothing happens until you consume the iterator with .collect(), .sum(), .for_each(), or a for loop. Each adapter takes a closure, making closures and iterators inseparable in idiomatic Rust:
let v = vec![1, 2, 3, 4, 5];
// filter + map + collect
let doubled_evens: Vec<i32> = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
// [4, 8]
// fold for aggregation
let sum: i32 = v.iter().fold(0, |acc, &x| acc + x);
// filter_map combines filter and map in one step
let parsed: Vec<i32> = ["1", "two", "3"].iter()
.filter_map(|s| s.parse().ok())
.collect();
// [1, 3]
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("blue", 10);
scores.insert("red", 25);
// Access with get (returns Option<&V>)
let blue_score = scores.get("blue"); // Some(&10)
// Entry API for conditional insert
scores.entry("green").or_insert(50);
// Build from iterator of tuples
let teams = vec![("blue", 10), ("red", 25)];
let scores: HashMap<&str, i32> = teams.into_iter().collect();
Entry API Cookbook
The Entry API avoids double lookups. Three patterns you'll use constantly:
use std::collections::HashMap;
let mut counts: HashMap<&str, i32> = HashMap::new();
let words = vec!["hello", "world", "hello"];
// Counting: increment or initialize to 0
for word in &words {
*counts.entry(word).or_insert(0) += 1;
}
// Lazy default: only compute if key is missing
let mut cache: HashMap<String, Vec<u8>> = HashMap::new();
let data = cache.entry("key".into())
.or_insert_with(|| expensive_computation());
// Update-or-insert: modify existing, insert if absent
let mut scores: HashMap<&str, i32> = HashMap::new();
scores.entry("player")
.and_modify(|s| *s += 10)
.or_insert(10);
or_insert_with takes a closure — the value is only computed when the key is missing. Use this instead of or_insert when the default is expensive.
HashMap Iteration Order
HashMap uses randomly seeded hashing for DoS resistance, so iteration order can vary between runs. If you need deterministic order:
- Sorted keys: Use
BTreeMap(keys iterate in sorted order) - Insertion order: Use
IndexMapfrom theindexmapcrate
In Rust 2018, [1, 2, 3].into_iter() yielded references (&i32) for backwards compatibility. In Rust 2021+, it yields owned values (i32):
// Rust 2021: iterates by value
let arr = [1, 2, 3];
for x in arr.into_iter() {
// x is i32 (owned)
}
// If you want references explicitly:
for x in arr.iter() {
// x is &i32
}
If you're reading older code or upgrading editions, watch for this change. cargo fix --edition handles the migration automatically. For a compact reference of all iterator adapters and collection patterns, see the iterators cheat sheet.
- The Rust Book: Iterators — lazy evaluation, consuming adapters
- std::iter::Iterator — full API with
collect()guidance - HashMap docs — entry API, hashing, performance
- Rust Edition Guide: IntoIterator for Arrays — the 2021 behavior change
When to Use Rust Collections & Iterators: Vec, HashMap, Iterator Adapters
- Use `Vec` for growable arrays when you do not know the size at compile time.
- Use `HashMap` for key-value associations with fast lookup.
- Use iterators to transform collections without mutation.
- Chain iterator adapters for complex transformations.
- Use `collect()` to turn an iterator back into a collection.
Check Your Understanding: Rust Collections & Iterators: Vec, HashMap, Iterator Adapters
What is the difference between iter(), into_iter(), and iter_mut()?
`iter()` borrows elements (`&T`). `into_iter()` takes ownership (consumes the collection). `iter_mut()` borrows mutably (`&mut T`). Choose based on whether you need ownership or just want to read/modify.
What You'll Practice: Rust Collections & Iterators: Vec, HashMap, Iterator Adapters
Common Rust Collections & Iterators: Vec, HashMap, Iterator Adapters Pitfalls
- You chain `.map()` and `.filter()` but nothing happens. Iterator adapters are lazy --- they produce no output until consumed. Add `.collect()`, `.sum()`, `.for_each()`, or use a `for` loop to drive the iterator.
- You get `error[E0382]: borrow of moved value` after a `for` loop. You used `.into_iter()` (or `for x in vec`), which consumes the collection. Switch to `.iter()` or `&vec` if you need the collection afterward.
- You get `error[E0282]: type annotations needed` on `.collect()`. The compiler cannot infer which collection type to build. Add a turbofish `.collect::<Vec<_>>()` or annotate the binding type.
- You get `error[E0277]` collecting references into a `Vec<T>`. You called `.iter()` which yields `&T`, but the target collection expects `T`. Insert `.cloned()` or `.copied()` before `.collect()`, or switch to `.into_iter()`.
- Your HashMap iteration prints keys in a different order every run. HashMap does not guarantee insertion order. Use `BTreeMap` if you need sorted keys, or `.sorted()` from itertools for one-off ordering.
- You call `.unwrap()` inside a `.map()` and panic on bad data. Use `.filter_map(|x| x.parse().ok())` to silently skip failures, or `.map(...).collect::<Result<Vec<_>, _>>()` to propagate errors.
Rust Collections & Iterators: Vec, HashMap, Iterator Adapters FAQ
Why are iterators lazy?
Iterator adapters like `map` and `filter` don't do anything until you consume the iterator. This allows chaining without intermediate allocations. Call `.collect()`, `.sum()`, or iterate with `for` to actually execute.
Why does collect() need a type annotation?
`collect()` can produce many collection types (Vec, HashSet, String, etc.). The compiler needs to know which one: `.collect::<Vec<_>>()` or `let v: Vec<_> = iter.collect()`.
When should I use fold vs reduce?
`fold` takes an initial value and works on any type: `fold(0, |acc, x| acc + x)`. `reduce` uses the first element as initial and requires the same type. Use `fold` when the accumulator type differs from elements.
How do I iterate over HashMap entries?
`for (key, value) in &map { }` iterates over references. `for (key, value) in map { }` consumes the map. Use `.iter()`, `.keys()`, or `.values()` for specific access patterns.
What is the difference between filter and filter_map?
`filter` keeps elements where the predicate is true. `filter_map` transforms elements and filters simultaneously: `filter_map(|x| x.parse().ok())` parses and keeps only successes.
Rust Collections & Iterators: Vec, HashMap, Iterator Adapters Syntax Quick Reference
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];v.push(4);
v.pop();
v.len();use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("key", "value");let value = map.get("key");
map.entry("key").or_insert("default");for x in &v { println!("{x}"); } // borrows: &T
for x in v { println!("{x}"); } // consumes: Tlet doubled: Vec<_> = v.iter()
.filter(|x| *x > 0)
.map(|x| x * 2)
.collect();let sum: i32 = v.iter().fold(0, |acc, x| acc + x);let v: Vec<_> = iter.collect();
let v = iter.collect::<Vec<_>>();let numbers: Vec<i32> = strings.iter()
.filter_map(|s| s.parse().ok())
.collect();for (index, value) in v.iter().enumerate() {
println!("{index}: {value}");
}Rust Collections & Iterators: Vec, HashMap, Iterator Adapters Sample Exercises
Fill in the method to add an element to the Vec.
pushFill in the macro to create a Vec literal.
vec!Fill in the constructor to create an empty HashMap.
new()+ 56 more exercises
Copy-ready syntax examples for quick lookup