[Rust Series Part 15] Shaping Data Flow with Iterators and Closures

한국어 버전

Loops over collections grow wordy fast. Rust's iterators and closures let you express the same logic in shorter, more declarative pipelines. Here we tour the key Iterator methods, closure syntax, and how they pair together in practice.

Start with Closure Syntax

Closures are anonymous functions that can capture surrounding scope.

fn main() {
    let threshold = 50;
    let is_high = |score: i32| score > threshold;

    println!("Is 70 passing? {}", is_high(70));
}
  • |score: i32| score > threshold is the closure.
  • You can omit types and let the compiler infer them.
  • Capture mode—by move, immutable borrow, or mutable borrow—depends on how the body uses the captured values. Add move explicitly when you need to transfer ownership.

A quick heuristic:

  • Read-only use captures by shared reference.
  • Mutating the capture uses a mutable reference.
  • Passing data out of the closure often requires move.

Core Ideas Behind the Iterator Trait

  • Every iterator implements a next method.
  • Standard collections (Vec, HashMap, Range, …) expose iter, iter_mut, and into_iter constructors.
  • Chainable adapters such as map, filter, take, and collect let you build pipelines.
fn main() {
    let scores = vec![45, 67, 88, 52];
    let high_scores: Vec<_> = scores
        .iter()
        .filter(|score| **score >= 60)
        .map(|score| format!("Pass: {}", score))
        .collect();

    println!("{:?}", high_scores);
}
  • iter() yields immutable references, into_iter() consumes the collection.
  • collect() gathers results into the desired collection type. When inference fails, annotate the target type (let result: Vec<_> = ...).

Ownership Rules and Iterators

  • iter() yields &T values, iter_mut() yields &mut T, into_iter() yields owned T.
  • Mutating through iter_mut() updates the original collection immediately.
fn main() {
    let mut names = vec!["yuna".to_string(), "min".to_string()];
    names.iter_mut().for_each(|name| name.make_ascii_uppercase());
    println!("{:?}", names);
}

Building Custom Iterators

struct Counter {
    current: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Self { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current >= self.max {
            None
        } else {
            self.current += 1;
            Some(self.current)
        }
    }
}

fn main() {
    let sum: u32 = Counter::new(5).map(|n| n * 2).sum();
    println!("sum = {}", sum);
}
  • type Item specifies what the iterator yields.
  • Consumers like sum or product exhaust the iterator to produce a single value.

Closures Plus Error Handling

  • Methods such as filter_map or map_while express pipelines that mix Result and Option cleanly.
fn parse_numbers(lines: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    lines
        .iter()
        .map(|line| line.trim().parse::<i32>())
        .collect()
}
  • collect() can infer Result<Vec<_>, E>. It stops early on the first Err and otherwise gathers all values.

Practice Exercises

  1. Start with Vec<User> and build a pipeline that filters active users and collects their names.
  2. Compare into_iter() and iter() by moving ownership out of a vector and then trying to print the original.
  3. Implement Iterator for a Fibonacci generator and use take(10).collect::<Vec<_>>() to capture the first values.

Completion Checklist

  • Comfortable with closure syntax and capture rules.
  • Have used iterator combinators such as map, filter, collect, and sum.
  • Have written a custom iterator that implements next.

By the end of part 15 you've tied together collections, error handling, modules, generics, lifetimes, and iterators. In part 16 we'll dive into smart pointers and heap-managed data to deepen your model of Rust's memory story.

💬 댓글

이 글에 대한 의견을 남겨주세요