一連の要素をイテレータで処理する
イテレータパターンにより、一連の要素に順番に何らかの作業を行うことができます。イテレータは、 各要素を繰り返し、シーケンスが終わったことを決定するロジックの責任を負います。イテレータを使用すると、 自身でそのロジックを再実装する必要がなくなるのです。
Rustにおいて、イテレータは怠惰です。つまり、イテレータを使い込んで消費するメソッドを呼ぶまで何の効果もないということです。
例えば、リスト13-13のコードは、Vec<T>
に定義されたiter
メソッドを呼ぶことでv1
ベクタの要素に対するイテレータを生成しています。
このコード単独では、何も有用なことはしません。
リスト13-13: イテレータを生成する
一旦イテレータを生成したら、いろんな手段で使用することができます。第3章のリスト3-5では、
ここまでiter
の呼び出しが何をするかごまかしてきましたが、for
ループでイテレータを使い、
各要素に何かコードを実行しています。
リスト13-14の例は、イテレータの生成とfor
ループでイテレータを使用することを区別しています。
イテレータは、v1_iter
変数に保存され、その時には繰り返しは起きていません。v1_iter
のイテレータで、
for
ループが呼び出された時に、イテレータの各要素がループの繰り返しで使用され、各値が出力されます。
リスト13-14: for
ループでイテレータを使用する
標準ライブラリにより提供されるイテレータが存在しない言語では、変数を添え字0から始め、 その変数でベクタに添え字アクセスして値を得て、ベクタの総要素数に到達するまでループでその変数の値をインクリメントすることで、 この同じ機能を書く可能性が高いでしょう。
イテレータはそのロジック全てを処理してくれるので、めちゃくちゃにしてしまう可能性のあるコードの繰り返しを減らしてくれます。 イテレータにより、添え字を使えるデータ構造、ベクタなどだけではなく、多くの異なるシーケンスに対して同じロジックを使う柔軟性も得られます。 イテレータがそれをする方法を調査しましょう。
Iterator
トレイトとnext
メソッド
全てのイテレータは、標準ライブラリで定義されているIterator
というトレイトを実装しています。
このトレイトの定義は、以下のようになっています:
この定義は新しい記法を使用していることに注目してください: type Item
とSelf::Item
で、
これらはこのトレイトとの関連型(associated type)を定義しています。関連型についての詳細は、第19章で語ります。
とりあえず、知っておく必要があることは、このコードがIterator
トレイトを実装するには、Item
型も定義する必要があり、
そして、このItem
型がnext
メソッドの戻り値の型に使われていると述べていることです。換言すれば、
Item
型がイテレータから返ってくる型になるだろうということです。
Iterator
トレイトは、一つのメソッドを定義することを実装者に要求することだけします: next
メソッドで、
これは1度にSome
に包まれたイテレータの1要素を返し、繰り返しが終わったら、None
を返します。
イテレータに対して直接next
メソッドを呼び出すこともできます; リスト13-15は、
ベクタから生成されたイテレータのnext
を繰り返し呼び出した時にどんな値が返るかを模擬しています。
ファイル名: src/lib.rs
リスト13-15: イテレータに対してnext
メソッドを呼び出す
v1_iter
を可変にする必要があったことに注目してください: イテレータのnext
メソッドを呼び出すと、
今シーケンスのどこにいるかを追いかけるためにイテレータが使用している内部の状態が変わります。
つまり、このコードはイテレータを消費、または使い込むのです。
next
の各呼び出しは、イテレータの要素を一つ、食います。for
ループを使用した時には、
v1_iter
を可変にする必要はありませんでした。というのも、ループがv1_iter
の所有権を奪い、
陰で可変にしていたからです。
また、next
の呼び出しで得られる値は、ベクタの値への不変な参照であることにも注目してください。
iter
メソッドは、不変参照へのイテレータを生成します。v1
の所有権を奪い、所有された値を返すイテレータを生成したいなら、
iter
ではなくinto_iter
を呼び出すことができます。同様に、可変参照を繰り返したいなら、
iter
ではなくiter_mut
を呼び出せます。
イテレータを消費するメソッド
Iterator
トレイトには、標準ライブラリが提供してくれているデフォルト実装のある多くの異なるメソッドがあります;
Iterator
トレイトの標準ライブラリのAPIドキュメントを検索することで、これらのメソッドについて知ることができます。
これらのメソッドの中には、定義内でnext
メソッドを呼ぶものもあり、故にIterator
トレイトを実装する際には、
next
メソッドを実装する必要があるのです。
next
を呼び出すメソッドは、消費アダプタ(consuming adaptors)と呼ばれます。呼び出しがイテレータの使い込みになるからです。
一例は、sum
メソッドで、これはイテレータの所有権を奪い、next
を繰り返し呼び出すことで要素を繰り返し、
故にイテレータを消費するのです。繰り返しが進むごとに、各要素を一時的な合計に追加し、
繰り返しが完了したら、その合計を返します。リスト13-16は、sum
の使用を説明したテストです:
ファイル名: src/lib.rs
リスト13-16: sum
メソッドを呼び出してイテレータの全要素の合計を得る
sum
は呼び出し対象のイテレータの所有権を奪うので、sum
呼び出し後にv1_iter
を使用することはできません。
他のイテレータを生成するメソッド
Iterator
トレイトに定義された他のメソッドは、イテレータアダプタ(iterator adaptors)として知られていますが、
イテレータを別の種類のイテレータに変えさせてくれます。イテレータアダプタを複数回呼ぶ呼び出しを連結して、
複雑な動作を読みやすい形で行うことができます。ですが、全てのイテレータは怠惰なので、消費アダプタメソッドのどれかを呼び出し、
イテレータアダプタの呼び出しから結果を得なければなりません。
リスト13-17は、イテレータアダプタメソッドのmap
の呼び出し例を示し、各要素に対して呼び出すクロージャを取り、
新しいイテレータを生成します。ここのクロージャは、ベクタの各要素が1インクリメントされる新しいイテレータを作成します。
ところが、このコードは警告を発します:
ファイル名: src/main.rs
リスト13-17: イテレータアダプタのmap
を呼び出して新規イテレータを作成する
出る警告は以下の通りです:
warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy
and do nothing unless consumed
(警告: 使用されねばならない`std::iter::Map`が未使用です: イテレータアダプタは怠惰で、
消費されるまで何もしません)
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_must_use)] on by default
リスト13-17のコードは何もしません; 指定したクロージャは、決して呼ばれないのです。警告が理由を思い出させてくれています: イテレータアダプタは怠惰で、ここでイテレータを消費する必要があるのです。
これを修正し、イテレータを消費するには、collect
メソッドを使用しますが、これは第12章のリスト12-1でenv::args
とともに使用しました。
このメソッドはイテレータを消費し、結果の値をコレクションデータ型に集結させます。
リスト13-18において、map
呼び出しから返ってきたイテレータを繰り返した結果をベクタに集結させています。
このベクタは、最終的に元のベクタの各要素に1を足したものが含まれます。
ファイル名: src/main.rs
リスト13-18: map
メソッドを呼び出して新規イテレータを作成し、
それからcollect
メソッドを呼び出してその新規イテレータを消費し、ベクタを生成する
map
はクロージャを取るので、各要素に対して行いたいどんな処理も指定することができます。
これは、Iterator
トレイトが提供する繰り返し動作を再利用しつつ、
クロージャにより一部の動作をカスタマイズできる好例になっています。
環境をキャプチャするクロージャを使用する
イテレータが出てきたので、filter
イテレータアダプタを使って環境をキャプチャするクロージャの一般的な使用をデモすることができます。
イテレータのfilter
メソッドは、イテレータの各要素を取り、論理値を返すクロージャを取ります。
このクロージャがtrue
を返せば、filter
が生成するイテレータにその値が含まれます。クロージャがfalse
を返したら、
結果のイテレータにその値は含まれません。
リスト13-19では、環境からshoe_size
変数をキャプチャするクロージャでfilter
を使って、
Shoe
構造体インスタンスのコレクションを繰り返しています。指定したサイズの靴だけを返すわけです。
ファイル名: src/lib.rs
リスト13-19: shoe_size
をキャプチャするクロージャでfilter
メソッドを使用する
shoes_in_my_size
関数は、引数として靴のベクタとサイズの所有権を奪います。指定されたサイズの靴だけを含むベクタを返します。
shoes_in_my_size
の本体で、into_iter
を呼び出してベクタの所有権を奪うイテレータを作成しています。
そして、filter
を呼び出してそのイテレータをクロージャがtrue
を返した要素だけを含む新しいイテレータに適合させます。
クロージャは、環境からshoe_size
引数をキャプチャし、指定されたサイズの靴だけを保持しながら、
その値を各靴のサイズと比較します。最後に、collect
を呼び出すと、
関数により返ってきたベクタに適合させたイテレータから返ってきた値が集まるのです。
shoes_in_my_size
を呼び出した時に、指定した値と同じサイズの靴だけが得られることをテストは示しています。
Iterator
トレイトで独自のイテレータを作成する
ベクタに対し、iter
、into_iter
、iter_mut
を呼び出すことでイテレータを作成できることを示してきました。
ハッシュマップなどの標準ライブラリの他のコレクション型からもイテレータを作成できます。
Iterator
トレイトを自分で実装することで、したいことを何でもするイテレータを作成することもできます。
前述の通り、定義を提供する必要のある唯一のメソッドは、next
メソッドなのです。一旦、そうしてしまえば、
Iterator
トレイトが用意しているデフォルト実装のある他の全てのメソッドを使うことができるのです!
デモ用に、絶対に1から5をカウントするだけのイテレータを作成しましょう。まず、値を保持する構造体を生成し、
Iterator
トレイトを実装することでこの構造体をイテレータにし、その実装内の値を使用します。
リスト13-20は、Counter
構造体とCounter
のインスタンスを作るnew
関連関数の定義です:
ファイル名: src/lib.rs
リスト13-20: Counter
構造体とcount
に対して0という初期値でCounter
のインスタンスを作るnew
関数を定義する
Counter
構造体には、count
というフィールドがあります。このフィールドは、
1から5までの繰り返しのどこにいるかを追いかけるu32
値を保持しています。Counter
の実装にその値を管理してほしいので、
count
フィールドは非公開です。count
フィールドは常に0という値から新規インスタンスを開始するという動作をnew
関数は強要します。
次に、next
メソッドの本体をこのイテレータが使用された際に起きてほしいことを指定するように定義して、
Counter
型に対してIterator
トレイトを実装します。リスト13-21のようにですね:
ファイル名: src/lib.rs
リスト13-21: Counter
構造体にIterator
トレイトを実装する
イテレータのItem
関連型をu32
に設定しました。つまり、イテレータは、u32
の値を返します。
ここでも、まだ関連型について心配しないでください。第19章で講義します。
イテレータに現在の状態に1を足してほしいので、まず1を返すようにcount
を0に初期化しました。
count
の値が5以下なら、next
はSome
に包まれた現在の値を返しますが、
count
が6以上なら、イテレータはNone
を返します。
Counter
イテレータのnext
メソッドを使用する
一旦Iterator
トレイトを実装し終わったら、イテレータの出来上がりです!リスト13-22は、
リスト13-15のベクタから生成したイテレータと全く同様に、直接next
メソッドを呼び出すことで、
Counter
構造体のイテレータ機能を使用できることをデモするテストを示しています。
ファイル名: src/lib.rs
リスト13-22: next
メソッド実装の機能をテストする
このテストは、counter
変数に新しいCounter
インスタンスを生成し、
それからイテレータにほしい動作が実装し終わっていることを実証しながら、next
を繰り返し呼び出しています:
1から5の値を返すことです。
他のIterator
トレイトメソッドを使用する
next
メソッドを定義してIterator
トレイトを実装したので、今では、標準ライブラリで定義されているように、
どんなIterator
トレイトメソッドのデフォルト実装も使えるようになりました。全てnext
メソッドの機能を使っているからです。
例えば、何らかの理由で、Counter
インスタンスが生成する値を取り、最初の値を飛ばしてから、
別のCounter
インスタンスが生成する値と一組にし、各ペアを掛け算し、3で割り切れる結果だけを残し、
全結果の値を足し合わせたくなったら、リスト13-23のテストに示したように、そうすることができます:
ファイル名: src/lib.rs
リスト13-23: Counter
イテレータに対していろんなIterator
トレイトのメソッドを使用する
zip
は4組しか生成しないことに注意してください; 理論的な5番目の組の(5, None)
は、
入力イテレータのどちらかがNone
を返したら、zip
はNone
を返却するため、決して生成されることはありません。
next
メソッドの動作方法を指定し、標準ライブラリがnext
を呼び出す他のメソッドにデフォルト実装を提供しているので、
これらのメソッド呼び出しは全て可能です。