LINQの中でフィルタリングを司るWhereの使い方と使用上の注意を解説する。
Contents
Whereの役割
Whereの役割はコレクションをフィルタリングすることだ。例えば数字の入った配列があったとして、その中から偶数だけを取り出したいときなどに使える。
役割が単純な分使いやすく、使用機会も多い。これは裏を返せばつい無駄なところでも使ってしまうということ。今回の記事の後半ではWhereを使わなくても良いパターンを紹介する。
Whereの仕様と具体例 1
さて、Whereの仕様を見てみよう。Whereには2つのオーバーロードがあるが、まずはシンプルなこちらから。
1 2 3 |
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate) |
コレクション(source)に対し、その要素(predicateの引数)をフィルターに通すかどうか(predicateの返り値)を記述する。入力と出力が非常にハッキリしていて分かりやすい。predicateが取る引数はコレクションの一要素で、この要素がフィルタリング条件にマッチしているならtrue、そうでないならfalseを返すようpredicateを書こう。
今、Nameというプロパティを持つPersonなるクラスがあり、Tanakaという名前の人でフィルタリングする場合を考えるとこうなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var people = new Person[] { new Person { Name = "Tanaka" }, new Person { Name = "Kato" }, new Person { Name = "Ota" }, new Person { Name = "Tanaka" } }; // メソッド形式での使用例 var tanakas = people.Where(person => person.Name == "Tanaka"); // クエリ形式での使用例 var tanakas2 = from person in people where person.Name == "Tanaka" select person; |
先ほど紹介したように、personを引数にとり、person.NameがTanaka、すなわちTanakaという名前の人だけをフィルターに通すよう指定した。
メソッド形式で書いてもクエリ形式でも結果は同じだけれど、個人的にはメソッド形式の方が使いやすいように感じる。
Whereの仕様と具体例 2
次に紹介するWhereのオーバーロードは先程のものより少し複雑になっている。
1 2 3 |
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, int, bool> predicate) |
違いはpredicateが第二引数として要素のインデックスを取るということだ。コレクションをフィルタリングするとき、要素のインデックスを条件にしたいときがたまにある。そんな時にこちらのWhereを使おう。
先ほどの具体例と同じくPersonクラスのインスタンスを複数持つPeopleというコレクションをフィルタリングしてみよう。二番目以降(インデックスが1以上)の人だけを取り出してみる。
1 2 3 |
const int TargetIndex = 1; var peopleAfterTargetIndex = people.Where((person, index) => index >= TargetIndex); |
Whereを使わなくてもいいパターン3つ
このようにお手軽にフィルタリングが出来るWhereだけれど、使わなくてもいいパターンが存在する。それは、Whereと他のメソッドを組み合わせる時だ。一つづつ見ていこう。
条件に合う要素の数を数える – Count
フィルタリングにマッチした要素の数を数えるときが一つ目。Tanakaという名前の人が何人いるかを数えたいとき、要素の数を数えるCountというメソッドと組み合わせるとこうなる。
1 2 3 |
var tanakaCount = people .Where(person => person.Name == "Tanaka") .Count(); |
フィルタリングして(Where)、数を数える(Count)。とても素直なコードだと思うけれど、実はWhereは無くてもいい。Countにフィルタリング条件を指定できるからだ。
1 2 |
var tanakaCount2 = people .Count(person => person.Name == "Tanaka"); |
このようにCountを呼び出すだけで良くなる。
「Whereでフィルタリング条件を指定しなくていい=Whereがいらない」という点はこの後でも紹介するパターンでも同じだ。
条件に合う要素を1つだけ取得する – First / FirstOrDefault
LINQの中でかなり出現頻度が高いのがこのパターンだ。Whereと同じLINQメソッドであるFirstあるいはFirstOrDefaultを使えば、コレクションの最も先頭の要素を取得できる。これをWhereと組み合わせ、「フィルタリング後のコレクションから先頭要素だけを取ればいいんじゃん!」となるけれどここでお立ち会い。先ほどの例と同じく、First / FirstOrDefaultにも直接フィルタリング条件を指定できるのだ。
1 2 3 4 5 6 7 8 |
// アンチパターン2 Where + First / FirstOrDefault var tanaka = people .Where(person => person.Name == "Tanaka") .FirstOrDefault(); // FirstOrDefaultだけにできる。 var tanaka2 = people .FirstOrDefault(person => person.Name == "Tanaka"); |
Firstは要素がないときに例外を飛ばし、FirstOrDefaultはnullを返す。コレクションに要素があるかどうか(あるいはフィルタリング条件に合った要素があるかどうか)は分からないため、極力FirstOrDefaultを使うことをオススメする。Firstが原因のバグをいくつ見てきたことか……。
条件に合う要素があるかを判定する – Any
コレクションに要素があるかどうかを判定するAnyなるLINQメソッドがある。これをWhereと組み合わせれば、条件に合う要素があるかどうかが分かると思うかもしれない。それはその通りだけれど、例のごとくAnyに条件を指定できる。
1 2 3 4 5 6 7 8 |
// アンチパターン3 Where + Any var tanakaExist = people .Where(person => person.Name == "Tanaka") .Any(); // Anyに直接条件を指定すればOK var tanakaExist2 = people .Any(person => person.Name == "Tanaka"); |
余談 ResharperやRiderならLINQのアンチパターンを指摘してくれる
我々エンジニアは覚えることが多い。その上このようなアンチパターンを一々覚えていくのは大変じゃないだろうか? TOACHでも何回か紹介しているResharperやRiderを使えば、このようなLINQのアンチパターンを指摘してくれる。ResharperはVisual Studioの拡張機能で、C#開発を爆速にしてくれる。Riderは、Resharperの開発元であるJetBrains社が提供している、C#専用のクロスプラットフォームIDEだ。
例えばWhere + CountをCountだけにできるケース。Riderを使えば問題箇所に下線を引いて、ポップアップで改善案「Countだけにしようぜ〜」ということを言ってくれる。その上、改善まで自動でやってくれるから驚きだ。
C#erなら足を向けては寝られないJetBrains。そしてそのプロダクトであるResharperとRider。これを紹介した記事はこちら。
Rider : 期待度MAXな次世代のC#開発環境 Rider を使ってみた。
Resharper : [永久保存版] C#のイマドキな開発環境はこれなのですぞ
LINQの豊富な実用例を知りたい人にはこれがオススメ