Course outline

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.

About 5 min readContent reviewed
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.

rust A lazy description collected into owned values
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());
}
Expected output
[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.

rust An owning closure that remains reusable
fn main() {
    let prefix = String::from("item");
    let label = move |id: u32| format!("{prefix}-{id}");
    println!("{} {}", label(3), label(4));
}
Expected output
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.

Official references