Iterators turn "loop over a collection and do something" into a pipeline. They're lazy: nothing actually runs until you ask for a result. The compiler usually fuses chained iterator calls into a single tight loop, so the abstraction is free at runtime.
Here's how iterators work in practice:
.iter(), .into_iter(), .iter_mut(), or directly from .chars(), .lines(), etc..map(...), .filter(...), .take(...).
These are lazy..collect(), .sum(), .count(), .any(...), or a for loop.let names = vec!["alice", "ADMIN", "bob"];
let active: Vec<String> = names
.iter() // &&str
.filter(|n| n.starts_with('a')) // keep some
.map(|n| n.to_lowercase()) // transform
.collect(); // back to Vec<String>
// active == ["alice"]
The three "iter" methods differ in what they yield:
.iter() yields &T (immutable references).
Use when reading..iter_mut() yields &mut T.
Use when modifying in place..into_iter() yields T (consumes the collection).
Use when you don't need the original anymore.Some adapters change the item type.
After .map(|n| n.to_lowercase()), the items are Strings, not &&strs.
The compiler infers types through the chain, so trust it: write the chain, then add a type annotation on the binding if needed.
.collect() is interesting: it can produce many different collections.
Tell it which one with a type annotation: Vec<_>, HashMap<_, _>, String.
The _ lets the compiler fill in the inner types.
Remember the three little functions from the word count chapter's exercise break?
Each one was a counter, a for loop, and a return.
With iterators, the whole trio shrinks to:
fn word_count(text: &str) -> usize { text.split_whitespace().count() }
fn char_count(text: &str) -> usize { text.chars().count() }
fn longest_word(text: &str) -> usize {
text.split_whitespace().map(|w| w.chars().count()).max().unwrap_or(0)
}
The loops, the mut counters, the running-maximum bookkeeping: all gone.
That's the payoff for spending a chapter on iterators: every "walk a collection and reduce it to one number, or to one new collection" problem gets shorter and harder to get wrong.
A running total is a good first place to see what an iterator consumer does. Rust's iterators are lazy, so they don't do any work until you ask for a result.
You could add the values with a for loop and an accumulator.
Here, sum asks the iterator for each value and collapses the sequence into one total.
Useful from the standard library
<[T]>::iterproduces an iterator of shared references over the slice.Iterator::sumreduces a numeric iterator to a single total. It's generic over the output type, so the compiler needs a hint: either annotate the binding (let total: i32 = ...) or use the turbofish (.sum::<i32>()).Iterator::productis the multiplicative cousin if you ever need a running product.
sales.iter().sales.iter().sum().
And you'll need a type annotation (let total: i32 = β¦, or .sum::<i32>()) because sum is generic.
/// Calculates total revenue from sales data.
///
/// The simplest iterator pattern: take a sequence, produce one number.
/// You could write a `for` loop with a running total, but the standard
/// library can collapse a numeric iterator down for you in one call.
/// See: <https://doc.rust-lang.org/std/iter/trait.Iterator.html>
fn calculate_total_revenue() -> i32 {
let sales = [1200, 850, 2300, 950, 1800, 3200, 1100, 2800];
sales.iter().sum()
}
#[test]
fn test_calculate_total_revenue() {
let total = calculate_total_revenue();
assert_eq!(total, 14200); // Sum of all sales
}
Now you need to transform every element instead of collapsing the sequence. Read this pipeline from left to right: take ownership of the vector's items, transform each one, then collect the results into a new vector.
map is lazy: it just describes the transformation.
Nothing runs until collect (or another consumer) asks for the results.
Useful from the standard library
Vec::into_iterconsumes the vec and yields owned items. That's what lets the closure call.to_lowercase()on aStringdirectly.Iterator::mapapplies a closure to each item and produces a new iterator with the transformed items.Iterator::collectturns the pipeline back into a collection. The return type (Vec<String>) tellscollectwhich collection to produce.str::to_lowercasereturns a freshStringwith the case folded.
into_iter() (consume the input vec) β map(...) β collect().String.
Call .to_lowercase() on it.
/// Normalizes email addresses to lowercase.
///
/// Now you need to transform every element instead of collapsing the
/// sequence. The pattern is `vec.into_iter()` -> some combinator that
/// applies a closure -> back to a `Vec` via `collect()`.
/// See: <https://doc.rust-lang.org/std/string/struct.String.html#method.to_lowercase>
fn normalize_emails(emails: Vec<String>) -> Vec<String> {
emails
.into_iter()
.map(|email| email.to_lowercase())
.collect()
}
#[test]
fn test_normalize_emails() {
let emails = vec!["Alice@EXAMPLE.COM".to_string(), "BOB@test.ORG".to_string()];
let normalized = normalize_emails(emails);
assert_eq!(normalized, vec!["alice@example.com", "bob@test.org"]);
}
With map you transformed every item, while filter keeps some items and drops the rest.
There is one borrowing detail to watch: .iter() yields &T, but filter gives its closure another reference on top, so the closure sees &&T.
That's why you'll often see **c == ... or s.starts_with(...) (which auto-derefs) instead of plain c == ....
If the compiler reports a missing &, the iterators entry in the cheatsheet shows the reference layers side by side.
Useful from the standard library
Iterator::filterkeeps only items where the predicate returnstrue. The closure receives a reference to the item, regardless of whether the iterator yields owned values or borrows.str::starts_withtakes achar(or another&str) and answers yes/no. Method-call syntax auto-derefs through the extra reference.collect()here picksVec<&str>straight from the return type. No turbofish needed.
into_iter() β filter(...) β collect().filter's closure takes a reference to each item.
Since the iterator yields &str, the closure parameter is &&str.
Method calls auto-deref, so |s| s.starts_with('a') Just Works.
/// Returns all users whose usernames start with 'a'.
///
/// Same idea, but instead of transforming each element you keep some
/// and drop others. Watch out for one borrowing gotcha: the closure
/// receives a reference to each element, not the element itself.
/// See: <https://doc.rust-lang.org/std/primitive.str.html#method.starts_with>
fn select_usernames_starting_with_a(usernames: Vec<&str>) -> Vec<&str> {
usernames
.into_iter()
.filter(|username| username.starts_with('a'))
.collect()
}
#[test]
fn test_select_usernames_starting_with_a() {
let users = vec!["alice", "admin", "bob", "anonymous", "charlie"];
let active = select_usernames_starting_with_a(users);
assert_eq!(active, vec!["alice", "admin", "anonymous"]);
}
This time the input is a &[&str], a borrowed slice of borrowed strings, so the iterator yields &&str.
We sidestep that double-reference by returning owned Strings; the lesson here is iterators, not lifetimes.
str::to_string converts each surviving &&str into an owned String through auto-deref.
Chain it after your filter with a map, then collect into a Vec.
Useful from the standard library
Iterator::filteragain. Same closure structure; auto-deref still saves you for.ends_with(".rs").Iterator::mapis what converts the surviving&&strs into ownedStrings.str::to_stringconverts a borrowed string slice into an ownedString. Auto-deref reaches through the extra reference for you.str::ends_withis the suffix check used by the predicate.
&&&str.
Auto-deref still saves you for .ends_with(".rs").Vec<String>, not Vec<&str>.
Add a .map(...) step that converts each &&str into an owned String.
/// Finds all files with ".rs" extension.
///
/// Same idea as the previous one, but the input is a `&[&str]` (a
/// borrowed slice of borrowed strings), so the iterator yields `&&str`.
/// We sidestep that double-reference by returning owned `String`s; the
/// lesson here is iterators, not lifetimes. To go from `&&str` to
/// `String`, reach for [`str::to_string`].
fn find_rust_files(files: &[&str]) -> Vec<String> {
files
.iter()
.filter(|file| file.ends_with(".rs"))
.map(|file| file.to_string())
.collect()
}
#[test]
fn test_find_rust_files() {
let files = &[
"main.rs",
"README.md",
"lib.rs",
"package.json",
"config.rs",
];
let rust_files = find_rust_files(files);
assert_eq!(rust_files, vec!["main.rs", "lib.rs", "config.rs"]);
}
You collapsed a numeric vector with sum, transformed every element with map, kept just the matching ones with filter, and combined filter with map to convert borrowed slices into owned strings.
What we learned
- An iterator pipeline starts with
.iter(),.iter_mut(),.into_iter(), or a method such as.chars()or.lines(). Lazy adapters describe what should happen to each item. A consumer finishes the pipeline by asking for results.iteryields&T,iter_mutyields&mut T,into_itermoves out of the collection and yieldsT. Pick the one that matches what you intend to do with each item.- Adapters (
map,filter,take,skip, ...) describe the pipeline but do nothing on their own. The actual work happens when a consumer (collect,sum,count,forloop) asks for results.collectis generic over the target collection. The return type (or a turbofish like.collect::<Vec<_>>()) tells it what to build.sumandproductneed to know their output type. Annotate the binding or use.sum::<i32>()to keep the compiler happy.filter's closure always takes&T, so on a&striterator you'll see&&str. Method calls auto-deref, so.starts_with(...)Just Works; comparison operators sometimes need an explicit*.- The
|x| ...syntax you've been seeing is a closure: an anonymous function passed as an argument. Iterators are where closures earn their keep.