一連の要素をイテレータで処理する
イテレータパターンにより、一連の要素に順番に何らかの作業を行うことができます。イテレータは、 各要素を繰り返し、シーケンスが終わったことを決定するロジックの責任を負います。イテレータを使用すると、 自身でそのロジックを再実装する必要がなくなるのです。
Rustにおいて、イテレータは怠惰です。つまり、イテレータを使い込んで消費するメソッドを呼ぶまで何の効果もないということです。
例えば、リスト13-10のコードは、Vec<T>に定義されたiterメソッドを呼ぶことでv1ベクタの要素に対するイテレータを生成しています。
このコード単独では、何も有用なことはしません。
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
リスト13-10: イテレータを生成する
イテレータはv1_iter変数に保存されます。一旦イテレータを生成したら、いろんな手段で使用することができます。
第3章のリスト3-5では、forループを使用して配列を走査し、各要素に対して何らかのコードを実行しました。
今までそれが正確にはどう機能するのかごまかしてきましたが、裏で暗黙にイテレータを作成して消費していたのです。
リスト13-11の例では、イテレータの生成とforループでイテレータを使用することを区別しています。
v1_iterのイテレータでforループが呼び出された時に、イテレータの各要素がループの繰り返しで使用され、各値が出力されます。
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {}", val); } }
リスト13-11: forループでイテレータを使用する
標準ライブラリにより提供されるイテレータが存在しない言語では、変数を添え字0から始め、 その変数でベクタに添え字アクセスして値を得て、ベクタの総要素数に到達するまでループでその変数の値をインクリメントすることで、 この同じ機能を書く可能性が高いでしょう。
イテレータはそのロジック全てを処理してくれるので、めちゃくちゃにしてしまう可能性のあるコードの繰り返しを減らしてくれます。 イテレータにより、添え字を使えるデータ構造、ベクタなどだけではなく、多くの異なるシーケンスに対して同じロジックを使う柔軟性も得られます。 イテレータがそれをする方法を調査しましょう。
Iteratorトレイトとnextメソッド
全てのイテレータは、標準ライブラリで定義されているIteratorというトレイトを実装しています。
このトレイトの定義は、以下のようになっています:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // デフォルト実装のあるメソッドは省略 // methods with default implementations elided } }
この定義は新しい記法を使用していることに注目してください: type ItemとSelf::Itemで、
これらはこのトレイトとの関連型(associated type)を定義しています。関連型についての詳細は、第19章で語ります。
とりあえず、知っておく必要があることは、このコードがIteratorトレイトを実装するには、Item型も定義する必要があり、
そして、このItem型がnextメソッドの戻り値の型に使われていると述べていることです。換言すれば、
Item型がイテレータから返ってくる型になるだろうということです。
Iteratorトレイトは、一つのメソッドを定義することを実装者に要求することだけします: nextメソッドで、
これは1度にSomeに包まれたイテレータの1要素を返し、繰り返しが終わったら、Noneを返します。
イテレータに対して直接nextメソッドを呼び出すこともできます; リスト13-12は、
ベクタから生成されたイテレータのnextを繰り返し呼び出した時にどんな値が返るかを模擬しています。
ファイル名: src/lib.rs
#[cfg(test)]
mod tests {
#[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の各呼び出しは、イテレータの要素を一つ、食います。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-13は、sumの使用を説明したテストです:
ファイル名: src/lib.rs
#[cfg(test)]
mod tests {
#[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は呼び出し対象のイテレータの所有権を奪うので、sum呼び出し後にv1_iterを使用することはできません。
他のイテレータを生成するメソッド
イテレータアダプタ(iterator adaptors)は、Iteratorトレイト上で定義されたメソッドのうち、イテレータを消費しないものです。
これらはイテレータを消費しない代わりに、元のイテレータの一部の様相を変更することによって、別のイテレータを生成します。
リスト13-14は、イテレータアダプタメソッドのmapの呼び出し例を示しています。
mapは、各要素が反復処理されるときに、その要素に対して呼び出すクロージャを取ります。
mapメソッドは、修正された要素を生成する新しいイテレータを返します。
map新しいイテレータを生成します。ここのクロージャは、ベクタの各要素が1インクリメントされる新しいイテレータを作成します:
ファイル名: src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
リスト13-14: イテレータアダプタのmapを呼び出して新規イテレータを作成する
ところが、このコードは警告を発します:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
(警告: 使用されねばならない`Map`が未使用です)
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
(メモ: イテレータは怠惰で、消費されるまで何もしません)
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
リスト13-14のコードは何もしません; 指定したクロージャは、決して呼ばれないのです。警告が理由を思い出させてくれています: イテレータアダプタは怠惰で、ここでイテレータを消費する必要があるのです。
この警告を修正し、イテレータを消費するには、collectメソッドを使用しますが、これは第12章のリスト12-1でenv::argsとともに使用しました。
このメソッドはイテレータを消費し、結果の値をコレクションデータ型に集結させます。
リスト13-15において、map呼び出しから返ってきたイテレータを繰り返した結果をベクタに集結させています。
このベクタは、最終的に元のベクタの各要素に1を足したものが含まれます。
ファイル名: src/main.rs
fn main() { 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トレイトが提供する繰り返し動作を再利用しつつ、
クロージャにより一部の動作をカスタマイズできる好例になっています。
複雑な操作を可読性の高い方法で行うために、イテレータアダプタの呼び出しを複数チェーンすることができます。 ですが、すべてのイテレータは怠惰なので、イテレータアダプタの呼び出しから結果を得るには、消費アダプタメソッドのうちいずれかを呼ぶ必要があります。
環境をキャプチャするクロージャを使用する
多くのイテレータアダプタは引数としてクロージャを取り、また、イテレータアダプタに引数として指定するクロージャは、 環境をキャプチャするクロージャであることは、よくあることでしょう。
この例では、クロージャを取るfilterメソッドを使用します。クロージャは、イテレータからの要素を取り、boolを返すクロージャです。
このクロージャがtrueを返せば、filterが生成する繰り返し処理にその値が含まれます。クロージャがfalseを返したら、
その値は含まれません。
リスト13-16では、環境からshoe_size変数をキャプチャするクロージャでfilterを使って、
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を呼び出した時に、指定した値と同じサイズの靴だけが得られることをテストは示しています。