Iteratorとクロージャ

このページの目次

イテレータは「次の要素を生成する」と「要素をどう処理するか」を分離します。クロージャは、小さな動作とその環境をまとめて、mapやfilterなどの操作に渡します。これらを組み合わせることで、呼び出し側はデータ処理の手順を記述でき、消費側がどの程度の結果を必要とするかを制御できます。

理解する際には、以下の3つを同時に追跡する必要があります。いつ逐要素ロジックが実際に実行されるか、Itemが値か参照か、そしてクロージャがキャプチャしたオブジェクトに対して何を行ったかです。

nextは契約のエントリポイントであり、すべての実装における唯一の実行パスではない

Iteratorの必須メンバは、関連型 Item と next(&mut self) -> Option<Self::Item> です。これらを提供するだけで、デフォルト実装を持つ多くのメソッドを使用できますが、具体的なイテレータは nth、fold、try_fold などをオーバーライドして最適化できます。すべての操作の実際の機械的な実行が、デフォルトの next の逐次呼び出しであるとは限りません。

一般的な Iterator は、None を返した後も再び Some を返す可能性があります。終了したら永久に終了するというセマンティクスが必要な場合は、fuse を使用するか、FusedIterator の保証に依存してください。for ループは、最初の None に遭遇すると停止します。Iterator

遅延評価:消費がなければ逐要素変換も発生しない

0..10整数を提供filter偶数を保持map2を掛けるtake(3)最大3件要素は右へ流れるcollect が消費を駆動:[0, 4, 8] を得ると停止遅延パイプラインは要求に応じてプルする:take(3) はソースイテレータを最後まで走査する必要がないアダプタの構築は状態の作成とクロージャ環境のキャプチャを行います。遅延とは、逐要素ロジックがまだ実行されていないことを指します。
fn main() {
    let mut inspected = 0;
    let output: Vec<_> = (0..10)
        .inspect(|_| inspected += 1)
        .filter(|x| x % 2 == 0)
        .map(|x| x * 2)
        .take(3)
        .collect();
    assert_eq!(output, [0, 4, 8]);
    assert_eq!(inspected, 5); // ソースの 0, 1, 2, 3, 4 のみを読み取った
}

これは消費側から逆方向に要求し、ソース側から順方向に配信するパターンです。3番目の出力 8 を得るために、filter はソース値 4 を読み取る必要があり、その後の 5 から 9 はアクセスされません。クロージャの構築時やソースイテレータの構築時に発生する副作用は、即時に発生する可能性があります。遅延評価とは、式全体が何もしないという意味ではありません。

順序は結果を変えます。filter(...).take(3) は条件を満たす最初の3つの要素を返しますが、take(3).filter(...) はソースの最初の3つの要素のみをフィルタリングします。find、any、all、try_fold などはショートサーキットする可能性があり、実行されていない後半部分を信頼できる副作用のある処理として扱うことはできません。

take の位置で元を読む範囲が変わる

中間配列を先に作るのではなく、消費要求が読む範囲を決めます。k=3 では filter が先なら元の 4 まで読んで偶数を 3 個得ますが、take が先なら 0、1、2 だけを調べます。k=0 なら要素処理はありませんが、構築の費用までゼロとは限りません。

図を準備しています
take の位置で元を読む範囲が変わる

filter 後の take は最初の k 個の一致を返し、take 後の filter は元の最初の k 個だけを調べます。

iter、iter_mut、into_iter の所有権

Vec の一般的なエントリItem元のコンテナへの影響
v.iter() または (&v).into_iter()&T要素を借用し、コンテナは依然として元の所有者に帰属
v.iter_mut() または (&mut v).into_iter()&mut T要素ごとに排他的借用を行う
v.into_iter()TVec を消費し、その要素を移動

IntoIterator は「イテレータに変換可能である」というインターフェースであり、Iterator はすでに進めることができる状態です。for ループは IntoIterator を使用します。メソッド名に into_iter があっても、レシーバー型を離れて「必ず基底要素が移動する」と断定しないでください。

map は現在の Item を受け取り、filter の述語は &Item を受け取ります。そのため、iter() に対する filter の引数では、二重参照が見える可能性があります。copied は Copy 要素に適しており、cloned は Clone を実行します。後者は String などで割り当てが発生する可能性があり、型チェックラーを満足させるだけの無コストな操作ではありません。

数要素を一時的に消費した後に同じイテレータを引き続き使用する必要がある場合は、by_ref を使用できます。

