In JavaScript, besides using loops like for to iterate through an array, there are many ways to do the same thing. For example, iterating through an array of numbers.
const arr = [1, 2, 3, 4];
arr.map(fn());
arr.filter(fn());
arr.reduce(fn());
These are methods implemented for iterable data types. In Rust, we also have a similar concept: Iterator.
The Iterator is responsible for the logic of iterating through each item and determining when the sequence ends, helping programmers not to have to re-implement this logic. Iterators in Rust are created by calling methods like iter() on data structures (for example: Vec<T>). Iterators are lazy, meaning they do not take effect until you call consuming methods on them.
For example:
fn main() {
let v1 = vec![6-8];
let v1_iter = v1.iter(); // Create iterator
for val in v1_iter { // The for loop implicitly consumes the iterator
println!("Got: {val}");
}
}
All Iterators implement the Iterator trait defined in the standard library. The structure of the Iterator trait looks like this:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
When a function uses Iterator, it calls the next function to iterate through the elements. Or it can directly call next if it is implementing the handling function itself.
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
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);
}
Note that calling next will change the internal state that the iterator uses to track its position in the sequence. In other words, it consumes one item of the iterator until it is completed, and that iterator can no longer be accessed.
The values returned from next when using the iter() method are immutable references to the values in the vector. If you want to take ownership of the values, you use into_iter(). If you want to iterate over mutable references, use iter_mut().
There are 2 types of functions using Iterators in 2 different ways, one type consumes the iterator completely, and the other does not consume it, instead, it creates a new iterator and processes the data on it. For example, sum consumes the iterator, and if you call sum, you will not be able to use the previously created iterator again.
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
}
Other methods like map, filter do not consume but instead create new iterators that have been transformed. However, since all iterators are lazy, you need to call a consuming adapter (like collect) to actually get the results from the chain of iterators.
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
In the example above, you see the appearance of a closure inside the map function. Many iterator adapters accept closures as arguments. Using closures allows you to customize behavior while still reusing the looping behavior that the Iterator trait provides.
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter() // Create iterator, take ownership of the vector
.filter(|s| s.size == shoe_size)
.collect() // Consume the iterator and collect the results into Vec
}
The filter function accepts a closure that returns bool. If the closure returns true, the value will be retained during the iteration.
let v1: Vec<i32> = vec![6-8];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
Another example, the map method accepts a closure to call on each item, creating a new iterator with the transformed items.
At this point, you may notice that the usage of map and filter is quite similar to JavaScript, right!
The performance between using a for loop and using Iterators is equivalent. Iterators are one of Rust's zero-cost abstractions, meaning that using this abstraction mechanism incurs no additional runtime cost.
Since there is no performance penalty when using iterators, Rust programmers often prefer the iterator style because it is clearer and easier to understand once you are familiar with the different adapters. The iterator style helps the code focus on the high-level goals of the loop, abstracting away the repetitive details.
Iterators in Rust not only provide a flexible and convenient approach to traversing data structures but also ensure optimal performance thanks to the zero-cost abstraction mechanism. Compared to traditional loops like for, Iterators help programmers focus on higher-level goals rather than repetitive loop details. With the ability to create new Iterators through adapters like map and filter, and the use of closures to customize behavior on each element, Rust brings a powerful and clear programming style similar to the array processing methods in JavaScript.
The distinction between different types of Iterators like iter, into_iter, and iter_mut helps in managing ownership and immutability of data flexibly. The combination of consuming and non-consuming functions allows programmers to have more control over data handling. Iterators are not only a powerful tool but also an essential part of writing efficient, clear, and maintainable code in Rust.