はじめに
反復子を使った一連の項目の処理へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、反復子を使って一連の項目を処理する方法を探ります。反復子は遅延評価で、自分たちでロジックを再実装することなく、項目のシーケンスを反復処理することができます。
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 から始まる変数を使って実装します。この変数を使ってベクトルにインデックスを指定して値を取得し、ループ内で変数の値をインクリメントしていき、ベクトル内の要素の総数に達するまで繰り返します。
反復子はそのようなロジック全てを代行してくれるため、間違える可能性のある繰り返しコードを減らすことができます。反復子を使えば、ベクトルのようにインデックスを指定できるデータ構造だけでなく、多くの種類のシーケンスに対して同じロジックを使う柔軟性が増えます。では、反復子がどのようにそれを行うのか見てみましょう。
すべての反復子は、標準ライブラリに定義された Iterator
という名前のトレイトを実装しています。このトレイトの定義は次のようになっています。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// デフォルト実装が省略されたメソッド
}
この定義ではいくつかの新しい構文が使われていることに注意してください。type Item
と Self::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 でさらに多くの実験を行って練習してください。