fn main() {
    let mut items = 0..5;
    let first: Vec<_> = items.by_ref().take(2).collect();
    assert_eq!(first, [0, 1]);
    assert_eq!(items.next(), Some(2));
}

停止したとき、条件を満たさない要素はどこへ行くのか

take_while は述語を判断するために、まず要素を1つ取り出す必要があります。条件を満たさない最初の要素に遭遇すると、その要素はソースイテレータからすでに取り出されており、後の消費者には渡されません。区切り文字を含むデータを解析する際、この位置の変化は特に誤解を招きやすいです。

fn main() {
    let mut source = [1, 2, 0, 3].into_iter();
    let prefix: Vec<_> = source.by_ref().take_while(|x| *x != 0).collect();
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(3)); // 0 は失敗した述語判断のために使用済み

    let mut source = [1, 2, 0, 3].into_iter().peekable();
    let mut prefix = Vec::new();
    while let Some(value) = source.next_if(|x| *x != 0) {
        prefix.push(value);
    }
    assert_eq!(prefix, [1, 2]);
    assert_eq!(source.next(), Some(0)); // 判断に失敗した場合でも、その要素は保持される
}

Peekable は論理的な次の要素を保持できますが、最初の peek は内部の next を呼び出して要素をキャッシュする可能性があるため、「まだ取り出されていない」からといって「内部に副作用がない」とは限りません。上記の例では、next_if の失敗により 0 が保持され、その後区切り文字を処理する専用のコードによって消費されます。この要件を満たすために take_while を書いた後に区切り文字がまだあると仮定しないでください。take_while

キャプチャ方法と呼び出し能力は別の次元である

move はクロージャが環境をキャプチャする際に所有権を取得することを決定しますが、それは呼び出すたびに環境が移動されることを意味しません。Fn/FnMut/FnOnce は、呼び出し時にクロージャ自体にどのようにアクセスする必要があるかを記述します。

能力呼び出しレシーバー典型的な制限
FnOnceselfクロージャを消費する呼び出しを許可し、その後も呼び出せることは保証されない
FnMut&mut self繰り返し呼び出し可能だが、キャプチャされた状態を変更する必要がある場合がある
Fn&self共有借用を通じて呼び出し可能で、クロージャ環境への排他的アクセスは不要

Fn 型は FnMut と FnOnce も満たし、FnMut 型は FnOnce も満たします。FnOnce を受け取る関数は、最も広い範囲の「1回限りの呼び出し」を許可します。Fn を要求すると実装者に厳格な制約を課すため、Fn を「最も汎用的な型」と一般化して呼ぶことはできません。通常のクロージャは少なくとも FnOnce を実装しており、async クロージャには独自の呼び出しトレイトのルールがあります。クロージャ型

fn call_twice<F: Fn()>(f: F) { f(); f(); }

fn main() {
    let values = vec![1, 2, 3];
    call_twice(move || println!("{values:?}"));
    // move によりクロージャが Vec を所有する;println! はそれを借用するだけで、Fn を満たす。

    let mut count = 0;
    {
        let mut add = || count += 1;
        add();
        add();
    }
    assert_eq!(count, 2);

    let text = String::from("owned");
    let take = move || text;
    assert_eq!(take(), "owned");
    // 再度 take() を呼び出すことはできない:1回目の呼び出しで text がクロージャから移動済み。
}

Cell、RefCell、またはロックなどの内部可変性を使用すれば、Fn クロージャも外部から観測可能な状態を変更できるため、Fn が純粋関数や副作用がないことを意味するわけではありません。Send や Sync かどうか、'static を満たすかどうかは、キャプチャ内容と使用方法によって別途決定されます。move で短いライフタイムの参照をキャプチャしても、それが 'static になるわけではありません。

クロージャを返す際は、環境の寿命をインターフェースに書き込む

fn starts_with<'a>(prefix: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |value| value.starts_with(prefix)
}

fn main() {
    let prefix = String::from("api/");
    let matches = starts_with(&prefix);
    assert!(matches("api/items"));
    assert!(!matches("static/items"));
}

クロージャが所有しているのはその参照であり、文字列自体は呼び出し元に属します。返り値の型における 'a は環境への依存関係を表現しており、prefix の実際の寿命を延ばすものではありません。'static を要求されるタスクにクロージャを保存する必要がある場合は、String を受け取って所有するように変更できます。割り当てやコピーを行う価値があるかどうかは、インターフェースが要求する所有関係によって決定されるべきです。

