14. クロージャとイテレータ

🎯 この章のゴール

  • クロージャ(無名関数)を書き、環境の捕捉を理解する
  • イテレータのアダプタをつなげてデータを処理する

💡 ひとことで言うと

クロージャは 周りの変数を持ち歩ける小さな関数。イテレータは データのベルトコンベア で、filter や map という加工機を並べて最後に collect で箱詰めします。

クロージャ

fn main() {
    let add_one = |x: i32| x + 1;          // 型注釈は省略可
    let square = |x| x * x;                 // 推論される
    println!("{} {}", add_one(1), square(4));

    let tax = 1.1;
    let with_tax = |price: f64| price * tax; // 環境の tax を捕捉!
    println!("{}", with_tax(100.0));

    let mut count = 0;
    let mut inc = || count += 1;            // 可変で捕捉
    inc();
    inc();
    println!("count = {count}");
}
Playground で開く ↗

捕捉の方法と Fn トレイト

クロージャは中身に応じて、自動的に最小限の方法で変数を捕捉します。

捕捉方法実装するトレイト呼べる回数
不変借用Fn何度でも
可変借用FnMut何度でも(ただし排他的)
所有権を奪うFnOnce1 回だけかも
fn apply<F: Fn(i32) -> i32>(f: F, v: i32) -> i32 {
    f(v)
}

fn main() {
    let offset = 10;
    println!("{}", apply(|x| x + offset, 5)); // 15

    let name = String::from("Ferris");
    let greet = move || println!("やあ {name}"); // name の所有権を移動
    greet();
    // println!("{name}"); // ← ムーブ済みなので使えない
}
Playground で開く ↗

イテレータ

fn main() {
    let words = vec!["apple", "banana", "cherry", "date"];

    let long: Vec<String> = words
        .iter()                          // イテレータを作る
        .filter(|w| w.len() > 5)         // 条件で絞る
        .map(|w| w.to_uppercase())       // 変換する
        .collect();                      // Vec に集める
    println!("{long:?}");                // ["BANANA", "CHERRY"]

    for (i, w) in words.iter().enumerate().skip(1).take(2) {
        println!("{i}: {w}");
    }

    let total: u32 = (1..=10).sum();
    let has_c = words.iter().any(|w| w.starts_with('c'));
    let pos = words.iter().position(|&w| w == "date");
    println!("{total} {has_c} {pos:?}");
}
Playground で開く ↗

よく使うメソッド

種類メソッド
アダプタ(新しいイテレータを返す・怠惰)map filter enumerate zip skip take rev chain flat_map
消費(結果を返す)collect sum count min max fold any all find position for_each

イテレータは 怠惰(lazy) です。collect などの消費メソッドを呼ぶまで処理は始まりません。しかもコンパイラが最適化するので、手書きの for ループと同等の速さになります(ゼロコスト抽象化)。

自作イテレータ

Iterator トレイトの next を実装するだけで、全メソッドが使えるようになります。

struct Countdown(u32);

impl Iterator for Countdown {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.0 == 0 {
            None
        } else {
            self.0 -= 1;
            Some(self.0 + 1)
        }
    }
}

fn main() {
    let v: Vec<u32> = Countdown(3).collect();
    println!("{v:?} 発射!🚀"); // [3, 2, 1]
}
Playground で開く ↗

🧪 やってみよう

下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。

1〜100 のうち「3 の倍数かつ 5 の倍数でない数」の二乗の合計を、イテレータのチェーン 1 本で求めよう。

fn main() {
    let total: u32 = (1..=100)
        // TODO: filter と map をつなげよう
        .sum();
    println!("{total}"); // 正解は 92286
}
Playground で開く ↗
💡 解答例を見る
fn main() {
    let total: u32 = (1..=100)
        .filter(|n| n % 3 == 0 && n % 5 != 0)
        .map(|n| n * n)
        .sum();
    println!("{total}"); // 正解は 92286
}
Playground で開く ↗

📝 まとめ

  • クロージャは |引数| 式。環境を Fn / FnMut / FnOnce の形で捕捉
  • move で所有権を強制的に移動
  • イテレータは怠惰。アダプタをつないで消費メソッドで実行

✅ 理解度クイズ

Q1
次のコードの出力は?
fn main() {
    let v = vec![1, 2, 3, 4, 5, 6];
    let s: i32 = v.iter().filter(|&&x| x % 2 == 0).map(|x| x * 10).sum();
    println!("{s}");
}
Q2
次のコードは何も出力しない。なぜ?
fn main() {
    let v = vec![1, 2, 3];
    v.iter().map(|x| println!("{x}"));
}
Q3
スレッドに渡すクロージャで、捕捉した変数の所有権を 強制的に 移すキーワードは?
Q4
v.iter()、v.iter_mut()、v.into_iter() が返す要素の型はそれぞれ?(v: Vec<T>)