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:
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_collectioncallsinto_iter()on&T, yielding borrowed items&mut my_collectioncallsinto_iter()on&mut T, yielding mutable referencesmy_collection(owned) callsinto_iter()onT, consuming the collection and yielding owned items
Let's implement IntoIterator for a custom Portfolio struct holding stock positions:
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&Titer_mut()returns an iterator over&mut Tinto_iter()consumes self and returns an iterator overT
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:
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 movedThe 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:
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:
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
Iteratorby definingnext(), which returnsOption<Self::Item>and modifies internal state. - Implement
IntoIteratorfor 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(), andinto_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.






Comments
Loading comments…