collect のターゲット型が終了方法を決定する

collect は FromIterator を通じてターゲットを構築するため、Vec に限定されません。Iterator<Item=Result<T,E>> を Result<Vec<T>,E> に収集する場合、最初のエラーで収集がショートサーキットします。

fn main() {
    let parsed: Result<Vec<u32>, _> = ["10", "bad", "30"]
        .into_iter()
        .map(str::parse)
        .collect();
    assert!(parsed.is_err());
}

これは filter_map(|s| s.parse().ok()) と同等ではありません。後者はエラーのある項目を静かに破棄します。どちらを選ぶかは、ビジネスロジックで不良データを無視できるかどうかによって決まり、チェーン式の長さによるものではありません。

"10"解析済み、累積値 10"bad"取り出済み、解析失敗"30"未読、処理継続可能Err を返す;イテレータの次要素は "30"ショートサーキットは残りのイテレーション位置を保存するが、実行済みの操作はロールバックしないtry_fold の累積値は外部トランザクションではない;これまでに発生したログ出力、書き込み、送信などの副作用は自動的に取り消されない。

try_fold のエラーと残りの位置

fn main() {
    let mut input = ["10", "bad", "30"].into_iter();
    let total: Result<u32, &'static str> = input.try_fold(0u32, |sum, text| {
        let value = text.parse::<u32>().map_err(|_| "invalid number")?;
        sum.checked_add(value).ok_or("sum overflow")
    });
    assert_eq!(total, Err("invalid number"));
    assert_eq!(input.next(), Some("30"));
}

失敗した項目はすでに消費されており、その後の項目は処理可能です。これは、回復戦略付きの逐次入力において有用です。失敗した項目を再試行する必要がある場合は、エラーの中にそれを保持するか、別途キャッシュする必要があります。イテレータが自動的に巻き戻ることを期待してはいけません。例の checked_add は、デバッグビルドとリリースビルドの両方で、オーバーフローのポリシーが依存しないようにしています。

データ損失を隠しやすい別の式として、Result イテレータに対して直接 flatten する場合があります。Ok は値を1つ提供しますが、Err は値を提供しないため、エラー情報が消去されます。失敗情報を必要とする場合は、Result への収集、明示的な match、または逐要素結果の保持を選択してください。try_fold

size_hint は割り当てに役立つかも知れないが、境界外書き込みを許可するものではない

fn main() {
    let values = [1u32, 2, 3, 4];
    let mut exact = values.iter();
    assert_eq!(exact.len(), 4);
    exact.next();
    assert_eq!(exact.len(), 3);

    let filtered = values.iter().filter(|x| **x % 2 == 0);
    assert_eq!(filtered.size_hint(), (0, Some(4)));
    assert_eq!(filtered.count(), 2);
}

フィルタリング前には、残りが正確に4つあることがわかります。フィルタリング後、述語を実行する前には、通常、0から4の範囲しか示すことができません。size_hint の下限は、すでに初期化された要素数を表すものではなく、上限も容量ではありません。通常の安全なトレイトの実装は誤っている可能性があり、unsafe コードはこれらのヒントだけを頼りに、割り当て済みの領域を書き込んだり、未初期化メモリを有効な値として扱ったりすることはできません。ExactSizeIterator が正確な長さのインターフェースを提供していても、それは呼び出し側にメモリ安全性のチェックを放棄させる unsafe 契約ではありません。size_hint

ゼロコスト抽象は正しく理解する必要がある

静的型付けされたアダプタチェーンは、コンパイラに単一化、インライン展開、ループ最適化を行うのに十分な情報を提供することが多く、各 map のたびに中間の Vec を作成する必要はありません。しかし、空でない Vec への collect は依然として割り当てを発生させる可能性があり、クロージャ内部でも割り当てが発生する可能性があります。動的ディスパッチ、複雑な分岐、最適化の境界は最終的なコードに影響を与えます。

「ゼロコスト」とは、任意のチェーンが特定の手作りループと完全に同じアセンブリを生成することを保証するものではなく、CPUを消費しないという意味でもありません。ホットパスでは、同じセマンティクス、同じ最適化設定、同じデータで比較を行い、割り当て、ショートサーキットの動作、最終的な生成物を確認してください。まずは明確なイテレーションモデルを維持し、証拠に基づいて最適化を行ってください。