Intermediate Level
Iterators and Closures
Chapter 15: Iterators š
Process sequences of data efficiently with iterators in Rust.
Understanding Iterators
What are Iterators?
Iterators are objects that can traverse through a collection of items one at a time. In Rust, iterators are lazy, meaning they don't do anything until you call methods that consume or transform them.
fn main() {
let v1 = vec![1, 2, 3];
// Creating an iterator doesn't execute anything
let v1_iter = v1.iter();
// Consuming the iterator executes the iteration
for val in v1_iter {
println!("Got: {}", val);
}
}Iterator Methods
Iterator Adapters
Iterator adapters are methods that produce new iterators:
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
// map creates a new iterator
let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();
println!("{:?}", v2); // [2, 3, 4]
}Consuming Adapters
Consuming adapters use up the iterator:
fn main() {
let v1 = vec![1, 2, 3];
// sum consumes the iterator
let total: i32 = v1.iter().sum();
println!("Total: {}", total); // 6
}Common Iterator Methods
filter
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let even_numbers: Vec<i32> = numbers
.into_iter()
.filter(|x| x % 2 == 0)
.collect();
println!("{:?}", even_numbers); // [2, 4, 6, 8, 10]
}map
fn main() {
let numbers = vec![1, 2, 3];
let squared: Vec<i32> = numbers
.iter()
.map(|x| x * x)
.collect();
println!("{:?}", squared); // [1, 4, 9]
}fold/reduce
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// fold with initial value
let sum = numbers.iter().fold(0, |acc, x| acc + x);
// reduce (similar to fold but without initial value)
let product = numbers.iter().fold(1, |acc, x| acc * x);
println!("Sum: {}", sum); // 15
println!("Product: {}", product); // 120
}Iterator Types
iter(), into_iter(), and iter_mut()
fn main() {
let mut numbers = vec![1, 2, 3];
// iter() - borrows each element
for num in numbers.iter() {
println!("{}", num);
}
// into_iter() - takes ownership of each element
for num in numbers.into_iter() {
println!("{}", num);
}
// iter_mut() - mutably borrows each element
let mut numbers = vec![1, 2, 3];
for num in numbers.iter_mut() {
*num *= 2;
}
println!("{:?}", numbers); // [2, 4, 6]
}Chaining Iterator Methods
Method Chaining
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]
}Practical Examples
Example 1: Processing User Data
#[derive(Debug)]
struct User {
name: String,
age: u32,
active: bool,
}
fn main() {
let users = vec![
User { name: String::from("Alice"), age: 25, active: true },
User { name: String::from("Bob"), age: 30, active: false },
User { name: String::from("Charlie"), age: 35, active: true },
User { name: String::from("Diana"), age: 28, active: true },
];
// Get names of active users over 30
let active_senior_names: Vec<&String> = users
.iter()
.filter(|user| user.active)
.filter(|user| user.age > 30)
.map(|user| &user.name)
.collect();
println!("{:?}", active_senior_names); // ["Charlie"]
// Calculate average age of active users
let active_users: Vec<&User> = users.iter().filter(|user| user.active).collect();
let total_age: u32 = active_users.iter().map(|user| user.age).sum();
let average_age = total_age as f64 / active_users.len() as f64;
println!("Average age of active users: {:.1}", average_age); // 29.3
}Example 2: Text Processing with Iterators
fn main() {
let text = "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.";
// Count words
let word_count = text.split_whitespace().count();
println!("Word count: {}", word_count);
// Find words longer than 5 characters
let long_words: Vec<&str> = text
.split_whitespace()
.filter(|word| word.len() > 5)
.collect();
println!("Long words: {:?}", long_words);
// Convert to uppercase and collect
let uppercase_words: Vec<String> = text
.split_whitespace()
.map(|word| word.to_uppercase())
.collect();
println!("{:?}", uppercase_words);
// Chain multiple operations
let processed_text: String = text
.split_whitespace()
.map(|word| word.to_lowercase())
.filter(|word| !word.starts_with("a"))
.collect::<Vec<String>>()
.join(" ");
println!("Processed text: {}", processed_text);
}Performance Benefits
Zero-Cost Abstractions
Iterator adapters are zero-cost abstractions, meaning they don't impose any runtime overhead:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Iterator approach (compiled to be as fast as loop approach)
let sum: i32 = numbers.iter().sum();
// Loop approach (manual implementation)
let mut total = 0;
for num in &numbers {
total += num;
}
assert_eq!(sum, total);
}Lazy Evaluation
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// This iterator chain is not executed until collect() is called
let processed: Vec<i32> = numbers
.iter()
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.filter(|&x| x > 10)
.collect();
println!("{:?}", processed); // [16]
}Custom Iterators
Implementing the Iterator Trait
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new();
for number in counter {
println!("{}", number);
}
// Using iterator methods
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.take(3)
.sum();
println!("Sum: {}", sum); // 56 (2*3 + 4*5 + 6*7)
}Common Iterator Methods
find
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let found = numbers.iter().find(|&x| x == 3);
match found {
Some(value) => println!("Found: {}", value),
None => println!("Not found"),
}
}any/all
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let has_even = numbers.iter().any(|&x| x % 2 == 0);
let all_positive = numbers.iter().all(|&x| x > 0);
println!("Has even number: {}", has_even); // true
println!("All positive: {}", all_positive); // true
}enumerate
fn main() {
let words = vec!["Hello", "World", "Rust"];
for (index, word) in words.iter().enumerate() {
println!("{}: {}", index, word);
}
}zip
fn main() {
let numbers = vec![1, 2, 3];
let words = vec!["one", "two", "three"];
for (num, word) in numbers.iter().zip(words.iter()) {
println!("{}: {}", num, word);
}
}Common Mistakes
ā Not Collecting Iterator Results
fn main() {
let numbers = vec![1, 2, 3];
// This doesn't actually do anything because iterators are lazy
let squared: Vec<i32> = numbers.iter().map(|x| x * x);
// Error: expected Vec<i32>, found Map<..., ...>
// Need to collect the results
let squared: Vec<i32> = numbers.iter().map(|x| x * x).collect();
}ā Properly Collecting Iterator Results
fn main() {
let numbers = vec![1, 2, 3];
let squared: Vec<i32> = numbers.iter().map(|x| x * x).collect();
println!("{:?}", squared); // [1, 4, 9]
}ā Moving Values When You Don't Intend To
fn main() {
let names = vec![String::from("Alice"), String::from("Bob")];
// This moves the strings out of the vector
for name in names.into_iter() {
println!("{}", name);
}
// This won't compile because names has been moved
// println!("{:?}", names);
}ā Borrowing When You Want to Keep the Data
fn main() {
let names = vec![String::from("Alice"), String::from("Bob")];
// This borrows the strings
for name in names.iter() {
println!("{}", name);
}
// This works because we only borrowed
println!("{:?}", names);
}Key Takeaways
- ā Iterators are lazy and don't execute until consumed
- ā Iterator adapters (map, filter, etc.) don't modify the original collection
- ā Consuming adapters (collect, sum, etc.) actually execute the iteration
- ā Method chaining allows for expressive data processing pipelines
- ā Iterators are zero-cost abstractions with no runtime overhead
- ā
Use
iter(),into_iter(), anditer_mut()appropriately - ā Custom iterators can be created by implementing the Iterator trait
- ā Follow Rust naming conventions and idioms for iterator usage
Ready for Chapter 16? ā Smart Pointers