Iterators (a LINQ → Rust map)
If you write C#, you already think in LINQ: Select, Where, Aggregate, ToList.
Rust's iterator adaptors are the same idea — lazy, chainable transformations over a
sequence — with a different vocabulary and much stronger performance guarantees. This
module assumes you have just seen closures (module 35), because every adaptor takes a
closure, exactly like LINQ takes a Func<>/lambda.
LINQ → Rust cheat sheet
| LINQ (C#) | Rust iterator | Notes |
|---|---|---|
Select(x => ...) | .map(|x| ...) | transform each element |
Where(x => ...) | .filter(|x| ...) | keep elements where predicate is true |
ToList() / ToArray() | .collect::<Vec<_>>() | materialize into a collection |
Sum() | .sum() | terminal; needs a type annotation |
Aggregate(seed, (acc,x) => ...) | .fold(seed, |acc, x| ...) | general reduction |
Any(pred) / Any() | .any(pred) / .next().is_some() | short-circuits |
All(pred) | .all(pred) | short-circuits |
First() / FirstOrDefault() | .next() / .find(pred) | next() returns Option, never throws |
OrderBy(key) | .sort_by_key(...) / .sort() | Rust sorts a Vec, not the iterator |
Skip(n) | .skip(n) | drop first n |
Take(n) | .take(n) | keep first n |
SelectMany(x => ...) | .flat_map(|x| ...) | map then flatten |
Count() | .count() | terminal; consumes the iterator |
Distinct() | .collect::<HashSet<_>>() (or dedup on sorted) | no built-in lazy Distinct |
Zip(other) | .zip(other) | pair up two sequences |
Select((x,i) => ...) | .enumerate() | yields (index, value) |
Reverse() | .rev() | needs a DoubleEndedIterator |
Min() / Max() | .min() / .max() | return Option |
In C#: you would write nums.Where(n => n % 2 == 0).Select(n => n * n).Sum().
In Rust: nums.iter().filter(|&&n| n % 2 == 0).map(|n| n * n).sum::<i32>().
Same shape, same mental model.
Lazy adaptors vs. terminal operations
LINQ has deferred execution: a Where(...).Select(...) chain does nothing until you
enumerate it (foreach, ToList, Count, ...). Rust works identically.
- Adaptors are lazy.
map,filter,take,skip,enumerate,zip,flat_map,revall return a new iterator and run no user code yet. - Terminal (consuming) operations drive the chain.
collect,sum,count,fold,for_each,any,all,find,min,maxpull elements through and produce a final value.
let v = vec![1, 2, 3, 4, 5];
// Nothing runs here — no closure is called yet.
let pipeline = v.iter().map(|n| {
println!("mapping {n}"); // would print, but only when pulled
n * 2
});
// Only now does anything execute, one element at a time.
let doubled: Vec<i32> = pipeline.collect();In C#: this is exactly deferred execution — the lambda inside Select doesn't run
until you call ToList()/iterate. Rust just makes the laziness part of the type system:
an unconsumed iterator triggers a #[must_use] warning, so you can't silently forget to
consume it.
Because the chain runs element-by-element, it also short-circuits for free: any,
all, find, and take stop as soon as the answer is known — just like LINQ.
collect() — turning an iterator back into a collection
collect() is ToList/ToArray/ToDictionary rolled into one. It is generic over the
target type, so you must tell Rust what to build — either with a turbofish on the call or
with a type annotation on the binding.
use std::collections::HashMap;
// Into a Vec — annotation on the binding:
let v: Vec<i32> = (1..=5).collect();
// Into a Vec — turbofish on the call:
let v2 = (1..=5).collect::<Vec<i32>>();
// Into a String (iterator of chars):
let shout: String = "rust".chars().map(|c| c.to_ascii_uppercase()).collect();
// Into a HashMap (iterator of (K, V) tuples) — like ToDictionary:
let scores: HashMap<&str, i32> =
[("alice", 1), ("bob", 2)].into_iter().collect();In C#: collect::<Vec<_>>() ≈ ToList(), collect::<HashMap<_,_>>() ≈
ToDictionary(...), and collecting chars into a String ≈ string.Concat(...) or a
StringBuilder. The _ lets the compiler infer the element types.
iter() vs into_iter() vs iter_mut() — borrow, own, mutate
This trio has no LINQ equivalent because C# doesn't have ownership. It decides what the iterator yields and what happens to the source collection.
| Method | Yields | Effect on source |
|---|---|---|
.iter() | &T (shared refs) | borrows; source still usable after |
.iter_mut() | &mut T (mutable refs) | mutably borrows; lets you edit in place |
.into_iter() | T (owned values) | consumes; source is moved away |
let mut v = vec![10, 20, 30];
let sum: i32 = v.iter().sum(); // &i32 — v still usable
for x in v.iter_mut() { *x += 1; } // &mut i32 — edits v in place
let owned: Vec<i32> = v.into_iter().collect(); // i32 — v is consumed here
// v can no longer be used; it was moved.A for x in &v loop desugars to v.iter(), for x in &mut v to v.iter_mut(), and
for x in v to v.into_iter().
In C#: foreach (var x in list) always hands you the element by value/reference
semantics of the type; there is no "this loop consumes the list" concept. In Rust you
choose, and the borrow checker enforces it.
Implementing your own iterator
In C# you write an iterator with yield return (or implement IEnumerator<T>). In Rust
you implement the Iterator trait: pick the element type with type Item and write
next(), which returns Some(value) for each element and None when done.
struct Countdown {
current: u32,
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None
} else {
self.current -= 1;
Some(self.current + 1)
}
}
}
let v: Vec<u32> = Countdown { current: 3 }.collect(); // [3, 2, 1]The big payoff: once you implement next, you get every adaptor for free —
map, filter, take, sum, collect, and the rest are default methods on the
Iterator trait. Your Countdown can immediately do Countdown { current: 10 }.filter(|n| n % 2 == 0).sum::<u32>().
In C#: next() is the manual form of IEnumerator's MoveNext() + Current, and
returning None is the MoveNext() == false case. The default adaptor methods are like
getting all of LINQ's extension methods the moment your type is IEnumerable<T>.
Zero-cost: why this isn't IEnumerable
In C#, IEnumerable<T> is an interface. A LINQ chain allocates an iterator/state-machine
object per operator and makes a virtual (interface-dispatched) MoveNext()/Current call
per element. (It does not box value types — IEnumerable<int> is a reified generic — but
the per-operator allocations and virtual calls are real overhead.)
Rust iterators are monomorphized: each adaptor is a concrete struct, the closures are
inlined, and the whole chain typically compiles down to the same machine code as a
hand-written for loop — no per-operator allocation and no virtual dispatch. This is the
"zero-cost abstraction" promise: the high-level chain costs nothing at runtime versus the
manual loop.
// This chain...
let total: u64 = (1..=1_000).filter(|n| n % 3 == 0).map(|n| n * n).sum();
// ...optimizes to essentially the same code as a plain loop with an accumulator.In C#: the equivalent LINQ allocates iterator objects and dispatches virtually per
element; you reach for a manual loop or Span<T> when you need that performance. In Rust
you keep the readable chain and the loop's performance.
Key takeaway
Rust iterators are LINQ with a different spelling and stronger guarantees: map/filter
are lazy adaptors (deferred execution), and collect/sum/fold/for_each are the
terminal operations that actually run the chain. Use iter() to borrow, iter_mut() to
mutate in place, and into_iter() to consume. collect() rebuilds a Vec, HashMap,
or String once you tell it the target type. Implement Iterator by choosing
type Item and writing next() — and inherit every adaptor for free. Unlike IEnumerable,
the whole pipeline is monomorphized and inlined to zero-cost machine code.