Rustにおいて、データのコレクションから特定の条件を満たす要素だけを抽出したい場面は非常に多くあります。
そのような場合に最も頻繁に利用されるのが、Iteratorトレイトで定義されているfilterメソッドです。
本記事では、Rustを始めたばかりの方から実務で活用したい方までを対象に、filterメソッドの基本的な使い方から応用的なテクニックまでを詳しく説明します。
Rustのfilterメソッドとは
filterメソッドは、イテレータの各要素に対してクロージャ(匿名関数)を適用し、その結果がtrueである要素のみを維持する新しいイテレータを生成します。
このメソッドは、Rustの強力な抽象化機能である「イテレータ(Iterator)」の一部として提供されています。
大きな特徴の一つとして、filterは「遅延評価(Lazy Evaluation)」の性質を持っています。
これは、filterを呼び出した時点では要素の抽出は行われず、collectやforループなどで実際に要素が必要になったタイミングで初めて実行されることを意味します。
この仕組みにより、不必要な計算を避けつつ、メモリ効率の高いデータ処理が可能になります。
filterメソッドの基本的な構文と使い方
まずは、数値のベクタ(Vec)から特定の条件に合致する値を取り出す簡単な例を見てみましょう。
以下のコードは、数値のリストから偶数だけを抽出するサンプルです。
fn main() {
// 数値のベクタを作成します
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// filterメソッドを使用して偶数のみを抽出します
// collect()を使って結果を新しいベクタに格納します
let even_numbers: Vec<&i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.collect();
println!("偶数のリスト: {:?}", even_numbers);
}
偶数のリスト: [2, 4, 6, 8, 10]
このコードでは、iter()メソッドで不変の参照を生成するイテレータを作成しています。
filter内のクロージャ |&&x| x % 2 == 0 が各要素に対して評価され、条件を満たすものだけが残ります。
最後に collect() を呼び出すことで、イテレータを具体的なコレクション(この場合はVec)に変換しています。
参照の扱いに注意する
filterメソッドを使う際に多くの開発者が最初につまずくポイントが、参照の扱いです。
filterに渡されるクロージャの引数は、イテレータが生成する要素の参照(&Item)になります。
例えば、iter() を使って i32 型のベクタを回している場合、イテレータが返す要素は &i32 です。
filter メソッドはさらにその参照を渡すため、クロージャの引数の型は &&i32 (参照の参照)となります。
これを解決するためには、先ほどの例のように |&&x| とパターンマッチングを使ってデリファレンスするか、|x| **x % 2 == 0 のように明示的に参照を外す必要があります。
この挙動を正しく理解しておくことは、コンパイルエラーを防ぐために非常に重要です。
構造体のフィルタリングと実用的な例
数値だけでなく、独自の構造体(struct)に対しても filter は強力な効果を発揮します。
ユーザー情報のリストから、特定の条件(例:20歳以上かつアクティブなユーザー)を満たす人を抽出する例を考えてみましょう。
#[derive(Debug)]
struct User {
name: String,
age: u32,
is_active: bool,
}
fn main() {
let users = vec![
User { name: "Alice".to_string(), age: 25, is_active: true },
User { name: "Bob".to_string(), age: 17, is_active: true },
User { name: "Charlie".to_string(), age: 30, is_active: false },
User { name: "Dave".to_string(), age: 22, is_active: true },
];
// 20歳以上かつアクティブなユーザーのみを抽出します
let active_adults: Vec<&User> = users.iter()
.filter(|u| u.age >= 20 && u.is_active)
.collect();
for user in active_adults {
println!("{:?} は条件を満たしています。", user.name);
}
}
"Alice" は条件を満たしています。
"Dave" は条件を満たしています。
このように、複数の論理演算子(&& や ||)を組み合わせることで、複雑な条件での抽出が可能です。
filter 内で u.age のようにフィールドにアクセスできるため、可読性の高いコードを記述できます。
外部変数のキャプチャ
filter のクロージャは、スコープ外にある変数をキャプチャすることも可能です。
例えば、ユーザーが入力した閾値に基づいてリストをフィルタリングする場合などが該当します。
fn main() {
let threshold = 50;
let scores = vec![10, 45, 60, 85, 30, 95];
// 外部変数 threshold を使用してフィルタリングします
let high_scores: Vec<i32> = scores.into_iter()
.filter(|&s| s > threshold)
.collect();
println!("合格点以上のスコア: {:?}", high_scores);
}
この例では into_iter() を使用しているため、所有権が移動し、クロージャへの引数は &i32 となります。
キャプチャした threshold 変数は不変参照として利用されるため、Rustの所有権システムに則って安全に処理されます。
filterに関連する応用メソッド
filter 単体でも強力ですが、他のメソッドと組み合わせることでさらに高度なデータ処理が可能になります。
filter_map:抽出と変換を同時に行う
特定の条件に合う要素を抽出しつつ、同時に型を変換したい場合には filter_map が最適です。
filter_map は、クロージャが Some(value) を返したときだけその値を維持し、None を返したときはその要素を無視します。
fn main() {
let strings = vec!["10", "abc", "25", "hello", "50"];
// 文字列を数値に変換し、変換に成功したもの(Some)だけを抽出します
let numbers: Vec<u32> = strings.iter()
.filter_map(|s| s.parse::<u32>().ok())
.collect();
println!("数値への変換に成功した要素: {:?}", numbers);
}
数値への変換に成功した要素: [10, 25, 50]
このメソッドを使用すると、filter して map するという二段階の手間を一度に済ませられるため、コードがスッキリします。
エラー処理(Result型)をOption型に変換してフィルタリングする手法は、Rustにおける定番のパターンです。
find:最初の一致だけを探す
全要素を抽出するのではなく、条件に一致する最初の1つだけが必要な場合は find メソッドを使用します。
find はイテレータを消費し、最初に見つかった要素を Option<Item> 型で返します。
fn main() {
let numbers = vec![1, 3, 5, 8, 9, 10];
// 最初に見つかった偶数を取得します
let first_even = numbers.iter().find(|&&x| x % 2 == 0);
match first_even {
Some(n) => println!("最初に見つかった偶数: {}", n),
None => println!("偶数は見つかりませんでした"),
}
}
パフォーマンスの観点では、条件に合致した瞬間に探索を終了するため、大規模なデータセットにおいて filter().next() を使うよりも意図が明確になります。
filterメソッドとパフォーマンスの検討
Rustのイテレータ設計は「ゼロコスト抽象化」を目標としています。
そのため、filter メソッドを多用しても、手書きの for ループと if 文による抽出と比較してパフォーマンスが劣化することはほとんどありません。
むしろ、コンパイラ(LLVM)によって強力な最適化がかかり、ループアンローリングなどが適用されることで、手書きよりも高速になるケースさえあります。
ただし、いくつかの注意点があります。
1. 中間イテレータの連鎖
filter を何重にも重ねることは可能ですが、条件のチェック回数が増えるため、可能な限り1つの filter 内で論理演算子を使ってまとめる方が効率的な場合があります。
2. メモリ確保のタイミング
collect() を呼び出す際、Rustは結果を格納するベクタの容量をあらかじめ予測することが困難です(フィルタリングによって何個残るか分からないため)。
そのため、抽出結果が多い場合は動的な再確保が発生する可能性があります。
もし結果の最大数が分かっている場合は、with_capacity であらかじめメモリを確保しておくなどの対策が検討できます。
Vec::retainとの違い
filter と似た機能を持つメソッドに、Vec::retain があります。
これらには決定的な違いがあるため、使い分けが必要です。
| 機能 | filter (Iterator) | retain (Vec) |
|---|---|---|
| 操作の対象 | イテレータ(元のデータは不変) | ベクタ(元のデータを直接書き換える) |
| 戻り値 | 新しいイテレータ | なし(void / 破壊的変更) |
| メモリ効率 | 新しいコレクションを作成する | インプレース(既存のメモリを再利用) |
元のベクタをそのまま加工して、不要な要素を削除したい場合は retain を使うのが最も効率的です。
一方で、元のデータを残したまま特定の条件で抽出したリストを別に作りたい場合や、他のイテレータ処理(mapなど)と繋げたい場合は filter が適しています。
よくあるミスとデバッグのコツ
filter を使っている際に遭遇しやすい問題として、「副作用を持つ関数の呼び出し」があります。
原則として、filter のクロージャ内で外部の状態を書き換えたり、画面に出力したりする副作用を持たせるべきではありません。
前述の通り filter は遅延評価されるため、副作用が期待したタイミングで発生しない可能性があるからです。
デバッグのために要素を確認したい場合は、inspect メソッドを利用することを検討してください。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let result: Vec<i32> = numbers.into_iter()
.inspect(|x| println!("評価中: {}", x)) // フィルタリング前の値を確認
.filter(|x| x % 2 == 0)
.collect();
println!("結果: {:?}", result);
}
inspect を挟むことで、どの要素が filter に渡され、どの要素が落とされたのかを流れの中で確認することができます。
まとめ
Rustの filter メソッドは、安全かつ効率的にデータを操作するための基本でありながら非常に強力なツールです。
イテレータの遅延評価の仕組みを活用することで、大規模なデータセットでもパフォーマンスを維持したまま記述できます。
特に参照の扱い(& と && の違い)に慣れるまではコンパイルエラーに戸惑うこともあるかもしれませんが、型システムを理解すれば強力な武器になります。
今回紹介した filter_map や retain との使い分けを意識しながら、よりクリーンでRustらしいコードを記述していきましょう。
条件付き抽出をマスターすることは、Rustにおける関数型プログラミング的な側面を楽しむ第一歩となります。
