Rust 53 🦀 Processing Collections with Iterators
The previous chapter established the iterator trait and the split between adapters and consumers. This chapter applies that machinery to real collections. Vec, HashMap, HashSet, BTreeMap, String, and slices all implement IntoIterator, and each one offers a different set of iteration forms. Knowing which iterator each collection produces, what type its items are, and how to move between collections inside a single chain is what turns a sequence of awkward loops into a single readable pipeline. This chapter is a working reference for the practical question: given a collection, which iterator do I reach for?
Key point: Every collection implements IntoIterator in three forms. iter() borrows shared, iter_mut() borrows mutable, and into_iter() consumes. The item type changes with each form, and choosing the wrong one is the most common source of borrow-checker frustration when working with collections.
Why collection iteration deserves its own treatment
The item-type problem. A Vec<T> yields &T, &mut T, or T depending on which form is used. A HashMap<K, V> yields (&K, &V), (&K, &mut V), or (K, V). A String yields char, &str, or u8 depending on the method. The item type determines what the closure receives and what the chain can do. Getting it right is the first step in every iteration task.
The consumption problem. into_iter() consumes the collection. After it runs, the collection is gone. This is often desirable — it avoids cloning — but it changes the ownership picture of the surrounding code. Choosing iter() preserves the collection at the cost of references.
The ordering problem. Vec iterates in insertion order. HashMap iterates in an unspecified order. BTreeMap iterates in key order. HashSet iterates in an unspecified order. Programs that rely on HashMap iteration order are incorrect, even if they appear to work.
The mutation problem. Mutating a collection while iterating it is a borrow-checker error. The mutable iterator holds a mutable borrow of the collection, and any other access is rejected. Understanding this rule is what lets a developer restructure a loop that “just needs to update one field” into something the compiler accepts.
The conversion problem. Many tasks move between collection types inside a single chain: a Vec becomes a HashMap, a HashMap becomes a Vec of tuples, a String becomes a Vec of words. The collect consumer handles the conversion, but the target type must implement FromIterator.
The efficiency problem. Collecting intermediate results is sometimes necessary, but often avoidable. A chain that calls collect twice when once would do is wasting allocations. The lazy adapter model lets the whole chain run in one pass if the target type allows it.
a. Vec and slices
Vec<T> is the most common collection. It is ordered, contiguous, and supports all three iteration forms.
The three forms
fn main() {
let mut v = vec![1, 2, 3];
// Shared borrow: items are &i32.
for x in v.iter() { }
// Mutable borrow: items are &mut i32.
for x in v.iter_mut() { *x += 1; }
// Consuming: items are i32, v is gone.
for x in v.into_iter() { }
}
The for x in &v form is shorthand for v.iter(), and for x in &mut v is shorthand for v.iter_mut(). Both are idiomatic and produce the same iterators.
Indexed iteration
enumerate pairs each item with its index.
let v = vec!["a", "b", "c"];
for (i, s) in v.iter().enumerate() {
println!("{}: {}", i, s);
}
The index is a usize starting at zero. To start at a different number, add to the index in the loop body.
Windows and chunks
Slices provide windows(n) and chunks(n) for iterating over sub-slices.
let data = [1, 2, 3, 4, 5];
for w in data.windows(2) {
println!("{:?}", w); // [1,2], [2,3], [3,4], [4,5]
}
for c in data.chunks(2) {
println!("{:?}", c); // [1,2], [3,4], [5]
}
windows yields overlapping sub-slices of fixed length. chunks yields non-overlapping sub-slices, with the last chunk possibly shorter.
Chunks_exact and rchunks
chunks_exact(n) yields only full-length chunks and drops the remainder. rchunks(n) iterates from the end.
let data = [1, 2, 3, 4, 5];
for c in data.chunks_exact(2) {
println!("{:?}", c); // [1,2], [3,4]
}
The Vec adapter chain
The standard pattern is to filter, transform, and collect.
let v = vec![1, 2, 3, 4, 5, 6];
let evens_doubled: Vec<i32> = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
assert_eq!(evens_doubled, vec![4, 8, 12]);
The &&x pattern in the filter is because iter() yields &i32, and filter passes &&i32 to the predicate. The &x in the map dereferences the &i32 to i32.
Consuming a Vec
When the Vec is no longer needed, into_iter avoids cloning.
let names = vec![String::from("alice"), String::from("bob")];
let uppercase: Vec<String> = names.into_iter()
.map(|s| s.to_uppercase())
.collect();
// names is no longer available.
Each String is moved into the closure, transformed, and moved into the new Vec.
b. HashMap and HashSet
HashMap<K, V> iterates as pairs of (&K, &V). HashSet<T> iterates as &T. Neither guarantees order.
The iteration forms
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
// Shared borrow: items are (&K, &V).
for (k, v) in &map { }
// Mutable borrow: items are (&K, &mut V).
for (k, v) in &mut map { *v += 1; }
// Consuming: items are (K, V).
for (k, v) in map { }
}
The shared form yields a tuple of references. The mutable form allows changing the value but not the key, because the key is part of the hash and changing it would corrupt the table.
The keys, values, and values_mut methods
HashMap provides keys(), values(), and values_mut() for iterating over just one side.
for key in map.keys() { }
for value in map.values() { }
for value in map.values_mut() { *value += 1; }
keys() yields &K, values() yields &V, and values_mut() yields &mut V.
Building a HashMap from a Vec
A common pattern is to build a map from a collection of pairs.
let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<&str, i32> = pairs.into_iter().collect();
The collect consumer builds the map because HashMap implements FromIterator<(K, V)>. Duplicate keys keep the last value.
Transforming a HashMap
A map can be transformed into another map by iterating and collecting.
let doubled: HashMap<&str, i32> = map.iter()
.map(|(k, v)| (*k, v * 2))
.collect();
Each (k, v) is a tuple of references. The *k copies the key and v * 2 computes the new value.
HashSet operations
HashSet yields &T. The collect consumer builds a set from any iterator.
use std::collections::HashSet;
let set: HashSet<i32> = vec![1, 1, 2, 3, 3].into_iter().collect();
assert_eq!(set.len(), 3);
Set operations
HashSet provides union, intersection, difference, and symmetric_difference.
let a: HashSet<i32> = vec![1, 2, 3].into_iter().collect();
let b: HashSet<i32> = vec![3, 4, 5].into_iter().collect();
let union: HashSet<i32> = a.union(&b).copied().collect();
let intersection: HashSet<i32> = a.intersection(&b).copied().collect();
The set operations return iterators of references, so copied() converts &i32 to i32 before collecting.
The unordered caveat
HashMap and HashSet iterate in an unspecified order. If order matters, use BTreeMap or BTreeSet, which iterate in key order.
c. BTreeMap, BTreeSet, and String
BTreeMap and BTreeSet are sorted collections. String and &str have their own iteration forms for characters, bytes, and lines.
BTreeMap and BTreeSet
The iteration forms match HashMap and HashSet, but the order is sorted by key.
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert("c", 3);
map.insert("a", 1);
map.insert("b", 2);
for (k, v) in &map {
println!("{}: {}", k, v); // a: 1, b: 2, c: 3
}
The sorted order makes BTreeMap appropriate for range queries. The range method iterates over a key range.
for (k, v) in map.range("a".."c") {
println!("{}: {}", k, v); // a: 1, b: 2
}
String characters
String and &str provide chars() for Unicode scalar values and bytes() for UTF-8 bytes.
let s = "hello";
for c in s.chars() { }
for b in s.bytes() { }
chars() yields char, which is a Unicode scalar value. bytes() yields u8. For ASCII strings, they are equivalent, but for non-ASCII they differ.
String lines and words
lines() splits on newlines. split_whitespace() splits on any whitespace.
let text = "first line\nsecond line\nthird line";
for line in text.lines() { }
let sentence = "hello world foo";
for word in sentence.split_whitespace() { }
lines() yields &str for each line without the trailing newline. split_whitespace() yields &str for each word.
The split method
split(pat) splits on a pattern. The pattern can be a character, a string, or a closure.
let csv = "a,b,c";
for field in csv.split(',') { }
let multi = "a b c";
for field in multi.split(' ') { } // Yields empty strings.
Building a String from an iterator
collect can build a String from an iterator of char or &str.
let chars: Vec<char> = vec!['h', 'i'];
let s: String = chars.into_iter().collect();
let words = vec!["hello", " ", "world"];
let sentence: String = words.into_iter().collect();
String implements FromIterator<char> and FromIterator<&str>.
The join method
join concatenates string slices with a separator.
let words = vec!["hello", "world"];
let sentence = words.join(" ");
assert_eq!(sentence, "hello world");
join is a slice method, not an iterator method, but it is the idiomatic way to combine an iterator of strings.
Complete Example Session
// ============================================
// PART 1: VEC — THE THREE FORMS
// ============================================
fn main() {
let mut v = vec![1, 2, 3];
for x in &v { println!("{}", x); } // &i32
for x in &mut v { *x *= 2; } // &mut i32
let owned: Vec<i32> = v.into_iter().collect(); // i32
}
// ============================================
// PART 2: VEC — FILTER AND MAP
// ============================================
fn main() {
let v = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
assert_eq!(result, vec![4, 8, 12]);
}
// ============================================
// PART 3: VEC — WINDOWS AND CHUNKS
// ============================================
fn main() {
let data = [1, 2, 3, 4, 5];
let pairs: Vec<_> = data.windows(2).collect();
assert_eq!(pairs, vec![&[1, 2][..], &[2, 3], &[3, 4], &[4, 5]]);
let groups: Vec<_> = data.chunks(2).collect();
assert_eq!(groups.len(), 3); // [1,2], [3,4], [5]
}
// ============================================
// PART 4: HASHMAP — ITERATION
// ============================================
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("alice", 10);
scores.insert("bob", 20);
for (name, score) in &scores {
println!("{}: {}", name, score);
}
let total: i32 = scores.values().sum();
assert_eq!(total, 30);
}
// ============================================
// PART 5: HASHMAP — BUILDING FROM A VEC
// ============================================
fn main() {
let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
let map: HashMap<&str, i32> = pairs.into_iter().collect();
assert_eq!(map["a"], 1);
}
// ============================================
// PART 6: HASHSET — DEDUPLICATION
// ============================================
use std::collections::HashSet;
fn main() {
let numbers = vec![1, 1, 2, 3, 3, 3];
let unique: HashSet<i32> = numbers.into_iter().collect();
assert_eq!(unique.len(), 3);
}
// ============================================
// PART 7: BTREEMAP — SORTED ITERATION
// ============================================
use std::collections::BTreeMap;
fn main() {
let mut map = BTreeMap::new();
map.insert("c", 3);
map.insert("a", 1);
map.insert("b", 2);
let keys: Vec<&&str> = map.keys().collect();
assert_eq!(keys, vec![&"a", &"b", &"c"]);
}
// ============================================
// PART 8: STRING — CHARS, LINES, WORDS
// ============================================
fn main() {
let s = "hello world";
let chars: Vec<char> = s.chars().collect();
assert_eq!(chars.len(), 11);
let words: Vec<&str> = s.split_whitespace().collect();
assert_eq!(words, vec!["hello", "world"]);
}
// ============================================
// PART 9: STRING — JOIN AND COLLECT
// ============================================
fn main() {
let words = vec!["hello", "world"];
let sentence = words.join(" ");
assert_eq!(sentence, "hello world");
let chars: Vec<char> = vec!['h', 'i'];
let s: String = chars.into_iter().collect();
assert_eq!(s, "hi");
}
// ============================================
// PART 10: MOVING BETWEEN COLLECTIONS
// ============================================
use std::collections::BTreeMap;
fn main() {
// Vec of tuples → HashMap.
let pairs = vec![("a", 1), ("b", 2)];
let map: HashMap<&str, i32> = pairs.into_iter().collect();
// HashMap → Vec of tuples, sorted.
let mut sorted: Vec<(&str, i32)> = map.into_iter().collect();
sorted.sort_by_key(|&(k, _)| k);
// Vec of tuples → BTreeMap.
let btree: BTreeMap<&str, i32> = sorted.into_iter().collect();
// BTreeMap → Vec of keys.
let keys: Vec<&str> = btree.keys().copied().collect();
assert_eq!(keys, vec!["a", "b"]);
}
The ten parts progressed from the Vec iteration forms through filter and map, windows and chunks, HashMap iteration and construction, HashSet deduplication, BTreeMap sorted iteration, String characters and words, join and collect, and moving between collection types. The same collections ran through every part, showing how each type’s iterator is used.
Quick Reference
Iteration Forms by Collection
| Collection | iter() yields | iter_mut() yields | into_iter() yields |
|---|---|---|---|
Vec<T> | &T | &mut T | T |
HashMap<K, V> | (&K, &V) | (&K, &mut V) | (K, V) |
HashSet<T> | &T | — | T |
BTreeMap<K, V> | (&K, &V) | (&K, &mut V) | (K, V) |
BTreeSet<T> | &T | — | T |
String / &str | char via chars(), u8 via bytes() | — | — |
Slice Methods
| Method | Yields | Purpose |
|---|---|---|
windows(n) | Sub-slices of length n | Overlapping windows |
chunks(n) | Sub-slices of length up to n | Non-overlapping chunks |
chunks_exact(n) | Sub-slices of length n | Drops remainder |
rchunks(n) | Sub-slices from the end | Reverse chunks |
HashMap and BTreeMap Methods
| Method | Yields | Order |
|---|---|---|
keys() | &K | Hash: unspecified; BTree: sorted |
values() | &V | Same |
values_mut() | &mut V | Same |
iter() | (&K, &V) | Same |
range(..) | (&K, &V) | BTree only; sorted |
String Methods
| Method | Yields | Splits On |
|---|---|---|
chars() | char | Unicode scalar values |
bytes() | u8 | UTF-8 bytes |
lines() | &str | Newlines |
split_whitespace() | &str | Any whitespace |
split(pat) | &str | The pattern |
Collection Conversions
| From | To | Method |
|---|---|---|
Vec<(K, V)> | HashMap<K, V> | collect() |
Vec<T> | HashSet<T> | collect() |
HashMap<K, V> | Vec<(K, V)> | collect() |
Vec<&str> | String | join("") |
Vec<char> | String | collect() |
Best Practices
✅ Do This:
// Choose the iteration form that matches the need.
for x in &v { } // ✅ Shared borrow
for x in &mut v { } // ✅ Mutable borrow
for x in v { } // ✅ Consume when the collection is done
// Use into_iter to avoid cloning.
let upper: Vec<String> = names.into_iter().map(|s| s.to_uppercase()).collect(); // ✅
// Use enumerate instead of a manual counter.
for (i, x) in v.iter().enumerate() { } // ✅
// Use values() when only values are needed.
let total: i32 = scores.values().sum(); // ✅
// Use BTreeMap when sorted iteration is required.
let sorted: BTreeMap<_, _> = map.into_iter().collect(); // ✅
// Use split_whitespace for word splitting.
for word in text.split_whitespace() { } // ✅
❌ Don’t Do This:
// Don't clone the collection just to iterate.
for x in v.clone() { } // ❌ Use &v or into_iter
// Don't rely on HashMap iteration order.
let first = map.iter().next(); // ❌ Order is unspecified
// Don't collect intermediate results unnecessarily.
let temp: Vec<_> = v.iter().map(...).collect();
let result: Vec<_> = temp.into_iter().filter(...).collect(); // ❌ One pass suffices
// Don't use split(' ') when split_whitespace is meant.
"a b".split(' ') // ❌ Yields empty strings
// Don't ignore the remainder with chunks_exact.
data.chunks_exact(3) // ✅ If remainder is intentionally dropped
// Don't mutate while iterating without iter_mut.
for x in &v { v.push(...); } // ❌ Borrow error
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
&&x in filter | iter() yields &T, filter passes &&T | Pattern-match the double reference |
Cannot move out of &T | Shared borrow yields references | Use into_iter() or clone |
| Unsorted HashMap output | Hash order is unspecified | Use BTreeMap |
split yields empty strings | Consecutive separators | Use split_whitespace |
| Borrow error in loop | Mutating while iterating | Use iter_mut or restructure |
| Collect type ambiguous | Target type not specified | Annotate the binding |
| Intermediate Vec allocated | Two collect calls | Combine into one chain |
Keys change in iter_mut | Key is part of hash | Mutate values only |
Real-World Examples
1. Filter and Double
let doubled: Vec<i32> = v.iter().filter(|&&x| x > 0).map(|&x| x * 2).collect();
2. Sum Values
let total: i32 = scores.values().sum();
3. Build a Map
let map: HashMap<&str, i32> = pairs.into_iter().collect();
4. Deduplicate
let unique: HashSet<i32> = numbers.into_iter().collect();
5. Sorted Keys
let keys: Vec<&&str> = btree_map.keys().collect();
6. Split Words
let words: Vec<&str> = text.split_whitespace().collect();
7. Join Strings
let sentence = words.join(" ");
8. Sliding Window
for pair in data.windows(2) { }
9. Batch Processing
for batch in data.chunks(100) { }
10. Convert HashMap to Sorted Vec
let mut sorted: Vec<_> = map.into_iter().collect();
sorted.sort_by_key(|&(k, _)| k);
Visual
Iteration Forms
┌────────────────────────────────────────────────────────────┐
│ THREE FORMS OF COLLECTION ITERATION │
│ │
│ Collection: Vec<T> │
│ ┌──────────────────────────────────────────────────┐ │
│ │ &v / v.iter() → &T (shared borrow) │ │
│ │ &mut v / v.iter_mut() → &mut T (mutable borrow) │ │
│ │ v / v.into_iter() → T (owned, consumes) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Collection: HashMap<K, V> │
│ ┌──────────────────────────────────────────────────┐ │
│ │ &map / map.iter() → (&K, &V) │ │
│ │ &mut map → (&K, &mut V) │ │
│ │ map / map.into_iter() → (K, V) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The choice determines what the closures see. │
│ into_iter avoids clones but consumes the collection. │
│ │
└────────────────────────────────────────────────────────────┘
Collection Conversions
┌────────────────────────────────────────────────────────────┐
│ MOVING BETWEEN COLLECTION TYPES VIA collect() │
│ │
│ Vec<(K, V)> ──────────────▶ HashMap<K, V> │
│ │ ▲ │
│ │ .into_iter().collect() │
│ │ │
│ ▼ │
│ HashMap<K, V> ────────────▶ Vec<(K, V)> │
│ │ ▲ │
│ │ .into_iter().collect() │
│ │ │
│ ▼ │
│ BTreeMap<K, V> ──────────▶ Vec<&K> (keys) │
│ │
│ collect targets any type implementing FromIterator. │
│ HashMap: FromIterator<(K, V)> │
│ HashSet: FromIterator<T> │
│ Vec: FromIterator<T> │
│ String: FromIterator<char> or FromIterator<&str> │
│ │
└────────────────────────────────────────────────────────────┘
String Iteration
┌────────────────────────────────────────────────────────────┐
│ ITERATING A STRING │
│ │
│ let s = "hello\nworld"; │
│ │
│ s.chars() → 'h', 'e', 'l', 'l', 'o', '\n', 'w', ... │
│ Unicode scalar values, one per code point. │
│ │
│ s.bytes() → 104, 101, 108, 108, 111, 10, 119, ... │
│ UTF-8 bytes. Equivalent to chars for ASCII only. │
│ │
│ s.lines() → "hello", "world" │
│ Splits on newlines, strips the newline. │
│ │
│ s.split_whitespace() → "hello", "world" │
│ Splits on any whitespace, no empty strings. │
│ │
│ s.split(',') → splits on the literal comma │
│ May yield empty strings between consecutive commas. │
│ │
└────────────────────────────────────────────────────────────┘
Ordered vs Unordered
┌────────────────────────────────────────────────────────────┐
│ ITERATION ORDER BY COLLECTION │
│ │
│ Vec<T> │
│ Insertion order. Predictable. │
│ [3, 1, 2] iterates as 3, 1, 2. │
│ │
│ HashMap<K, V> │
│ Unspecified. Depends on hash function and capacity. │
│ Not safe to rely on. │
│ │
│ HashSet<T> │
│ Unspecified. Same caveat as HashMap. │
│ │
│ BTreeMap<K, V> │
│ Sorted by key. Predictable. │
│ Insertion order does not matter. │
│ │
│ BTreeSet<T> │
│ Sorted. Predictable. │
│ │
│ Use BTree variants when order matters. │
│ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
iter() | Yields &T; shared borrow |
iter_mut() | Yields &mut T; mutable borrow |
into_iter() | Yields T; consumes the collection |
windows(n) | Overlapping sub-slices |
chunks(n) | Non-overlapping sub-slices |
chars() | Unicode scalar values |
bytes() | UTF-8 bytes |
lines() | Lines without newlines |
split_whitespace() | Words without empty strings |
keys() / values() | One side of a map |
values_mut() | Mutable values |
range(..) | Key range in BTreeMap |
join(sep) | Concatenate strings with a separator |
collect() | Build any FromIterator target |
Key takeaways:
- Every collection has three iteration forms.
iter()borrows shared,iter_mut()borrows mutable, andinto_iter()consumes. The item type changes with each form, and choosing the wrong one is the most common source of borrow-checker frustration. HashMapandHashSetiterate in unspecified order. Programs that rely on hash iteration order are incorrect. UseBTreeMaporBTreeSetwhen order matters.collectbuilds anyFromIteratortype. AVecof tuples becomes aHashMap, aVecbecomes aHashSet, aHashMapbecomes aVecof tuples. The target type is inferred from context.- Slices provide
windowsandchunksfor sub-slice iteration.windowsoverlaps,chunksdoes not.chunks_exactdrops the remainder, andrchunksiterates from the end. - Strings have several iteration methods.
charsfor Unicode scalar values,bytesfor UTF-8,linesfor line splitting,split_whitespacefor words.splitwith a character may yield empty strings between consecutive separators. joinis the idiomatic way to combine an iterator of strings. A slice method, not an iterator method, but it complements the iterator API.- Mutating a collection while iterating requires
iter_mut. A shared borrow and a mutable access cannot coexist. The compiler rejects the combination, and the fix is to useiter_mutor restructure the loop. - Intermediate
collectcalls are often avoidable. A single chain that filters, transforms, and collects into the final type is more efficient than multiple collects. The lazy adapter model ensures the work happens once.
Remember: Processing collections with iterators is a matter of choosing the right iteration form and chaining adapters into a consumer. Vec yields references or owned values depending on the form. HashMap and HashSet iterate unordered; BTreeMap and BTreeSet iterate sorted. String provides chars, bytes, lines, and split_whitespace for different notions of iteration. collect converts between collection types wherever the target implements FromIterator. The whole set of patterns is built on the same primitives: iter, iter_mut, into_iter, adapters, and consumers. Once the item type of each form is known, the chain writes itself.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!