Iterators: Difference between revisions
Created page with "After playing with vectors, and enums the last two days, it’s time to look in on Rust’s iterators -a feature that makes working with collections feel like a breeze. Iterators are like a conveyor belt in a factory, delivering items one by one for your code to process, without you needing to micromanage the details. They’re flexible, efficient, and pack a punch for real-world tasks. In this guide, we’ll walk through what iterators are, how to use them, and how the..." |
(No difference)
|
Latest revision as of 18:51, 23 November 2025
After playing with vectors, and enums the last two days, it’s time to look in on Rust’s iterators -a feature that makes working with collections feel like a breeze. Iterators are like a conveyor belt in a factory, delivering items one by one for your code to process, without you needing to micromanage the details. They’re flexible, efficient, and pack a punch for real-world tasks.
In this guide, we’ll walk through what iterators are, how to use them, and how they shine in practical scenarios like processing game scores or filtering server logs. Let’s get the conveyor belt rolling!
What Are Iterators?
In Rust, an iterator is anything that can produce a sequence of items one at a time. Think of it as a playlist for your data, you hit “next” to get the next song, or in this case, the next item.
Iterators work with collections like vectors, arrays, or even custom types, and they’re designed to be lazy, meaning they only compute values when you ask for them.
This laziness saves memory and CPU, especially for large datasets. You get an iterator from a collection using methods like iter(), into_iter(), or iter_mut(), like this:
let scores = vec![100, 200, 300];
for score in scores.iter() {
println!("Score: {}", score);
}
Here, scores.iter() gives you an iterator that borrows each element, printing 100, 200, 300.
Iterators are everywhere in Rust, and understanding their flavors is important to using them well. Let’s see how they’re created and what makes them tick. Creating Iterators
Rust gives us a few ways to create iterators, depending on how you want to interact with your data. Let’s use a real-world example: a game leaderboard with player scores. Suppose you have a vector of scores:
let leaderboard = vec![500, 300, 450];
You can create iterators in three main ways:
* iter(): Borrows each element immutably, great for reading data without changing it.
for &score in leaderboard.iter() {
println!("Player scored: {}", score);
}
* into_iter(): Takes ownership of the collection, moving its elements. Use this when you’re done with the original vector.
let owned: Vec<i32> = leaderboard.into_iter().collect();
* iter_mut(): Borrows elements mutably, letting you modify them in place.
let mut leaderboard = vec![500, 300, 450];
for score in leaderboard.iter_mut() {
*score += 50; // Boost each score
}
Each method fits a different scenario, reading, moving, or modifying. You can also create iterators from scratch, like 0..5 for a range of numbers. Now that we know how to get iterators, let’s see what we can do with them.
Common Iterator Methods
Iterators come with a toolbox of methods that let you transform, filter, or combine data in clever ways. These methods are chainable, meaning you can string them together like a pipeline.
Let’s say you’re running a gaming tournament and need to process scores. Here’s a taste of what iterators can do:
let scores = vec![100, 250, 75, 300, 150];
let total: i32 = scores.iter()
.filter(|&&score| score > 100) // Keep scores above 100
.map(|&score| score + 10) // Add 10-point bonus
.sum(); // Add them up
println!("Total bonus scores: {}", total); // Prints 560 (260 + 310)
- filter: Keeps only items that match a condition, like scores above 100.
- map: Transforms each item, here adding a 10-point bonus.
- sum: Adds all items together, perfect for totaling scores.
Other handy methods include:
- take(n): Grabs the first n items, useful for limiting output.
- skip(n): Ignores the first n items, great for pagination.
- collect: Turns an iterator back into a collection, like a Vec.
These methods make iterators useful for processing data efficiently. Let’s see how they work.
Iterators in Real-World Scenarios Iterators shine in practical tasks where you need to process data cleanly. Imagine you’re building a game server that logs player actions, and you want to analyze them. Here’s a log of actions:
let actions = vec!["jump", "attack", "jump", "heal", "attack"];
let jump_count: i32 = actions.iter()
.filter(|&&action| action == "jump")
.count() as i32;
println!("Players jumped {} times", jump_count); // Prints 2
This counts how many times players jumped, using filter and count. Now, suppose you’re running a coffee shop app and need to process orders:
let orders = vec![2.50, 3.75, 0.0, 4.25, 2.50];
let valid_total: f32 = orders.into_iter()
.filter(|&price| price > 0.0) // Skip invalid orders
.map(|price| price * 1.1) // Add 10% tax
.sum();
println!("Total with tax: ${:.2}", valid_total); // Prints $9.35
Here, into_iter consumes the vector, filter removes invalid orders, map adds tax, and sum calculates the total. Iterators make this code concise and readable. Another example: generating a leaderboard’s top 3 scores:
let mut scores = vec![500, 300, 450, 600, 200]; scores.sort_by(|a, b| b.cmp(a)); // Sort descending
let top_three: Vec<i32> = scores.into_iter().take(3).collect();
println!("Top scores: {:?}", top_three); // Prints [600, 500, 450]
These examples show how iterators handle real tasks with minimal fuss. But to use them well, you need to follow some best practices. Best Practices for Using Iterators
To make iterators work smoothly, you need to keep a few tips in mind.
- Choose the right iterator method for your needs i.e only use iter() for borrowing, into_iter() when you’re okay losing the collection, and iter_mut() for in-place changes.
- Chain methods thoughtfully to avoid unnecessary work, filter first to reduce the dataset before mapping or summing. For example:
let valid_scores: i32 = scores.iter()
.filter(|&&score| score > 0) // Filter first .map(|&score| score * 2) // Then transform .sum();
- If you’re collecting results, specify the type explicitly (e.g., Vec<i32>) to help Rust’s type inference.
- Also, be mindful of iterator laziness, i.e methods like map or filter don’t run until you consume the iterator with something like collect or for.
- Finally, use ranges (0..n) for simple sequences to avoid creating vectors unnecessarily.
Common Pitfalls and How to Avoid Them Iterators are great, but they can trip you up. One common mistake is forgetting that into_iter() consumes the collection, making it unusable afterward.
let v = vec![1, 2, 3]; let sum: i32 = v.into_iter().sum(); println!("Vector: {:?}", v); // Error: v was moved!
Use iter() instead if you need the vector later. Another pitfall is infinite iterators, like the cycle() from our quiz. Without take(), this loops forever:
let infinite = (0..3).cycle(); // 0, 1, 2, 0, 1, 2, ...
Always pair cycle() with take(n). Also, avoid redundant cloning of large collections, pass references to iter() instead.
Finally, watch out for empty iterators when using methods like sum() or max(), which might return unexpected types or None:
let empty: Vec<i32> = vec![]; let max = empty.iter().max(); // Returns None
Check for empty collections before calling such methods.
Read the full article here: https://medium.com/rustaceans/iterators-e9199987811f