Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 099 / 100

Rust traits Iterator and IntoIterator: a practical guide

Make your types iterable.

Expona Admin8 min read

Introduction

By day 99, you've built custom types, trait implementations, and error handlers—but your collections probably still feel like black boxes. The ability to make your own types iterable transforms how you design APIs and unlocks the full power of Rust's iterator ecosystem. This practical guide on Rust traits Iterator and IntoIterator walks you through making your types integrate seamlessly with for-loops, adaptor chains, and standard library functions.

Implement next() for Iterator

The Iterator trait is deceptively simple: it requires a single method, next(), which returns Option<Self::Item>. When None is returned, the iterator is exhausted. Everything else—map, filter, collect—builds on this foundation.

Let's say we're building a range iterator for a custom DateRange type that yields consecutive dates:

rust
use std::ops::Add;

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Date {
    day: u32,
    month: u32,
    year: u32,
}

impl Date {
    fn next_day(self) -> Date {
        let mut day = self.day + 1;
        let mut month = self.month;
        let mut year = self.year;
        
        let days_in_month = match month {
            2 => if year % 4 == 0 { 29 } else { 28 },
            4 | 6 | 9 | 11 => 30,
            _ => 31,
        };
        
        if day > days_in_month {
            day = 1;
            month += 1;
            if month > 12 {
                month = 1;
                year += 1;
            }
        }
        Date { day, month, year }
    }
}

struct DateRange {
    current: Date,
    end: Date,
}

impl DateRange {
    fn new(start: Date, end: Date) -> Self {
        DateRange { current: start, end }
    }
}

impl Iterator for DateRange {
    type Item = Date;
    
    fn next(&mut self) -> Option {
        if self.current <= self.end {
            let result = self.current;
            self.current = self.current.next_day();
            Some(result)
        } else {
            None
        }
    }
}

Now this custom type works naturally with for-loops and adaptor chains. The key insight: next() must modify self to track position, so implementors take &mut self.

IntoIterator for for-loops

When you write for x in my_collection, Rust doesn't call Iterator directly—it calls IntoIterator::into_iter(). This trait converts a container into an iterator, and it has three variants determined by how you pass the value:

  • &my_collection calls into_iter() on &T, yielding borrowed items
  • &mut my_collection calls into_iter() on &mut T, yielding mutable references
  • my_collection (owned) calls into_iter() on T, consuming the collection and yielding owned items

Let's implement IntoIterator for a custom Portfolio struct holding stock positions:

rust
struct Position {
    ticker: String,
    quantity: u32,
    price: f64,
}

struct Portfolio {
    positions: Vec,
}

// Consuming iterator: takes ownership
impl IntoIterator for Portfolio {
    type Item = Position;
    type IntoIter = std::vec::IntoIter;
    
    fn into_iter(self) -> Self::IntoIter {
        self.positions.into_iter()
    }
}

// Borrowing iterator: yields &Position
impl<'a> IntoIterator for &'a Portfolio {
    type Item = &'a Position;
    type IntoIter = std::slice::Iter<'a, Position>;
    
    fn into_iter(self) -> Self::IntoIter {
        self.positions.iter()
    }
}

// Mutable borrowing iterator: yields &mut Position
impl<'a> IntoIterator for &'a mut Portfolio {
    type Item = &'a mut Position;
    type IntoIter = std::slice::IterMut<'a, Position>;
    
    fn into_iter(self) -> Self::IntoIter {
        self.positions.iter_mut()
    }

let mut portfolio = Portfolio {
    positions: vec![
        Position { ticker: "AAPL".to_string(), quantity: 10, price: 150.0 },
        Position { ticker: "MSFT".to_string(), quantity: 5, price: 350.0 },
    ],
};

// Consuming
for pos in portfolio {
    println!("{}: {} shares", pos.ticker, pos.quantity);
}
// portfolio is now moved; can't use it

// Borrowing
let portfolio = Portfolio { /* ... */ };
for pos in &portfolio {
    println!("{}: {} shares", pos.ticker, pos.quantity);
}
// portfolio still usable

// Mutable borrowing
for pos in &mut portfolio {
    pos.quantity += 1;
}

This pattern is fundamental to Rust's ergonomics. When you implement all three, callers get natural, intuitive behavior without needing method names like iter_consuming() or iter_borrowed().

iter, iter_mut and into_iter

The standard library conventionally provides three iterator methods on collections. These aren't trait requirements—they're a naming convention that users expect:

