イテレータで一連の要素を処理する
イテレータパターンを使うと、ある一連の要素に対して順番に何らかの処理を実行できます。イテレータは、各要素を反復処理するロジックと、シーケンスがいつ終了したかを判断する責務を持ちます。イテレータを使えば、そのロジックを自分で再実装する必要はありません。
Rust では、イテレータは 遅延評価される ため、イテレータを消費して使い切るメソッドを呼ぶまでは何の効果もありません。たとえば、リスト13-10のコードは、Vec<T> に定義されている iter メソッドを呼び出して、ベクタ v1 の要素に対するイテレータを作成します。このコード自体は、単独では何も有用なことをしません。
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
このイテレータは v1_iter 変数に格納されます。イテレータを作成したら、それをさまざまな方法で使えます。リスト3-5では、配列に対して for ループを使って反復処理し、その各要素に対して何らかのコードを実行しました。その裏側では、暗黙的にイテレータが作成されて消費されていましたが、それが正確にどのように動くのかについては、これまで詳しく触れていませんでした。
リスト13-11の例では、イテレータの作成と、for ループでのイテレータの使用を分けています。v1_iter にあるイテレータを使って for ループが呼び出されると、イテレータ内の各要素がループの1回の反復で使われ、それぞれの値が表示されます。
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
標準ライブラリでイテレータが提供されていない言語では、おそらく同じ機能を実現するために、まずインデックス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 は、このトレイトに関連型を定義しています。関連型については第20章で詳しく説明します。今のところ知っておくべきことは、このコードが、Iterator トレイトを実装するには Item 型も定義する必要があり、この Item 型が next メソッドの戻り値の型で使われる、ということです。言い換えると、Item 型はそのイテレータから返される型になります。
Iterator トレイトが実装者に定義を要求するメソッドは1つだけです。それが next メソッドで、このメソッドはイテレータの要素を一度に1つずつ返し、その値を Some で包み、反復処理が終了したときには None を返します。
イテレータに対して next メソッドを直接呼び出すこともできます。リスト13-12では、ベクタから作成したイテレータに対して next を繰り返し呼び出したときに、どのような値が返されるかを示しています。
#[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);
}
}
ここで v1_iter をミュータブルにする必要があったことに注意してください。イテレータに対して next メソッドを呼び出すと、イテレータがシーケンスのどこにいるかを追跡するために使っている内部状態が変化します。言い換えると、このコードはイテレータを 消費 する、つまり使い切ります。next を呼び出すたびに、イテレータから1つの要素が取り出されます。for ループを使ったときには v1_iter をミュータブルにする必要はありませんでした。これは、ループが v1_iter の所有権を受け取り、裏側でそれをミュータブルにしていたからです。
また、next の呼び出しで得られる値は、ベクタ内の値への不変参照であることにも注意してください。iter メソッドは、不変参照に対するイテレータを生成します。v1 の所有権を受け取り、所有された値を返すイテレータを作りたい場合は、iter ではなく into_iter を呼び出します。同様に、可変参照に対して反復処理したい場合は、iter ではなく iter_mut を呼び出します。
イテレータを消費するメソッド
Iterator トレイトには、標準ライブラリによってデフォルト実装が提供されている多数の異なるメソッドがあります。これらのメソッドについては、Iterator トレイトの標準ライブラリ API ドキュメントを見ると調べられます。これらのメソッドの中には、その定義の中で next メソッドを呼び出すものがあります。そのため、Iterator トレイトを実装する際には next メソッドの実装が必須なのです。
next を呼び出すメソッドは 消費アダプタ と呼ばれます。なぜなら、それらを呼び出すとイテレータが使い切られるからです。1つの例が sum メソッドです。これはイテレータの所有権を受け取り、next を繰り返し呼び出して要素を走査することで、イテレータを消費します。反復処理の間、それぞれの要素を累積合計に加算し、反復が完了すると合計を返します。リスト13-13には、sum メソッドの使用例を示すテストがあります。
#[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);
}
}
sum は、それを呼び出したイテレータの所有権を受け取るため、sum の呼び出し後に v1_iter を使うことはできません。
別のイテレータを生成するメソッド
イテレータアダプタ とは、Iterator トレイトに定義されているメソッドで、イテレータを消費しないものを指します。代わりに、元のイテレータの何らかの性質を変えることで、異なるイテレータを生成します。
リスト13-14は、イテレータアダプタメソッド map を呼び出す例を示しています。map は、要素が反復処理されるときに各要素に対して呼び出すクロージャを受け取ります。map メソッドは、変更後の要素を生成する新しいイテレータを返します。ここでのクロージャは、ベクタの各要素が1ずつ増加した新しいイテレータを作成します。
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
しかし、このコードは警告を生成します。
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> 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` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
リスト13-14のコードは何もしません。指定したクロージャは一度も呼び出されないのです。この警告はその理由を思い出させてくれます。イテレータアダプタは遅延評価されるため、ここではイテレータを消費する必要があります。
この警告を解消し、イテレータを消費するために、リスト 12-1 で
env::args とともに使った collect メソッドを使用します。このメソッドは
イテレータを消費し、得られた値をコレクションのデータ型に集めます。
リスト 13-15 では、map の呼び出しから返されたイテレータを反復した結果を
ベクタに集めています。このベクタには最終的に、元のベクタの各要素を 1 ずつ
増やしたものが格納されます。
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]);
}
map はクロージャを受け取るので、各要素に対して実行したい任意の操作を
指定できます。これは、Iterator トレイトが提供するイテレーションの振る舞いを
再利用しつつ、クロージャによって一部の振る舞いをカスタマイズできることを示す
優れた例です。
複数のイテレータアダプタの呼び出しを連結して、複雑な処理を読みやすい方法で 実行できます。しかし、すべてのイテレータは遅延評価されるため、 イテレータアダプタの呼び出しから結果を得るには、消費アダプタメソッドのいずれかを 呼び出さなければなりません。
環境をキャプチャするクロージャ
多くのイテレータアダプタは引数としてクロージャを受け取り、一般的に、 イテレータアダプタに引数として指定するクロージャは、その環境をキャプチャする クロージャになります。
この例では、クロージャを受け取る filter メソッドを使います。この
クロージャはイテレータから 1 つの要素を受け取り、bool を返します。クロージャが
true を返した場合、その値は filter が生成するイテレーションに含まれます。
クロージャが false を返した場合、その値は含まれません。
リスト 13-16 では、環境から shoe_size
変数をキャプチャするクロージャとともに filter を使って、Shoe 構造体の
インスタンスのコレクションを反復します。これにより、指定したサイズの靴だけが
返されます。
#[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")
},
]
);
}
}
shoes_in_size 関数は、靴のベクタと靴のサイズの所有権を引数として受け取ります。
そして、指定したサイズの靴だけを含むベクタを返します。
shoes_in_size の本体では、into_iter を呼び出して、ベクタの所有権を受け取る
イテレータを作成します。次に、filter を呼び出して、そのイテレータを、
クロージャが true を返す要素だけを含む新しいイテレータに変換します。
クロージャは環境から shoe_size パラメータをキャプチャし、その値を各靴の
サイズと比較して、指定したサイズの靴だけを残します。最後に、collect を
呼び出すことで、変換後のイテレータが返す値をベクタに集め、それが関数から
返されます。
このテストは、shoes_in_size を呼び出したときに、指定した値と同じサイズの
靴だけが返されることを示しています。