Iterating Over Collections
Iterating over a collection means visiting each element it holds, one at a time, so you can read it, change it, or transform it into something new. In Rust, iteration is built on a single trait, Iterator, so the same vocabulary (map, filter, collect, enumerate, and more) works identically whether you are looping over a Vec<i32>, an array, a HashMap, or a custom type you write yourself. What makes Rust’s iteration distinctive is that it is governed by the same ownership rules as everything else in the language: every loop has to decide whether it borrows the elements or takes ownership of them, and the compiler enforces that decision for you. Understanding the three iteration “modes” and the fact that iterators are lazy is one of the most practical skills you can build in Rust.
Overview: How Iteration Works in Rust
Every collection that can be iterated implements the IntoIterator trait, which knows how to produce an Iterator — a type with a single required method, fn next(&mut self) -> Option<Self::Item>. Each call to next() returns Some(item) for the next element, or None once the collection is exhausted. A for loop is really just sugar: for x in collection { ... } compiles down to calling into_iter() on collection and repeatedly calling next() until it returns None. Because this is ordinary method dispatch on a trait, there is no runtime overhead compared to a hand-written loop — the compiler inlines and optimizes it away.
The important twist is that a collection like Vec<T> actually offers three different ways to get an iterator, and each one interacts with ownership differently:
collection.iter()produces an iterator over shared references (&T). The collection is only borrowed; you can still use it after the loop, but you cannot modify elements through the reference.collection.iter_mut()produces an iterator over mutable references (&mut T). This lets you modify elements in place, but while the loop is running you cannot read or modify the collection any other way — the borrow checker enforces the “one mutable borrow at a time” rule across the whole loop.collection.into_iter()(or simply writingfor x in collection) produces an iterator over owned values (T). Each element is moved out of the collection, and the original collection variable is consumed — you cannot use it again afterward.
Writing for item in &my_vec is shorthand for calling .iter(), and for item in &mut my_vec is shorthand for .iter_mut(). This is exactly the same borrowing logic you already use for function parameters and variable bindings — iteration does not get a special exemption from ownership, it is just another consumer of borrows and moves.
The second big idea is laziness. Methods like .map(), .filter(), .enumerate(), and .zip() are adapters: they wrap an existing iterator in a new iterator and do no work by themselves. Nothing actually runs until you call a consumer such as .collect(), a for loop, .sum(), or .count(), which drives the whole chain by repeatedly calling next(). This means you can chain many adapters together with zero intermediate allocations — the compiler fuses the whole pipeline into a single pass over the data.
Syntax
The general shapes of iteration you will use constantly:
for item in collection.iter() {
// use item (borrowed as &T)
}
for item in &collection {
// equivalent shorthand for collection.iter()
}
for item in collection.iter_mut() {
// use item (borrowed as &mut T), can modify in place
}
for item in collection {
// takes ownership of collection; item is T, collection can't be used after
}
| Form | Item type | Effect on the collection |
|---|---|---|
.iter() / &collection |
&T |
Borrowed; usable again after the loop |
.iter_mut() / &mut collection |
&mut T |
Mutably borrowed for the loop’s duration |
.into_iter() / plain collection |
T |
Moved (consumed); unusable after the loop |
Examples
Example 1: Borrowing with iter() and iter_mut()
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// iter() borrows each element as &i32
for n in numbers.iter() {
println!("value: {}", n);
}
let mut scores = vec![10, 20, 30];
// iter_mut() borrows each element as &mut i32
for s in scores.iter_mut() {
*s += 5;
}
println!("{:?}", scores);
}
value: 1
value: 2
value: 3
value: 4
value: 5
[15, 25, 35]
The first loop only reads numbers, so numbers is still valid afterward if we needed it. The second loop uses iter_mut(), so each s is a &mut i32; dereferencing it with *s lets us modify the value stored in the vector in place, without ever reallocating or replacing the whole Vec.
Example 2: Consuming with into_iter()
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
for name in names {
println!("Hello, {}!", name);
}
}
Hello, Ada!
Hello, Grace!
Because String does not implement Copy, writing for name in names (rather than for name in &names) moves each String out of the vector and into name. Inside the loop name owns its data outright. This is exactly what you want when you no longer need the original vector — for example, when you are transforming a collection into something else and discarding the source.
Example 3: Iterator adapters — filter, map, enumerate, zip
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let doubled_evens: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * 2)
.collect();
println!("{:?}", doubled_evens);
for (index, value) in numbers.iter().enumerate() {
if index == 2 {
println!("index {} -> {}", index, value);
}
}
let letters = vec!['a', 'b', 'c'];
let pairs: Vec<(i32, char)> = numbers
.iter()
.copied()
.zip(letters.iter().copied())
.collect();
println!("{:?}", pairs);
}
[4, 8, 12]
index 2 -> 3
[(1, 'a'), (2, 'b'), (3, 'c')]
filter keeps only elements matching a predicate, map transforms each remaining element, and collect() is a consumer that drives the whole lazy chain and gathers the results into a new Vec<i32>. enumerate() pairs each item with its index. zip() walks two iterators together and stops as soon as the shorter one runs out — here letters has 3 items, so only the first 3 numbers are paired up even though numbers has 6.
Example 4: Iterating a HashMap
use std::collections::HashMap;
fn main() {
let mut inventory: HashMap<String, i32> = HashMap::new();
inventory.insert(String::from("apples"), 10);
inventory.insert(String::from("bananas"), 5);
inventory.insert(String::from("cherries"), 20);
let mut names: Vec<String> = inventory.keys().cloned().collect();
names.sort();
for name in &names {
let count = inventory.get(name).unwrap();
println!("{}: {}", name, count);
}
}
apples: 10
bananas: 5
cherries: 20
HashMap also implements iter() (yielding (&K, &V) pairs), keys(), and values(), but its iteration order is not guaranteed and can even change between runs of the same program, because Rust randomizes the hasher seed to resist denial-of-service attacks. When you need a predictable order for display or testing, collect the keys, sort them, and look up values as shown above — or switch to a BTreeMap, which always iterates in sorted key order. The .unwrap() here is safe because every key in names came directly from inventory.keys(), so the lookup is guaranteed to succeed.
How It Works Step by Step
A for loop is not a primitive — it desugars to explicit calls against the Iterator trait. You can see this by writing the equivalent code manually:
fn main() {
let numbers = vec![1, 2, 3];
let mut iter = numbers.iter();
while let Some(n) = iter.next() {
println!("got {}", n);
}
}
got 1
got 2
got 3
Step by step, this is what the compiler does for every for loop:
- It calls
into_iter()on the thing afterin(here,numbers.iter()already returns an iterator, so that step is a no-op) to obtain a value implementingIterator. - It repeatedly calls
.next(&mut self)on that iterator. Becausenexttakes&mut self, the iterator itself must be mutable — that is whyiteris declared withmutabove. - Each call returns an
Option<Item>. If it isSome(value), the loop body runs withvaluebound to the item and the loop continues. - As soon as a call returns
None, the loop ends. TheVec‘s internal cursor lives entirely inside the iterator; the collection itself never needs a separate “current position” field.
Adapter chains work by nesting this same protocol: numbers.iter().filter(f).map(g) produces a value whose next() implementation calls the inner iterator’s next(), applies f and g as needed, and only advances one element at a time. This is why the whole chain runs in a single pass with no intermediate buffers, no matter how many adapters you stack.
Common Mistakes
1. Using a collection after moving it into a for loop
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
for name in names {
println!("{}", name);
}
println!("{:?}", names);
}
error[E0382]: borrow of moved value: `names`
for name in names calls into_iter(), which consumes names element by element. By the time the loop ends, names no longer owns any data, so the compiler refuses to let you read it again. The fix is to borrow instead of moving, if you still need the collection afterward:
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
for name in &names {
println!("{}", name);
}
println!("{:?}", names);
}
2. Mutating a collection while an immutable borrow of it is alive
fn main() {
let mut numbers = vec![1, 2, 3];
for n in &numbers {
if *n == 2 {
numbers.push(4);
}
}
}
error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
for n in &numbers holds a shared borrow of numbers for the entire loop. Calling numbers.push(4) inside the loop requires a mutable borrow at the same time, which the borrow checker forbids — and for good reason: pushing can reallocate the vector’s backing buffer, which would leave the iterator pointing at freed memory. If you need to build a new collection based on the old one, collect into a separate Vec instead of mutating the one you are iterating:
fn main() {
let numbers = vec![1, 2, 3];
let mut extended = numbers.clone();
for n in &numbers {
if *n == 2 {
extended.push(4);
}
}
println!("{:?}", extended);
}
3. Indexing one past the end
let numbers = vec![10, 20, 30];
let index = numbers.len();
println!("{}", numbers[index]);
thread 'main' panicked at src/main.rs:3:20:
index out of bounds: the len is 3 but the index is 3
This compiles fine — indexing is checked at runtime, not compile time — but it panics because valid indices for a 3-element vector are 0, 1, and 2. This usually comes from an off-by-one in a manual for i in 0..=numbers.len() loop (note the inclusive ..=). Prefer iterating with for n in &numbers or .iter().enumerate(), which never produce an out-of-range index, over manual index arithmetic.
Best Practices
- Default to
.iter()(borrowing) unless you specifically need to consume or mutate the collection — it keeps the original collection usable and avoids unnecessary clones. - Reach for
into_iter()only when you are done with the original collection, such as when transforming it into a new one and discarding the source. - Chain adapters (
filter,map,enumerate,zip,take,skip) instead of writing manual index-based loops — they are just as fast and far less error-prone. - Remember that adapters are lazy: a chain that ends without
.collect(), aforloop, or another consumer does nothing at all. - When you need a predictable iteration order for a map-like collection, sort the keys explicitly or use a
BTreeMaprather than relying onHashMaporder. - Use
iter_mut()for in-place updates instead of collecting into a newVecand reassigning, when you only need to tweak existing values. - Avoid mutating a collection from inside a loop that borrows it; build a separate collection of the changes instead.
Practice Exercises
- Given
let words = vec![String::from("rust"), String::from("is"), String::from("fun")];, use.iter()and.map()to build aVec<usize>of each word’s length, then print it with{:?}. Expected output:[4, 2, 3]. - Given
let mut nums = vec![1, 2, 3, 4, 5];, useiter_mut()to square every element in place, then print the vector. Expected output:[1, 4, 9, 16, 25]. - Given two vectors
let names = vec!["Ada", "Grace", "Alan"];andlet ages = vec![36, 85, 41];, use.zip()and aforloop to print each name with its age as"NAME is AGE years old". Hint: you’ll neednames.iter().zip(ages.iter()).
Summary
Iteratoris a trait built around one method,next(&mut self) -> Option<Item>; aforloop is sugar for calling it repeatedly..iter()borrows elements as&T,.iter_mut()borrows them as&mut T, and.into_iter()(or a bare collection in aforloop) moves ownedTvalues out.- Ordinary ownership and borrowing rules apply to iteration exactly as they do everywhere else — you cannot use a collection after moving it, and you cannot mutate it while it is immutably borrowed by a loop.
- Adapters like
map,filter,enumerate, andzipare lazy and only run when driven by a consumer such as.collect()or aforloop. HashMapiteration order is not guaranteed; sort keys explicitly or useBTreeMapwhen order matters.- Manual index-based loops are more error-prone (out-of-bounds panics) than idiomatic iteration and should be a last resort.