  • iter() returns an iterator over &T
  • iter_mut() returns an iterator over &mut T
  • into_iter() consumes self and returns an iterator over T

In production systems, we've found this convention invaluable for clarity. A reader sees for x in &items and understands the items remain available afterward. Here's a real-world example from a trading system where we need controlled iteration:

rust
struct OrderBook {
    bids: Vec,
    asks: Vec,
}

impl OrderBook {
    fn iter(&self) -> std::slice::Iter {
        self.bids.iter().chain(self.asks.iter())
    }
    
    fn iter_mut(&mut self) -> std::slice::IterMut {
        self.bids.iter_mut().chain(self.asks.iter_mut())
    }
    
    fn into_iter(self) -> impl Iterator {
        self.bids.into_iter().chain(self.asks)
    }
}

let mut book = OrderBook {
    bids: vec![],
    asks: vec![],
};

// Immutable iteration—useful for reads
for order in book.iter() {
    println!("Order: {}", order.id);
}

// Mutable iteration—update prices, for instance
for order in book.iter_mut() {
    order.price *= 1.01;
}

// Consuming—transfer orders elsewhere
let all_orders: Vec = book.into_iter().collect();
// book is now moved

The key difference from bare trait implementation: when you own the concrete type, you control what gets returned. Returning impl Iterator hides implementation details and lets you change internals (like switching from two vecs to a single flattened vec) without breaking public APIs.

Chain adaptors for free

Once you implement Iterator, you get adaptor methods for free: map, filter, take, skip, chain, zip, and dozens more. These compose beautifully and remain lazy—they don't execute until you consume the iterator with collect(), for_each, or similar.

In a real e-commerce pipeline, adaptors eliminate intermediate collections and keep memory usage constant:

rust
struct Product {
    id: u32,
    name: String,
    price: f64,
    in_stock: bool,
}

fn process_products(catalog: &[Product]) -> Vec<(u32, String)> {
    catalog
        .iter()
        .filter(|p| p.in_stock)
        .filter(|p| p.price > 10.0 && p.price < 1000.0)
        .map(|p| (p.id, p.name.clone()))
        .collect()
}

// Or for a single-pass scan (not collecting):
fn total_value(catalog: &[Product]) -> f64 {
    catalog
        .iter()
        .filter(|p| p.in_stock)
        .map(|p| p.price)
        .sum()
}

Chaining iterators is far faster than building intermediate Vecs at each step. In systems processing millions of events (Kafka consumer loops, trading feeds), this difference compounds. We've seen adaptor chains replace multi-step loops without sacrificing readability.

One subtle win: chain() lets you seamlessly combine iterators of the same type:

rust
let results: Vec = (1..=5)
    .chain(10..=15)
    .map(|x| x * 2)
    .collect();
// results: [2, 4, 6, 8, 10, 20, 22, 24, 26, 28, 30]

Common mistakes

Forgetting that Iterator::next() takes &mut self. You can't borrow an iterator immutably and call next() on it repeatedly. The mutable borrow is necessary because next() modifies internal state. If you need multiple simultaneous iterations over the same collection, clone the iterator or create multiple iterators from the source.

Implementing Iterator but not IntoIterator. Users expect both. At minimum, implement IntoIterator for &T and &mut T even if you don't support ownership transfer. This is why the standard library provides iter() and iter_mut() methods—they bridge user expectations and your trait implementations.

Infinite iterators without bounds. It's easy to write for x in (0..).filter(is_prime) and forget that 0.. never terminates. Always add take(n), take_while(), or a condition that guarantees termination. Infinite iterators are valid; just document them and provide safe boundaries for consumers.

Collecting unnecessarily. A common pattern from imperative languages: let filtered = items.iter().filter(...).collect::<Vec>(); for x in filtered { ... }. Skip the collection unless you need to iterate multiple times. Single-pass adaptors are orders of magnitude faster and use zero temporary heap allocation.

Key takeaways

  • Implement Iterator by defining next(), which returns Option<Self::Item> and modifies internal state.
  • Implement IntoIterator for your type to make it work seamlessly with for-loops in all three modes: borrowed, mutable-borrowed, and consuming.
  • Follow convention: provide iter(), iter_mut(), and into_iter() methods alongside trait implementations for clarity and discoverability.
  • Chain adaptor methods instead of collecting intermediate results; adaptors are lazy and compose with zero allocations.
  • Remember that next() requires &mut self—you can't iterate immutably over a mutable iterator.
  • Infinite iterators are valid and useful; document them clearly and guide callers toward termination conditions.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles