項目9:明示的なループの代わりにイテレータ変換を使用することを検討しよう
ループの抽象化
ループ変数は要素そのもの
code:rs
let even_sum_squares: u64 = values
.iter()
.filter(|x| *x % 2 == 0)
.take(5)
.map(|x| x * x)
.sum();
イテレータトレイト
コアとなる Iterator トレイトを実装するには、 next メソッドのみ実装すれば良い fn next(&mut self) -> Option<Self::Item>;
要素の型は関連型 Item で指定する
必須メソッドである into_iter は、Self を 消費(所有権 を奪う) する代わりに Iterator を返す fn into_iter(self) -> Self::IntoIter;
コンパイラは、以下のような形の式を見つけると、自動的にこのトレイトを用いる
for item in collection { ... }
すべての Iterator は、このトレイトを self を返すようにあらかじめ実装している
code:rs
impl<I: Iterator> IntoIterator for I {
type Item = I::Item;
type IntoIter = I;
fn into_iter(self) -> I {
self
}
}
イテレータが要素を消費した場合、コレクションは再利用できない
code:rs
struct Thing(u64);
for item in collection {
println!("Consumed item {item:?}")
}
println!("Collection = {collection:?}")
code:rs
for item in &collection {
println!("Consumed item {item:?}")
}
println!("Collection = {collection:?}")
上記のコードでは、コンパイラは &Collection に対する IntoIterator 実装を探す
標準的なコレクション型であれば、この実装を提供している
もちろん、 Thing に Copy トレイトを実装することでも回避可能
可変参照 に対して繰り返し処理を許容したい場合、for item in &mut collection が使える 標準で提供されているコンテナでは、内部要素への参照を与えるイテレータを返す iter() を(要素を変更してもいい場合は iter_mut() も)実装することが慣習になっている
これらのメソッドは for を用いると自動的に呼び出される が、イテレータ変換の開始点として使える ことが重要
e.g. let result: u64 = collection.iter().map(|thing| thing.0).sum();
上記のコードは以下と等価
let result: u64 = (&collection).into_iter().map(|thing| thing.0).sum();
イテレータ変換メソッド
Iterator トレイトにはイテレート処理を行う多くのメソッドが デフォルト実装 されている イテレート処理全体に影響を与える変換メソッド
take(n): 最大 n 個の要素のみを出力するイテレータに変換する
skip(n): 最初の n 個の要素をスキップするイテレータに変換する
step_by(n): n 個ごとに 1 つの要素だけを出力するイテレータに変換する
chain(other): 2 つのイテレータを結合し、1 つ目を処理した後に 2 つ目を処理するイテレータに変換する
cycle(): 終了するイテレータを無限に繰り返すイテレータに変換する
warning.icon 利用するには、イテレータが Clone を実装する必要がある
rev(): 向きを反転したイテレータに変換する
要素(Item)の性質に影響を与える変換メソッド
map(|item| {...}): 各要素に 1 つのクロージャを適用したイテレータに変換する
cloned(): 元のイテレータの各要素をクローンした要素を出力するイテレータに変換する
warning.icon 利用するには、Item 型が Clone を実装する必要がある
copied(): 元のイテレータの各要素をコピーした要素を出力するイテレータに変換する
warning.icon 利用するには、Item 型が Copy を実装する必要がある
enumerate(): 各要素に usize のインデックスを付与した ペア((usize, Item))を出力するイテレータに変換する zip(it): 2 つのイテレータそれぞれから得られる要素のペア(Item1, Item2)を出力するイテレータに変換する
要素をフィルタリングするメソッド
filter(|item| {...}): 条件を満たす要素のみを出力するイテレータに変換する
take_while(): 条件を満たす間だけ要素を出力するイテレータに変換する
skip_while() の逆
skip_while(): 条件を満たさなくなるまで要素をスキップし、その後の要素を出力するイテレータに変換する
take_while() の逆
flatten(): 要素自体がイテレータである場合、それを 平坦化 して出力するイテレータに変換する Option や Result はイテレータとして扱うことができる ため、flatten を用いることで有効な値のみを抽出できる
Some(value) / Ok(value) の場合、1 個の要素(value)を持つイテレータ
None / Err(err) の場合、要素を持たないイテレータ
e.g.
code:rs
let valid_values: Vec<_> = options.into_iter().flatten().collect();
イテレータ変換メソッド
Iterator トレイトにはイテレータを消費するメソッドも数多く用意されている
その中で最も 汎用的 なのが for_each(|item| {...}) メソッド このメソッドは、イテレータの各要素に対してクロージャを実行する
明示的な for ループでできることの大半が実現可能
しかし、クロージャ内で外部の状態を更新するには、可変参照を利用する必要がある
code:rs
let mut even_sum_squares = 0;
values
.iter()
.filter(|x| *x % 2 == 0)
.take(5)
.for_each(|value| { even_sum_squares += value * value; });
しかし、提供されているメソッドで実現可能であれば、そちらを用いた方がコードがコンパクトな Rust らしい(慣用的)コードとなる 1 つの値を生成するメソッド
sum(): 数値(整数または浮動小数点数)のコレクションを合計する
product(): 数値のコレクションを掛け合わせる
min(): Item の Ord 実装を用いて、コレクションの最小値を取得する
max(): Item の Ord 実装を用いて、コレクションの最大値を取得する
min_by(f): ユーザが指定した関数 f に基づいたコレクションの最小値を取得する
max_by(f): ユーザが指定した関数 f に基づいたコレクションの最大値を取得する
reduce(f): これまでの累積値と現在の要素を引数に取るクロージャ f を実行することで、Item 型の累積値を生成する
for_each 同様汎用性の高い関数で、上記のメソッドもこれで実現できる
fold(f): これまでの累積値と現在の要素を引数に取るクロージャ f を実行することで、任意の型の 累積値を生成する
reduce メソッドをより 汎化 したものと言える scan(init, f): 内部状態の可変参照と現在の要素を引数に取るクロージャ f を実行することで、任意の型の累積値を生成する
このメソッドも reduce を別の方向から汎化したものと言える
単一の値を選択するメソッド
find(p): 述語関数 を満たす最初の要素を返す position(p): 述語を満たす最初の要素のインデックスを返す
nth(n): イテレータ内の n 番目の要素が存在すれば、それを返す
すべての要素に対してテストを行うメソッド
下記の 2 つのメソッドは、反例が見つかった時点でイテレート処理を終了する
any(p): 述語関数を満たす要素が 1 つでも存在すれば true を返す
all(p): すべての要素が述語関数を満たせば true を返す
クロージャ内のエラーを許容するメソッド
下記の 3 つのメソッドは、クロージャがエラーを返した時点でイテレート処理を終了し、最初のエラーを返す
try_for_each(f): for_each のように動作するが、クロージャがエラーを返すことが可能
try_fold(f): fold のように動作するが、クロージャがエラーを返すことが可能
try_find(f): find のように動作するが、クロージャがエラーを返すことが可能
イテレータのすべての要素から新しいコレクションを生成するメソッド
FromIterator は標準ライブラリのすべてのコレクション型で実装されている
e.g. HashMap や BTreeSet ...
そのため、明示的に型を指定する必要がある
code:rs
let myvec: Vec<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
let h: HashSet<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
unzip(): ペアを要素に持つイテレータを 2 つのコレクションに分割する
partition(p): 各要素に述語関数を適用し、それに基づいてイテレータを 2 つのコレクションに分割する
イテレータ変換メソッド用いることで、コードが以下のようなメリットが得られる
1. コードが Rust らし苦なる
2. コードがコンパクトになる
3. 実装者の意図が明確になる
これにより、範囲外の値にアクセスしようとすると panic が発生する
イテレータの場合はこのチェックが必要ないため、明示的な for ループと比べて効率的になる 可能性がある
warning.icon ただし必ずしも効率的であるとは限らない
Rust のコンパイラは、スライスへのアクセスが安全であることをコードから確認できる場合、境界チェックをスキップするため
Result 値からコレクションを作る Tips
collect メソッドでは、Result を保持したコレクション型にも変換できる
e.g. 要素の型が i64 のベクタを u8 に変換する処理
code:rs
let result: Vec<u8> = inputs
.into_iter()
.map(|v| <u8>::try_from(v))
.collect::<Result<Vec<_>, _>>()?;
これを ? と組み合わせることで、以下のような望ましい振る舞いを実現できる
繰り返し実行中にエラーが起きた場合、繰り返しを中止して呼び出し元にエラー値を返す
エラーが起きなかった場合、以降のコードは正しい型の値のコレクションを利用できる
明示的なループ処理が望ましいケース
1. ループの本体が大きく複雑なケース
∵ クロージャに詰め込むと 可読性 が低下するため 2. ループの本体内でエラーが発生するケース
ループ内でエラーが発生した際に早期 return を行う場合には、明示的なループのままの方が良いケースが多い
∵ 可読性 や 柔軟性 を確保できるため radish-miyazaki.icon ただし、collect() を用いて Result を一括処理できる場合は、? を用いてエラー処理を簡素化することが可能
クロージャを用いたイテレータ変換が、明示的なループと同程度に高速になるように最適化(チューニング)が必要
パフォーマンス測定 をする際には、コンパイラが最適化をして楽観的な結果を出す ことを考慮して、テストデータが実際の運用状況を反映したものとなるようにした方が良い 重要なのは、「無理にループをイテレータ変換に書き換えたりしないこと」
最終的には好みの問題でもある