DEV Community

Subesh Yadav
Subesh Yadav

Posted on

Day 23 of #100DaysOfRust: Understanding Iterators in Rust

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
Enter fullscreen mode Exit fullscreen mode

🔂 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}");
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

After:

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}
Enter fullscreen mode Exit fullscreen mode

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 })
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the next() 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)