11. Closures and Iterators

📋 Jump to Takeaways

🎁 Write .map().filter().collect() and it reads like English — yet the compiler optimizes it into the same machine code as a hand-written for loop. No heap allocations, no virtual dispatch, no overhead. This is Rust's iterator system, and once you learn it, you'll never want to write a manual loop again.

Closure Syntax

A closure is an anonymous function you can store in a variable or pass as an argument. You write parameters between pipes instead of parentheses.

fn main() {
    let add_one = |x| x + 1;
    let multiply = |x, y| x * y;

    println!("{}", add_one(5));       // 6
    println!("{}", multiply(3, 4));   // 12

    // Multi-line closures use braces
    let calculate = |x: i32| {
        let doubled = x * 2;
        doubled + 10
    };
    println!("{}", calculate(5));     // 20
}

Unlike functions, closures infer their parameter and return types from usage. You can annotate them explicitly, but you rarely need to.

Capturing Variables

Closures can capture variables from their surrounding scope. This is what makes them more powerful than plain functions.

fn main() {
    let name = String::from("Rust");
    let greeting = || println!("Hello, {}!", name); // borrows name
    greeting(); // Hello, Rust!
    println!("{}", name); // still valid — closure only borrowed it
}

Rust determines how a closure captures based on what the closure body does. This maps to three traits:

  • Fn — borrows captured values immutably. Can be called many times.
  • FnMut — borrows captured values mutably. Can be called many times but needs mut access.
  • FnOnce — takes ownership of captured values. Can only be called once.
fn main() {
    let mut count = 0;

    // FnMut — mutates captured variable
    let mut increment = || { count += 1; };
    increment();
    increment();
    println!("count: {}", count); // count: 2

    let message = String::from("goodbye");

    // FnOnce — consumes captured variable
    let consume = || {
        let m = message; // moves message into closure body
        println!("{}", m);
    };
    consume(); // goodbye
    // consume(); // ERROR: can't call again, message was moved
}

The move Keyword

Sometimes you need the closure to own the captured data — especially when sending it to another thread. Use move before the pipes.

fn main() {
    let data = vec![1, 2, 3];

    let owns_data = move || {
        println!("{:?}", data);
    };

    owns_data(); // [1, 2, 3]
    // println!("{:?}", data); // ERROR: data was moved into the closure
}

With move, all captured variables are moved into the closure regardless of how the body uses them.

The Iterator Trait

The Iterator trait has one required method: next(), which returns Option<Self::Item>.

fn main() {
    let nums = vec![10, 20, 30];
    let mut iter = nums.iter();

    println!("{:?}", iter.next()); // Some(10)
    println!("{:?}", iter.next()); // Some(20)
    println!("{:?}", iter.next()); // Some(30)
    println!("{:?}", iter.next()); // None
}

Iterators are lazy — they do nothing until you consume them. This is what enables the compiler to fuse the entire chain into a single loop.

Creating Iterators

You have three ways to create an iterator from a collection:

  • .iter() — iterates over &T (immutable references)
  • .iter_mut() — iterates over &mut T (mutable references)
  • .into_iter() — iterates over T (takes ownership)
fn main() {
    let mut names = vec!["Alice", "Bob", "Carol"];

    // Immutable references
    for name in names.iter() {
        println!("{}", name);
    }

    // Mutable references
    for name in names.iter_mut() {
        *name = "Modified";
    }

    // Takes ownership — names is consumed
    let collected: Vec<&str> = names.into_iter().collect();
    println!("{:?}", collected); // ["Modified", "Modified", "Modified"]
}

Iterator Adaptors

Adaptors transform an iterator into another iterator. They're lazy — nothing happens until you consume the result.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // .map — transform each element
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

    // .filter — keep elements matching a predicate
    let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
    println!("{:?}", evens); // [2, 4, 6, 8, 10]

    // .enumerate — attach index to each element
    for (i, val) in numbers.iter().enumerate().take(3) {
        println!("index {}: {}", i, val); // index 0: 1, index 1: 2, index 2: 3
    }

    // .zip — pair elements from two iterators
    let letters = vec!['a', 'b', 'c'];
    let zipped: Vec<_> = numbers.iter().zip(letters.iter()).collect();
    println!("{:?}", zipped); // [(1, 'a'), (2, 'b'), (3, 'c')]

    // .skip and .chain
    let skipped: Vec<&i32> = numbers.iter().skip(7).collect();
    println!("{:?}", skipped); // [8, 9, 10]

    let extra = vec![11, 12];
    let chained: Vec<&i32> = numbers.iter().chain(extra.iter()).skip(8).collect();
    println!("{:?}", chained); // [9, 10, 11, 12]
}

Consumers

Consumers drive the iterator and produce a final value. Once consumed, the iterator is gone.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let total: i32 = numbers.iter().sum();
    println!("sum: {}", total); // sum: 15

    let count = numbers.iter().filter(|&&x| x > 2).count();
    println!("count > 2: {}", count); // count > 2: 3

    let has_even = numbers.iter().any(|x| x % 2 == 0);
    println!("has even: {}", has_even); // has even: true

    let all_positive = numbers.iter().all(|x| *x > 0);
    println!("all positive: {}", all_positive); // all positive: true

    let first_big = numbers.iter().find(|&&x| x > 3);
    println!("first > 3: {:?}", first_big); // first > 3: Some(4)

    // .fold — accumulate with custom logic
    let product = numbers.iter().fold(1, |acc, x| acc * x);
    println!("product: {}", product); // product: 120
}

Method Chaining

The real power emerges when you chain multiple adaptors together. Each step is clear, composable, and zero-cost.

fn main() {
    let words = vec!["hello", "world", "rust", "is", "fast"];

    let result: String = words
        .iter()
        .filter(|w| w.len() > 3)
        .map(|w| w.to_uppercase())
        .collect::<Vec<String>>()
        .join(", ");

    println!("{}", result); // HELLO, WORLD, RUST, FAST
}

You can read this top to bottom: take the words, keep those longer than 3 characters, uppercase them, collect into a vector, join with commas.

Zero-Cost Abstraction

Rust's iterator chains compile to the same code as hand-written loops. The compiler inlines the closures and fuses the adaptor chain into a single pass — no intermediate allocations, no function pointer indirection.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // This iterator chain...
    let sum_a: i32 = numbers.iter().filter(|&&x| x % 2 == 0).map(|x| x * x).sum();

    // ...compiles to the same machine code as this loop:
    let mut sum_b = 0;
    for &x in &numbers {
        if x % 2 == 0 {
            sum_b += x * x;
        }
    }

    println!("{} == {}", sum_a, sum_b); // 220 == 220
}

The iterator version is more readable, more composable, and equally fast. This is what "zero-cost abstraction" means in Rust.

Key Takeaways

  • Closures use |params| body syntax and infer types from context
  • Fn borrows immutably, FnMut borrows mutably, FnOnce consumes captured values
  • move forces a closure to take ownership of all captured variables
  • Iterators are lazy — adaptors build a pipeline, consumers execute it
  • .iter() borrows, .iter_mut() borrows mutably, .into_iter() consumes
  • Chain adaptors like .map(), .filter(), .enumerate(), .zip() freely
  • Consumers like .collect(), .sum(), .fold(), .find() drive execution
  • Iterator chains compile to the same machine code as manual loops — zero-cost abstraction

🎁 Next up: threads that physically cannot have data races — because the Rust compiler won't let you share mutable data across threads without proving it's safe. Welcome to fearless concurrency.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy