Welcome to Day 23 of my #100DaysOfRust journey! Today was all about iterators — a fundamental and powerful feature of Rust. I explored their structure, behavior, and how to harness their power for cleaner, more efficient code.
🔁 What is the Iterator Pattern?
The iterator pattern lets you perform actions on a sequence of items one by one. In Rust, iterators are *lazy* — they don't do anything unless consumed.
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // Creates the iterator, but doesn't do anything yet
🔂 Using Iterators with for
Loops
You can use iterators explicitly or implicitly. Here's an example:
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
Rust hides the iteration logic under the hood, simplifying your code.
🧵 The Iterator
Trait and next()
Method
Rust defines the core behavior of iterators via the Iterator
trait:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
You can call next()
to manually iterate:
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
Note:
- The iterator must be mutable.
-
iter()
returns immutable references (&T
). - Use
into_iter()
to take ownership. - Use
iter_mut()
for mutable references.
📥 Consuming Adaptors (e.g., sum
)
These methods consume the iterator. For example:
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
You can't reuse the iterator after a consuming method like sum
.
🧪 Iterator Adapters (e.g., map
)
These are lazy and return new iterators instead of consuming the current one.
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
The above transforms each value but doesn’t execute until collect()
is called.
🔍 Using filter()
with Captured Environment
Closures passed to iterator adapters can capture their environment:
#[derive(Debug, PartialEq)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
Closures here access the external shoe_size
variable.
🔧 Refactoring with Iterators
You can replace imperative logic with iterators to simplify code.
Before:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
After:
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
So much cleaner and more concise!
🔄 Replacing clone
with Iterator
We can reduce inefficient clone()
usage by changing a method to accept an iterator:
impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next(); // skip program name
let query = args.next().ok_or("Missing query")?;
let file_path = args.next().ok_or("Missing file path")?;
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore_case })
}
}
This makes the function more idiomatic and avoids unnecessary allocations.
🧠 Summary
- Iterators in Rust are lazy and only do work when consumed.
- The
Iterator
trait defines thenext()
method. - You can consume iterators (
sum
,collect
) or adapt them (map
,filter
). - Closures passed to adapters can capture external state.
- Iterators enable concise, declarative, and safe data processing.
That’s all for Day 23! 🚀
Iterators are at the heart of idiomatic Rust.
🔔 Follow me for the rest of my journey at @subesh_dev!
Top comments (0)