Rust / Intermediate / Lesson 11
Iterators, closures, and ownership
Compose lazy iteration with explicit ownership, choose closure captures deliberately, and collect only when a consumer needs an owned result.
On this page
Lesson goals
- Distinguish iter, iter_mut, and into_iter for owned collections.
- Explain lazy adapters, terminal consumers, and collect.
- Relate capture mode to Fn, FnMut, and FnOnce behavior.
Know what each step yields
Iterator has an associated Item type and a next method returning Option<Item>. For an owned Vec<T>, iter borrows shared elements, iter_mut borrows mutable elements, and into_iter consumes the vector and yields owned elements. The same method spelling on a reference can select a different implementation, so inspect the receiver type. A for loop uses IntoIterator and can hide this choice if ownership is not considered. copied is useful for Copy elements, while cloned performs the element type Clone operation. Neither should be added just to make an unexplained type error disappear.
Adapters describe work until consumption
map and filter return new iterators and generally perform element work when a consumer requests values. collect consumes the pipeline and builds a chosen collection. The first example borrows numbers, copies small integers, retains even values, triples them, and collects an owned vector. The original vector remains usable afterward because iter did not transfer it. The filter predicate receives a reference to its candidate item, even after copied yields integers; closures can often use operator coercions without explicit dereferencing. Review Item at each stage when a more complex chain becomes confusing.
fn main() {
let numbers = vec![1u32, 2, 3, 4];
let results: Vec<u32> = numbers.iter().copied()
.filter(|value| value % 2 == 0)
.map(|value| value * 3)
.collect();
println!("{:?} {}", results, numbers.len());
}
[6, 12] 4
Two and four become six and twelve. Collection order follows the source traversal, and numbers still has four elements.
Capture mode and call behavior are different
A closure captures only the environment it needs using borrowing or ownership according to its use and modifiers. move forces capture by value, which can move a String into the closure, but it does not automatically mean the closure can be called only once. FnOnce allows consuming captured state, FnMut permits mutable access during calls, and Fn permits shared access. A closure can implement multiple call traits. The second example owns its prefix yet borrows that prefix when formatting each result, so it can be called repeatedly without cloning the captured String for every call.
fn main() {
let prefix = String::from("item");
let label = move |id: u32| format!("{prefix}-{id}");
println!("{} {}", label(3), label(4));
}
item-3 item-4
prefix is owned by label. Each call formats through shared access and returns a new String.
Do not hide policy in iteration side effects
Consumers such as find, any, and position can stop before visiting every element. A pipeline that performs logging or mutation in map may therefore execute fewer effects than expected. Prefer explicit loops when ordered side effects, early error handling, or mutation of shared state is the central requirement. try_fold and collecting Results can express fallible traversal, but verify the accumulator and short-circuit contract. Do not claim an exact callback count for an abstract pipeline unless the documented operations guarantee it. Numeric transformations still require checked or otherwise deliberate arithmetic for untrusted values.
Stored closures retain lifetime obligations
A closure borrowing a local variable cannot safely escape that variable lifetime. Return an owning closure with suitable captures when later execution is required, or keep the consumer inside the borrowing scope. Moving a reference into a closure moves only the reference, not its referent. Tasks sent to another thread introduce Send requirements and often static bounds in addition to capture lifetimes. When choosing iterator style, consider readability, repeated traversal cost, allocation, and ownership together. A compact chain is useful when it clarifies the data transformation, while a loop is equally idiomatic when it makes the contract clearer.
Common errors and fixes
A pipeline produces no observable work.
- Why it happens
- Lazy adapters were created but never consumed.
- How to fix it
- Use an appropriate terminal consumer or explicit loop.
- Prevent it next time
- Identify who requests iterator items.
The original vector is unavailable afterward.
- Why it happens
- Owned into_iter consumed it.
- How to fix it
- Use iter when shared observation is intended.
- Prevent it next time
- Track the receiver and Item types.
A returned closure borrows a dead local.
- Why it happens
- Its capture was borrowed instead of owned.
- How to fix it
- Capture owned data or shorten the closure usage scope.
- Prevent it next time
- Review captures whenever callbacks escape.
Check your understanding
1. Change the first pipeline to retain odd values and add one. Give the collected vector and explain whether numbers is consumed.
Show worked solution
The retained values one and three become two and four, so results is [2, 4]. numbers still contains four original values because iter borrows and copied copies the small integers. An into_iter call on the owned vector would instead transfer its elements and prevent later use of that vector binding.
2. A move closure returns its captured String directly instead of formatting through it. Explain whether repeated calls remain valid.
Show worked solution
Returning the owned captured String moves it out of the closure, so that closure can only be consumed once through FnOnce. A second call cannot obtain ownership of the same String again. Return a borrow only with an appropriate interface and lifetime, or create an explicit cloned result if independent repeated owned strings are the intended behavior.
Key takeaways
- Inspect Item and ownership at every iterator stage.
- Lazy descriptions need a consumer.
- move capture does not by itself imply one call.