Skip to content
DocsRust LearningadvancedClosures & Iterators
Chapter 12 of 19·advanced·9 min read

Closures & Iterators

Closure & Iterator

Functional programming patterns

Hover or tap any paragraph to see Vietnamese translation

Closure Syntax and Type Inference

Closures are anonymous functions you can store in a variable or pass as an argument. Unlike regular functions, closures can capture values from the scope around them. Rust infers the types of a closure's parameters and return value in most cases.

closure_syntax.rs
1fn main() {2    // Regular function3    fn add_one_fn(x: i32) -> i32 { x + 1 }45    // Equivalent closure — types inferred6    let add_one = |x| x + 1;78    // Closure with explicit types (verbose, rarely needed)9    let add_one_typed = |x: i32| -> i32 { x + 1 };1011    // Multi-statement closure with braces12    let print_and_return = |x: i32| {

Capturing Variables (Fn, FnMut, FnOnce)

Closures can capture variables from their surrounding environment in three ways: immutable borrow (Fn), mutable borrow (FnMut), or taking ownership (FnOnce). The compiler automatically determines the minimum capture mode needed.

closure_capture.rs
1fn main() {2    // Fn: borrows immutably — can be called multiple times3    let message = String::from("hello");4    let print_msg = || println!("{message}");5    print_msg(); // hello6    print_msg(); // hello — still works7    println!("{message}"); // message still accessible89    // FnMut: borrows mutably — can be called multiple times10    let mut count = 0;11    let mut increment = || {12        count += 1;
Info
Fn is a subtrait of FnMut, which is a subtrait of FnOnce. A closure that implements Fn also implements FnMut and FnOnce. Use Fn bounds unless you need mutation or consumption.

Closures as Function Parameters

When a function accepts a closure as a parameter you use Fn, FnMut, or FnOnce as a generic trait bound. This is how iterator methods like map and filter accept callbacks.

closure_params.rs
1// Accept a closure that takes i32 and returns bool2fn apply_filter(numbers: &[i32], predicate: impl Fn(i32) -> bool) -> Vec<i32> {3    numbers.iter().filter(|&&x| predicate(x)).copied().collect()4}56// Accept a closure that can mutate state (FnMut)7fn apply_n_times<F: FnMut()>(mut f: F, n: usize) {8    for _ in 0..n { f(); }9}1011// Accept a closure that consumes its captures (FnOnce)12fn run_once<F: FnOnce() -> String>(f: F) -> String {

Returning Closures

Because closures have anonymous types you cannot return them using a concrete type. Two options: impl Fn in return position (static dispatch) or Box<dyn Fn> (dynamic dispatch, needed when the return type depends on runtime conditions).

returning_closures.rs
1// Return impl Fn — same concrete closure type always returned2fn make_adder(x: i32) -> impl Fn(i32) -> i32 {3    move |n| n + x4}56// Return impl Fn with captured state7fn make_greeting(prefix: String) -> impl Fn(&str) -> String {8    move |name| format!("{prefix}, {name}!")9}1011// Return Box<dyn Fn> — when the closure type varies at runtime12fn make_operation(double: bool) -> Box<dyn Fn(i32) -> i32> {

The Iterator Trait and next()

The Iterator trait is deeply integrated into Rust. Any type implementing Iterator can be used with for loops and the entire ecosystem of adaptors. The core protocol has just one method: next().

iterator_trait.rs
1// The Iterator trait definition:2// pub trait Iterator {3//     type Item;4//     fn next(&mut self) -> Option<Self::Item>;5//     // ~70 default methods built on next()6// }78struct Counter {9    count: u32,10    max: u32,11}12

Iterator Adaptors (map, filter, zip, chain)

Iterator adaptors are methods that take an iterator and return a different iterator. They are lazy — they perform no work until the iterator is consumed. You can chain many adaptors together.

iterator_adaptors.rs
1fn main() {2    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];34    // map: transform each element5    let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();6    println!("{doubled:?}"); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]78    // filter: keep elements matching predicate9    let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();10    println!("{evens:?}"); // [2, 4, 6, 8, 10]1112    // chain: concatenate two iterators

Consuming Adaptors (collect, sum, fold)

Consuming adaptors are methods that call next() to exhaust the iterator and produce a final value. Every iterator pipeline must end with a consuming adaptor.

consuming_adaptors.rs
1fn main() {2    let numbers = vec![1, 2, 3, 4, 5];34    // collect: materialize into any collection5    let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();6    let as_set: std::collections::HashSet<i32> = numbers.iter().copied().collect();78    // sum and product9    let total: i32 = numbers.iter().sum();10    let product: i32 = numbers.iter().product();11    println!("sum={total}, product={product}"); // 15, 12012

Creating Custom Iterators

Implementing the Iterator trait for your struct lets it participate in the entire iterator ecosystem: for loops, adaptors, consuming methods — all for free just by implementing next().

custom_iterator.rs
1struct Fibonacci {2    curr: u64,3    next: u64,4}56impl Fibonacci {7    fn new() -> Self {8        Fibonacci { curr: 0, next: 1 }9    }10}1112impl Iterator for Fibonacci {

Performance: Iterators vs Loops (Zero-Cost Abstractions)

Rust guarantees that iterators perform equivalently to hand-written loops. The compiler completely optimizes away the abstraction overhead — this is what Rust calls a zero-cost abstraction.

performance.rs
1fn sum_loop(v: &[f64]) -> f64 {2    let mut total = 0.0;3    for &x in v {4        total += x;5    }6    total7}89fn sum_iter(v: &[f64]) -> f64 {10    v.iter().sum()11}12

When to Use What

  • Use iterator adaptors for pipeline transformations — more concise and expressive
  • Use for loops when you need index access or complex control flow (break with value, continue)
  • Use while let for manual consumption or complex exit conditions
  • Chain adaptors instead of multiple loops — one pass through the data is faster
  • collect() can produce any collection — Vec, HashSet, HashMap, String
  • Use iter() for immutable refs, iter_mut() for mutable refs, into_iter() to consume
Tip
The beauty of Rust iterators: you get expressive, readable code and pay zero performance cost compared to hand-written C.

Key Takeaways

Điểm Chính

  • Closures capture variables from their enclosing scopeClosure bắt giữ biến từ phạm vi bao quanh
  • Fn, FnMut, and FnOnce determine how closures capture valuesFn, FnMut và FnOnce xác định cách closure bắt giữ giá trị
  • Iterator adapters like map and filter are lazy until consumedAdapter iterator như map và filter lười cho đến khi được tiêu thụ
  • Iterator chains compile to the same performance as manual loopsChuỗi iterator biên dịch thành hiệu suất tương đương vòng lặp thủ công

Practice

Test your understanding of this chapter

Quiz

Which closure trait is required when a closure takes ownership of a captured variable and can only be called once?

Trait closure nào được yêu cầu khi một closure lấy quyền sở hữu biến bắt giữ và chỉ có thể được gọi một lần?

Quiz

Iterator adaptors like map and filter are described as lazy. What does this mean in practice?

Các bộ điều hợp iterator như map và filter được mô tả là lười biếng. Điều này có nghĩa là gì trong thực tế?

True or False

Fn is a subtrait of FnMut and FnMut is a subtrait of FnOnce, so a closure that implements Fn also satisfies FnMut and FnOnce bounds.

Fn là subtrait của FnMut và FnMut là subtrait của FnOnce, vì vậy một closure triển khai Fn cũng thỏa mãn các ràng buộc FnMut và FnOnce.

True or False

Rust iterator chains such as .filter().map().collect() are less performant than equivalent hand-written for loops because of abstraction overhead.

Chuỗi iterator Rust như .filter().map().collect() kém hiệu quả hơn các vòng lặp for viết tay tương đương do chi phí abstraction.

Code Challenge

Complete the lazy iterator pipeline

Hoàn thành pipeline iterator lười biếng

let result: Vec<i32> = (1..=20)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .(5)
    .collect();
// result == [4, 16, 36, 64, 100]

Chapter Complete!

Great job! Keep the momentum going.

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM