反復子を使って一連の項目を処理する

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

反復子を使った一連の項目の処理へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、反復子を使って一連の項目を処理する方法を探ります。反復子は遅延評価で、自分たちでロジックを再実装することなく、項目のシーケンスを反復処理することができます。

反復子を使った一連の項目の処理

反復子パターンを使うと、一連の項目に対して順番に何かの処理を行うことができます。反復子は、各項目を反復処理するロジックと、シーケンスが終了した時を判断する責任を持っています。反復子を使うときは、自分でそのロジックを再実装する必要はありません。

Rust では、反復子は 遅延評価 です。つまり、反復子を消費して使い果たすメソッドを呼び出さない限り、何の影響もありません。たとえば、リスト 13-10 のコードは、Vec<T> で定義された iter メソッドを呼び出すことで、ベクトル v1 の要素に対する反復子を作成しています。このコードだけでは何の役にも立たない処理しか行いません。

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

リスト 13-10: 反復子の作成

反復子は v1_iter 変数に格納されます。反復子を作成したら、さまざまな方法で使うことができます。リスト 3-5 では、for ループを使って配列を反復処理し、その各要素に対して何かのコードを実行していました。内部的には、これは暗黙的に反復子を作成してから消費していましたが、これまでその詳細については触れていませんでした。

リスト 13-11 の例では、反復子の作成と for ループでの反復子の使用を分離しています。v1_iter の反復子を使って for ループを呼び出すと、反復子の各要素がループの 1 回の反復で使われ、各値が表示されます。

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {val}");
}

リスト 13-11: for ループで反復子を使用する

標準ライブラリに反復子が用意されていない言語では、おそらく同じ機能を、インデックス 0 から始まる変数を使って実装します。この変数を使ってベクトルにインデックスを指定して値を取得し、ループ内で変数の値をインクリメントしていき、ベクトル内の要素の総数に達するまで繰り返します。

反復子はそのようなロジック全てを代行してくれるため、間違える可能性のある繰り返しコードを減らすことができます。反復子を使えば、ベクトルのようにインデックスを指定できるデータ構造だけでなく、多くの種類のシーケンスに対して同じロジックを使う柔軟性が増えます。では、反復子がどのようにそれを行うのか見てみましょう。

反復子トレイトと next メソッド

すべての反復子は、標準ライブラリに定義された Iterator という名前のトレイトを実装しています。このトレイトの定義は次のようになっています。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // デフォルト実装が省略されたメソッド
}

この定義ではいくつかの新しい構文が使われていることに注意してください。type ItemSelf::Item で、このトレイトに関連付けられた型を定義しています。第 19 章で詳しく説明しますが、ここでは知っておく必要のあることは、このコードが Iterator トレイトを実装するには、Item 型も定義する必要があり、この Item 型は next メソッドの戻り値の型に使われるということだけです。言い換えると、Item 型は反復子から返される型になります。

Iterator トレイトは実装者に対して 1 つのメソッドの定義だけを要求しています。それは next メソッドで、このメソッドは反復子の 1 つの要素を 1 回に 1 つ返し、Some にラップされて返されます。反復処理が終了すると、None を返します。

反復子に対して直接 next メソッドを呼び出すことができます。リスト 13-12 は、ベクトルから作成した反復子に対して next を繰り返し呼び出したときに返される値を示しています。

ファイル名:src/lib.rs

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

リスト 13-12: 反復子に対して next メソッドを呼び出す

v1_iter を可変にする必要があることに注意してください。反復子に対して next メソッドを呼び出すと、反復子がシーケンス内のどこにいるかを追跡するために使う内部状態が変更されます。言い換えると、このコードは反復子を 消費 または 使い果たし ます。next の各呼び出しは、反復子から 1 つの要素を消費します。for ループを使ったときは、v1_iter を可変にする必要はありませんでした。なぜなら、ループが v1_iter の所有権を取得し、内部的に可変にしてくれたからです。

また、next の呼び出しから得られる値は、ベクトル内の値への不変参照であることにも注意してください。iter メソッドは不変参照に対する反復子を生成します。v1 の所有権を取得し、所有された値を返す反復子を作成したい場合は、代わりに into_iter を呼び出すことができます。同様に、可変参照を反復処理したい場合は、iter の代わりに iter_mut を呼び出すことができます。

反復子を消費するメソッド

Iterator トレイトには、標準ライブラリによって提供されるデフォルト実装を持つ多数の異なるメソッドがあります。これらのメソッドについては、Iterator トレイトの標準ライブラリ API ドキュメントを参照することで知ることができます。これらのメソッドの一部は、定義の中で next メソッドを呼び出しています。これが、Iterator トレイトを実装する際に next メソッドを実装する必要がある理由です。

next を呼び出すメソッドは、反復子を使い果たすために呼び出されるため、消費アダプタ と呼ばれます。その 1 つの例が sum メソッドで、このメソッドは反復子の所有権を取得し、next を繰り返し呼び出すことで要素を反復処理し、したがって反復子を消費します。反復処理を行う際に、各要素を累積和に加え、反復処理が完了したときに合計値を返します。リスト 13-13 には、sum メソッドの使用例を示すテストがあります。

ファイル名:src/lib.rs

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

リスト 13-13: 反復子内のすべての要素の合計値を取得するために sum メソッドを呼び出す

sum を呼び出した後は、v1_iter を使用することができません。なぜなら、sum はそれを呼び出した反復子の所有権を取得するからです。

他の反復子を生成するメソッド

反復子アダプタ は、Iterator トレイトに定義されたメソッドであり、反復子を消費しません。代わりに、元の反復子のある側面を変更することで、異なる反復子を生成します。

リスト 13-14 は、反復子アダプタメソッドである map を呼び出す例を示しています。このメソッドは、要素が反復処理される際に各要素に対して呼び出すクロージャを取ります。map メソッドは、変更された要素を生成する新しい反復子を返します。ここでのクロージャは、ベクトルの各要素が 1 増えた新しい反復子を作成します。

ファイル名:src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

リスト 13-14: 新しい反復子を作成するために反復子アダプタ map を呼び出す

ただし、このコードは警告を生成します。

warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

リスト 13-14 のコードは何も行いません。指定したクロージャは決して呼び出されません。この警告は、なぜそうなるのかを思い出させてくれます。反復子アダプタは遅延評価であり、ここで反復子を消費する必要があります。

この警告を修正して反復子を消費するには、リスト 12-1 で env::args と共に使用した collect メソッドを使用します。このメソッドは反復子を消費し、結果の値をコレクションデータ型に収集します。

リスト 13-15 では、map の呼び出しから返される反復子を反復処理した結果をベクトルに収集しています。このベクトルには、元のベクトルの各要素が 1 増えたものが含まれるようになります。

ファイル名:src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

リスト 13-15: 新しい反復子を作成するために map メソッドを呼び出し、その後新しい反復子を消費してベクトルを作成するために collect メソッドを呼び出す

map はクロージャを取るため、各要素に対して行いたい任意の操作を指定できます。これは、クロージャが Iterator トレイトが提供する反復処理の動作を再利用しながら、ある動作をカスタマイズできる素晴らしい例です。

反復子アダプタの複数の呼び出しをチェーン化して、読みやすい方法で複雑な操作を行うことができます。ただし、すべての反復子は遅延評価であるため、反復子アダプタの呼び出しから結果を得るには、消費アダプタメソッドの 1 つを呼び出す必要があります。

その環境をキャプチャするクロージャの使用

多くの反復子アダプタはクロージャを引数として取り、一般的に反復子アダプタの引数として指定するクロージャは、その環境をキャプチャするクロージャになります。

この例では、クロージャを取る filter メソッドを使用します。クロージャは反復子から 1 つの要素を取得し、bool を返します。クロージャが true を返す場合、その値は filter によって生成される反復処理に含まれます。クロージャが false を返す場合、その値は含まれません。

リスト 13-16 では、filter を使って、その環境から shoe_size 変数をキャプチャするクロージャを使って、Shoe 構造体インスタンスのコレクションを反復処理します。これにより、指定されたサイズの靴だけが返されます。

ファイル名:src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

リスト 13-16: shoe_size をキャプチャするクロージャを使って filter メソッドを使用する

shoes_in_size 関数は、靴のベクトルと靴のサイズを引数として受け取ります。この関数は、指定されたサイズの靴のみを含むベクトルを返します。

shoes_in_size の本体では、into_iter を呼び出してベクトルの所有権を取得する反復子を作成します。その後、filter を呼び出して、その反復子を、クロージャが true を返す要素のみを含む新しい反復子に変換します。

クロージャはその環境から shoe_size パラメータをキャプチャし、その値を各靴のサイズと比較して、指定されたサイズの靴のみを残します。最後に、collect を呼び出すことで、変換された反復子から返される値を関数が返すベクトルに収集します。

このテストは、shoes_in_size を呼び出すと、指定した値と同じサイズの靴のみが返されることを示しています。

まとめ

おめでとうございます!反復子を使って一連の項目を処理する実験を完了しました。スキルを向上させるために、LabEx でさらに多くの実験を行って練習してください。