Rust - Iterator and Closure
Rust: Iterators and Closures
Section titled “Rust: Iterators and Closures”Iterators and closures are two of Rust’s most powerful features, heavily influenced by functional programming. They allow you to write concise, expressive, and efficient code for processing sequences of data.
Iterators: Processing a Sequence of Items
Section titled “Iterators: Processing a Sequence of Items”An iterator is a pattern that allows you to perform some action on a sequence of items in turn. In Rust, an iterator is any type that implements the Iterator trait from the standard library. The core of this trait is the next method:
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // ... many other methods with default implementations}The next() method returns each item from the sequence wrapped in Some, and when the sequence is over, it returns None. Let’s see how to create an iterator from a vector and use it manually.
fn main() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); // Create an iterator
// Using next() manually 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);}Types of Iterators
Section titled “Types of Iterators”Collections typically offer three methods to create an iterator, depending on how they handle ownership of the data:
| Method | Description | Item Type |
|---|---|---|
iter() | Creates an iterator that immutably borrows each element. The original collection remains usable. | &T |
into_iter() | Creates an iterator that takes ownership of the collection and its elements. The collection is consumed. | T |
iter_mut() | Creates an iterator that mutably borrows each element, allowing for in-place modification. | &mut T |
The Power of Iterator Adapters
Section titled “The Power of Iterator Adapters”The real power of iterators comes from their adapter methods. These are methods that transform one iterator into another. Because they are lazy, they incur no performance overhead until you consume the iterator.
fn main() { let v1: Vec<i32> = vec![1, 2, 3];
// .map() takes a closure and creates an iterator that calls that closure on each element. // .collect() is a consuming adapter that gathers the results into a collection. let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
println!("Original vector: {:?}", v1); // v1 is still available! println!("New vector: {:?}", v2);
assert_eq!(v2, vec![2, 3, 4]);}Output
Section titled “Output”Original vector: [1, 2, 3]New vector: [2, 3, 4]Closures: Anonymous Functions That Capture Their Environment
Section titled “Closures: Anonymous Functions That Capture Their Environment”Closures are anonymous functions you can save in a variable or pass as arguments to other functions. Unlike functions, closures can capture values from the scope in which they’re defined.
Syntax
Section titled “Syntax”A closure’s definition is lightweight, using vertical bars (|) around the parameters, followed by the body. Type annotations are usually optional.
fn main() { // A simple closure that takes one argument let add_one = |x: u32| -> u32 { x + 1 }; println!("3 + 1 = {}", add_one(3));
// Type annotations are often optional, Rust can infer them. let add_two = |x| x + 2; println!("10 + 2 = {}", add_two(10));}Capturing the Environment
Section titled “Capturing the Environment”Closures can ‘borrow’ or ‘take’ variables from their surrounding scope. Rust infers how to do this based on how the variables are used inside the closure, choosing between immutable borrowing, mutable borrowing, or taking ownership. This is governed by the Fn, FnMut, and FnOnce traits.
fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list);
// This closure borrows `list` immutably (Fn trait). let prints_list = || println!("From closure: {:?}", list);
prints_list(); println!("After calling closure: {:?}", list); // `list` is still usable}Combining Iterators and Closures: A Perfect Match
Section titled “Combining Iterators and Closures: A Perfect Match”The true magic happens when you combine iterators and closures. Most iterator adapters, like map, filter, and fold, take closures as arguments to define their behavior.
#[derive(Debug, PartialEq)]struct Shoe { size: u32, style: String,}
fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter() // Take ownership of the vector .filter(|s| s.size == shoe_size) // Use a closure to filter by size .collect() // Collect the results into a new vector}
fn main() { let shoes = vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 13, style: String::from("sandal") }, Shoe { size: 10, style: String::from("boot") }, ];
let my_size_shoes = shoes_in_my_size(shoes, 10);
println!("Shoes in my size: {:?}", my_size_shoes);}Output
Section titled “Output”Shoes in my size: [Shoe { size: 10, style: "sneaker" }, Shoe { size: 10, style: "boot" }]