Iterators
Iterators are one of Rust's most powerful and elegant features. They let you process sequences of elements in a composable, zero-cost way — meaning the compiler optimizes iterator chains down to the same machine code as hand-written loops.
What Is an Iterator?
An iterator is any type that implements the Iterator trait, which requires a single method: next(). Each call to next() returns Some(item) until the sequence is exhausted, then returns None.
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(10)
println!("{:?}", iter.next()); // Some(20)
println!("{:?}", iter.next()); // Some(30)
println!("{:?}", iter.next()); // Some(40)
println!("{:?}", iter.next()); // Some(50)
println!("{:?}", iter.next()); // None
}
In practice, you rarely call next() manually. Rust's for loop and iterator adapters handle that for you.
Creating Iterators
There are three common ways to get an iterator from a collection:
.iter()— borrows each element as&T.iter_mut()— borrows each element as&mut T.into_iter()— consumes the collection, yieldingT
fn main() {
let fruits = vec!["apple", "banana", "cherry"];
// Borrow — fruits is still usable after the loop
for fruit in fruits.iter() {
println!("Borrowed: {}", fruit);
}
// into_iter on a reference gives &T too (common in for loops)
for fruit in &fruits {
println!("Via reference: {}", fruit);
}
println!("fruits still alive: {:?}", fruits);
}
Iterator Adapters
Iterator adapters are methods that transform one iterator into another. They are lazy — nothing runs until you consume the iterator.
map — Transform Each Element
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers
.iter()
.map(|&n| n * 2)
.collect();
println!("Original: {:?}", numbers);
println!("Doubled: {:?}", doubled);
// Chain multiple adapters
let squared_evens: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.collect();
println!("Squared evens: {:?}", squared_evens);
}
filter — Keep Only Matching Elements
filter takes a closure that returns true to keep an element or false to skip it.
enumerate — Track Position
enumerate wraps each element with its index, yielding (index, item) pairs.
fn main() {
let tasks = vec!["write tests", "fix bug", "deploy", "review PR"];
for (i, task) in tasks.iter().enumerate() {
println!("Task {}: {}", i + 1, task);
}
// Find the index of the first task containing "bug"
let bug_task = tasks
.iter()
.enumerate()
.find(|(_, task)| task.contains("bug"));
match bug_task {
Some((i, task)) => println!("Found at position {}: {}", i, task),
None => println!("No bug tasks found"),
}
}
Consuming Adapters
Consuming adapters call next() and exhaust the iterator. Common ones include:
| Method | Purpose |
|---|---|
.collect() | Gather into a collection |
.sum() | Add all elements |
.count() | Count elements |
.any() | True if any element matches |
.all() | True if all elements match |
.find() | First matching element |
.fold() | Reduce with an accumulator |
fn main() {
let scores = vec![85, 92, 78, 95, 88, 60, 73];
let total: i32 = scores.iter().sum();
let count = scores.len();
let average = total / count as i32;
let passing: Vec<&i32> = scores.iter().filter(|&&s| s >= 70).collect();
let all_pass = scores.iter().all(|&s| s >= 50);
let has_distinction = scores.iter().any(|&s| s >= 90);
println!("Total: {}, Average: {}", total, average);
println!("Passing scores: {:?}", passing);
println!("All above 50: {}", all_pass);
println!("Has distinction (>=90): {}", has_distinction);
// fold: compute product of all scores (just for demonstration)
let max_score = scores.iter().fold(i32::MIN, |acc, &s| acc.max(s));
println!("Highest score: {}", max_score);
}
Implementing Your Own Iterator
You can make any struct iterable by implementing Iterator:
struct Countdown {
count: u32,
}
impl Countdown {
fn new(start: u32) -> Self {
Countdown { count: start }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count == 0 {
None
} else {
let current = self.count;
self.count -= 1;
Some(current)
}
}
}
fn main() {
let countdown = Countdown::new(5);
// All iterator methods work for free!
let values: Vec<u32> = countdown.collect();
println!("Countdown: {:?}", values);
// Use adapters on your custom iterator
let even_counts: Vec<u32> = Countdown::new(10)
.filter(|n| n % 2 == 0)
.collect();
println!("Even counts: {:?}", even_counts);
}
Because Countdown implements Iterator, it automatically gains access to all iterator adapter methods like map, filter, take, zip, and dozens more.
Chaining Iterators
Two useful adapters for combining iterators:
.chain()— concatenate two iterators.zip()— pair elements from two iterators
fn main() {
let first = vec![1, 2, 3];
let second = vec![4, 5, 6];
// chain: iterate both sequences as one
let combined: Vec<i32> = first.iter().chain(second.iter()).copied().collect();
println!("Chained: {:?}", combined);
// zip: pair elements together
let names = vec!["Alice", "Bob", "Carol"];
let scores = vec![95, 87, 92];
let leaderboard: Vec<(&str, i32)> = names
.iter()
.copied()
.zip(scores.iter().copied())
.collect();
for (name, score) in &leaderboard {
println!("{}: {}", name, score);
}
}
Try It Yourself
Use iterators to process a list of words. Find all words longer than 4 characters, convert them to uppercase, and sort them alphabetically:
fn main() {
let words = vec![
"rust", "iterator", "closure", "map", "filter",
"collect", "trait", "impl", "enum", "match",
];
let mut result: Vec<String> = words
.iter()
.filter(|w| w.len() > 4)
.map(|w| w.to_uppercase())
.collect();
result.sort();
println!("Long words (uppercase, sorted):");
for word in &result {
println!(" {}", word);
}
println!("\nTotal: {} words", result.len());
}
Try modifying this to also include the word length in the output, or filter by a different condition.
Key Takeaways
- Iterators implement the
Iteratortrait with a singlenext()method that returnsOption<Item> - Use
.iter()for borrows,.iter_mut()for mutable borrows, and.into_iter()to consume a collection - Iterator adapters like
map,filter,enumerate, andzipare lazy — they do no work until consumed - Consuming adapters like
collect,sum,fold, andanydrive the iteration to completion - Implementing
Iteratoron your own types gives you all adapter methods for free - Iterator chains compile to efficient machine code with zero runtime overhead — they are not slower than manual loops
- Prefer iterator chains over manual loops when the intent is clearer and the logic is functional in nature
Pro Tip: When you need both the index and the value in a loop, reach for
.enumerate()instead of maintaining a manual counter. And when your iterator chain gets hard to read, consider breaking it into named intermediate variables — Rust's type inference handles the types, so you only need to name the values.
Next Steps
Now that you're comfortable with iterators, we'll explore strings and text processing — a topic where Rust's ownership model has some unique implications.
Next lesson
Strings and Text
Understand Rust's two string types, text manipulation, UTF-8 encoding, and common string operations
25 min