Iterators
An iterator is a way of processing a series of items. In Rust, iterators are lazy, meaning they don't do any work until you call methods that consume the iterator. Iterators are one of Rust's most powerful features, allowing you to write efficient, functional-style code that's both readable and performant.
What is an Iterator?
An iterator is a value that produces a sequence of values. The Iterator
trait is defined in the standard library and has one required method:
next():
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator trait is in the prelude,
so you don't need to import it. It provides many useful methods for working with
sequences of values.
Creating Iterators
Most collections in Rust can produce iterators. There are three main methods:
1. iter() - Borrows Elements
let v = vec![1, 2, 3];
for item in v.iter() {
println!("{}", item);
}
// v is still valid after iteration
2. into_iter() - Takes Ownership
let v = vec![1, 2, 3];
for item in v.into_iter() {
println!("{}", item);
}
// v is no longer valid - it was moved
3. iter_mut() - Mutable References
let mut v = vec![1, 2, 3];
for item in v.iter_mut() {
*item += 10;
}
// v is modified: [11, 12, 13]
iter() when you only
need to read values, into_iter() when you need to consume the
collection, and iter_mut() when you need to modify values.
Click Run to execute your code
Using next() Directly
You can call next() directly to get values one at a time:
let v = vec![1, 2, 3];
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
next() method returns
Option<Item>. When there are more items, it returns
Some(item). When the iterator is exhausted, it returns
None.
Lazy Evaluation
Iterators are lazy - they don't do any work until you consume them:
let v = vec![1, 2, 3, 4, 5];
let iter = v.iter().map(|x| {
println!("Processing {}", x);
x * 2
});
// Nothing printed yet - iterator is lazy!
let doubled: Vec<i32> = iter.collect(); // Now it executes
// Processing 1, Processing 2, ...
collect(), sum(), or a for loop) for
them to actually do work.
Iterator Consumers
Methods that consume the iterator and produce a final value:
| Method | Description | Example |
|---|---|---|
collect() |
Collect into collection | iter.collect::<Vec<_>>() |
sum() |
Sum all elements | iter.sum() |
product() |
Multiply all elements | iter.product() |
max() |
Find maximum | iter.max() |
min() |
Find minimum | iter.min() |
count() |
Count elements | iter.count() |
any() |
Check if any element matches | iter.any(|x| x > 5) |
all() |
Check if all elements match | iter.all(|x| x > 0) |
Iterator Adapters
Iterator adapters transform one iterator into another. They're lazy and return new iterators:
map() - Transform Each Element
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers.iter()
.map(|x| x * 2)
.collect();
// [2, 4, 6]
filter() - Keep Matching Elements
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = numbers.iter()
.filter(|x| *x % 2 == 0)
.collect();
// [2, 4]
take() - Take First N Elements
let numbers = vec![1, 2, 3, 4, 5];
let first_three: Vec<&i32> = numbers.iter()
.take(3)
.collect();
// [1, 2, 3]
skip() - Skip First N Elements
let numbers = vec![1, 2, 3, 4, 5];
let skipped: Vec<&i32> = numbers.iter()
.skip(2)
.collect();
// [3, 4, 5]
enumerate() - Add Index
let items = vec!["a", "b", "c"];
for (i, item) in items.iter().enumerate() {
println!("{}: {}", i, item);
}
// 0: a, 1: b, 2: c
zip() - Combine Two Iterators
let names = vec!["Alice", "Bob"];
let ages = vec![30, 25];
let people: Vec<_> = names.iter()
.zip(ages.iter())
.collect();
// [("Alice", 30), ("Bob", 25)]
chain() - Concatenate Iterators
let v1 = vec![1, 2, 3];
let v2 = vec![4, 5, 6];
let chained: Vec<&i32> = v1.iter()
.chain(v2.iter())
.collect();
// [1, 2, 3, 4, 5, 6]
flat_map() - Map and Flatten
let words = vec!["hello", "world"];
let chars: Vec<char> = words.iter()
.flat_map(|s| s.chars())
.collect();
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
rev() - Reverse Iterator
let numbers = vec![1, 2, 3];
let reversed: Vec<&i32> = numbers.iter()
.rev()
.collect();
// [3, 2, 1]
Chaining Multiple Adapters
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers.iter()
.filter(|x| *x % 2 == 0) // Keep evens
.map(|x| x * x) // Square them
.take(3) // Take first 3
.copied() // Copy values
.collect();
// [4, 16, 36]
Click Run to execute your code
Creating Custom Iterators
You can create your own iterators by implementing the Iterator trait:
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
let counter = Counter::new(5);
for num in counter {
println!("{}", num); // 1, 2, 3, 4, 5
}
Item defines what
type of value the iterator yields. This is required when implementing
Iterator.
Infinite Iterators
Iterators can be infinite - just never return None:
struct Fibonacci {
curr: u32,
next: u32,
}
impl Iterator for Fibonacci {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
let current = self.curr;
self.curr = self.next;
self.next = current + self.next;
Some(current)
}
}
let fib: Vec<u32> = Fibonacci::new()
.take(10) // Limit infinite iterator
.collect();
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
take() to limit them, or you'll get an infinite
loop.
Click Run to execute your code
When to Use Iterators
Use iterators when:
- Processing collections: Transforming, filtering, or aggregating data
- Functional style: When you prefer functional programming patterns
- Performance: Iterators are often optimized by the compiler
- Readability: Iterator chains can be more readable than loops
- Lazy evaluation: When you want to defer computation
Consider loops when:
- Simple iteration: When you just need to iterate without transformation
- Early exit needed: When you need break/continue
- Side effects: When the primary purpose is side effects, not transformation
Best Practices
- Prefer iterators for transformations: map, filter, etc. are more idiomatic
- Chain adapters efficiently: Multiple adapters are optimized together
- Use collect() when needed: Remember iterators are lazy
- Choose the right iterator method: iter(), into_iter(), or iter_mut()
- Implement Iterator for custom types: When you need custom iteration logic
- Use take() for infinite iterators: Always limit infinite iterators
- Consider performance: Iterators are usually fast, but benchmark if needed
Common Mistakes
1. Forgetting that iterators are lazy
// Wrong - Nothing happens!
let doubled = vec![1, 2, 3].iter().map(|x| x * 2);
// doubled is an iterator, not a Vec
// Correct - Consume the iterator
let doubled: Vec<i32> = vec![1, 2, 3].iter()
.map(|x| x * 2)
.collect();
2. Using into_iter() when you need the collection later
// Wrong - v is moved
let v = vec![1, 2, 3];
for item in v.into_iter() { }
println!("{:?}", v); // Error: v was moved
// Correct - Use iter() to borrow
let v = vec![1, 2, 3];
for item in v.iter() { }
println!("{:?}", v); // OK
3. Creating infinite iterators without limits
// Wrong - Infinite loop!
let infinite: Vec<i32> = (1..).collect(); // Never finishes
// Correct - Use take() to limit
let limited: Vec<i32> = (1..).take(10).collect();
4. Not specifying type for collect()
// Wrong - Compiler doesn't know what to collect into
let result = vec![1, 2, 3].iter().map(|x| x * 2).collect(); // Error!
// Correct - Specify type
let result: Vec<i32> = vec![1, 2, 3].iter()
.map(|x| x * 2)
.collect();
Exercise: Iterators Practice
Task: Practice using iterators for common operations.
Requirements:
- Use iterators to find sum of squares of even numbers
- Create a custom iterator for powers of 2
- Use zip to combine names and scores
- Flatten nested vectors using flat_map
- Create a prime number iterator
Click Run to execute your code
Show Solution
// Sum of squares of evens
let sum: i32 = numbers.iter()
.filter(|x| *x % 2 == 0)
.map(|x| x * x)
.sum();
// Powers of 2 iterator
struct PowersOfTwo {
current: u32,
max_power: u32,
}
impl Iterator for PowersOfTwo {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.max_power {
let result = 2_u32.pow(self.current);
self.current += 1;
Some(result)
} else {
None
}
}
}
// Zip and filter
let high_scorers: Vec<_> = names.iter()
.zip(scores.iter())
.filter(|(_, &score)| score >= 90)
.collect();
// Flatten nested vector
let flattened: Vec<i32> = nested.iter()
.flat_map(|v| v.iter())
.copied()
.collect();
Summary
- Iterators process sequences of values
- Three main methods:
iter(),into_iter(),iter_mut() - Iterators are lazy - they don't work until consumed
- Iterator adapters transform iterators (map, filter, take, etc.)
- Iterator consumers produce final values (collect, sum, etc.)
- Implement
Iteratortrait to create custom iterators - Use
take()to limit infinite iterators - Iterators are often as fast as loops (compiler optimizes them)
- Chain adapters for complex transformations
What's Next?
Now that you understand iterators, you'll learn about closures - anonymous functions that can capture their environment. Closures are often used with iterators (like in map() and filter()) and are a powerful feature for writing concise, functional-style code.
Enjoying these tutorials?