LINQ、そのWhere本当に必要ですか?


プログラミング言語 C#
今まで使えていた環境以外にも利用できる環境が広がっています。
Unityゲームエンジンで様々なプラットフォーム向けのゲームを開発できたり、Xamarinを用いて各モバイルプラットフォーム向けアプリを開発することが可能です。

さて、そのC#の中の強力な機能、LINQ。
Xamarinの紹介サイトの一部でも、LINQは結構推されています。LINQ、便利ですよね。for文やif文、途中の変数が無くなりコードがスッキリします。

さて、LINQを使っていると、Whereを使い要素をフィルタリングすることが多いと思います。

フィルタリングや条件を満たすものだけを対象とするということは、それ単体だけでなく、その後にそれを変換したり、グループ化したり、並び替えたりするかもしれません。また、数えたり、存在確認をしたりもすることもあるでしょう。

このような処理は、Whereメソッドでフィルタリングした後、さまざまなLINQメソッドを利用すれば実現できます。

しかし、LINQの各メソッドの多くはオーバーロードを持っていて、そのいくつかあるオーバーロードを適切に使えば、Whereメソッドを省略することができるかもしれません。

Whereはフィルタリングをする

まずは、Whereメソッドのサンプルを見てみます。
次のサンプルコードを実行すると、0から9を出力します。

0から9を出力
var numbers = Enumerable.Range(0, 10);

foreach(var num in numbers) {
    Console.WriteLine (num);
}

これを奇数だけ出力するように変更します。
LINQを使わない場合はif文が必要ですが、LINQのWhereメソッドを使えばより簡潔にコードを書けますね。

0から9の内、奇数のみを出力
var numbers = Enumerable.Range(0, 10);

foreach(var num in numbers.Where(n => n % 2 == 1)) {
    Console.WriteLine (num);
}

もう一例行きます。
次のようなクラスを使います。

サンプルクラス、Personクラス
public class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
}

このPersonクラスのリストを作ります。

Personクラスのリスト
var persons = new List<Person> {
    new Person { Name = "Taro", Age = 25},
    new Person { Name = "Jiro", Age = 22},
    new Person { Name = "Saburo", Age = 19},
    new Person { Name = "Shiro", Age = 16},
};

これをWhereメソッドでフィルタリングして、20歳以上の名前を出力します。

Whereでフィルタリング
var persons20OrOlder = persons.Where( person => person.Age >= 20);

foreach(var person20OrOlder in persons20OrOlder) {
    Console.WriteLine (person20OrOlder.Name);
}

TaroとJiroと出力されたと思います。

さて今回の二つのサンプルでは、Whereでフィルタリングした結果をコンソールに出力しました。

これをメソッドの返り値としてIEnumerable<T>型として返したり、ToArrayメソッドやToListメソッドでインスタンス化して返すこともあると思います。

それ以外のWhereメソッドの使い方としては、フィルタリングして終わりではなくて、その結果をSelectメソッドで変換をしたり、GroupByメソッドでグループ化したり、しぼった結果をOrderByメソッドで並び替えたりすることもできますね。

さてもう少し他の例を考えてみます。次の3個の例はどうでしょう?
Whereでフィルタリングした後にその結果を、

  • Countで条件を満たした要素を数える
  • Firstで条件を満たした最初の要素を得る
  • Anyで要素があるか調べてIEnumerable<T>全体で条件を満たすか調べる

実は上の3例では、Whereメソッドはいりません!
どうすればいいのでしょうか、実は上の3つのメソッドのは、デリゲートを引数にとるオーバーロードを持っていて、それを使えば良いのです。

条件を満たす要素を数えるのに、Whereはいらない。Countメソッド

先ほどのPersonクラスを使います。
Personのリストがあり、その中で20歳以上の人数を数えてみます。
LINQを使わない場合、次のような形になるのでしょうか。

LINQを使わず20歳以上の人数を数える
var persons = new List<Person> {
    new Person { Name = "Taro", Age = 25},
    new Person { Name = "Jiro", Age = 22},
    new Person { Name = "Saburo", Age = 19},
    new Person { Name = "Shiro", Age = 16},
};

int count = 0;
foreach (var person in persons) {
    if (person.Age >= 20) {
        count++;
    }
}

Console.WriteLine ("20歳以上は" + count + "人です。");

冗長ですね。無駄なループ、条件式があります。count変数も、初期化したり処理の途中で値が変わったりしますね。(このくらいなら簡単ですが)どう処理しているかの記述から、何がしたいか読み取る必要がありますね。

これをLINQを使って書き換えてみます。Whereメソッドと要素を数えるCountメソッドを使います。

LINQ、WhereとCountを使って20歳以上の人数を数える
int count = persons.Where ( person => person.Age >= 20).Count ();
Console.WriteLine ("20歳以上は" + count + "人です。");

WhereメソッドとCountメソッドを使うことで、for文やif文が無くなりましたね。count変数も初期化の際に一度代入した値から変わっていません。Whereで指定した条件の要素をCountするというコードは、何がしたいか直接的に読み取ることができます。

LINQを使ってコードがスッキリしました。

めでたし、めでたし。

ちょっと待ってください。実はこのコードもっと短くなります。

Countメソッドで20歳以上の人数を数える
int count = persons.Count (person => person.Age >= 20);
Console.WriteLine ("20歳以上は" + count + "人です。");

なんと一つのメソッドで、IEnumerable<Person>型のpersonsの要素内の20歳以上の人数を数えることができました。

実はCountメソッドは二つのオーバーロードを持っています。

一つ目は引数を持たないオーバーロードです。これは単純にIEnumerable<TSource>の要素数を数えます。

二つ目はFunc<TSource, Boolean>型のデリゲートを引数にとるオーバーロードです。こちらは、IEnumelable<TSource>の要素の中で、引数に渡したデリゲートを適用し、真になる個数を数えます。

CountメソッドはIEnumelable<T>型の要素を数えます。
Whereをした後にCountをすることで、ある条件を満たす要素を数えることもできますが、デリゲートを引数にとるオーバーロードの方のCountメソッドを使えば、より簡潔に記述することができます。

条件を満たした要素を1つ取得したい場合、First/FirstOrDefaultだけでいい

FirstメソッドとFirstOrDefalutメソッドはIEnumerable<T>の先頭要素を取得します。(Firstメソッドは要素が無い場合、例外が発生し、FirstOrDefalutは規定値が帰ります。)

前述のPersonクラスのリストpersonsから、特定条件を見たす最初の要素を取得します。

Whereを使ってフィルタリングして、Firstで先頭を取得してみます。

Firstメソッドで20未満を取得(その1)
var persons = new List<Person> {
    new Person { Name = "Taro", Age = 25},
    new Person { Name = "Jiro", Age = 22},
    new Person { Name = "Saburo", Age = 19},
    new Person { Name = "Shiro", Age = 16},
};

Person personUnder20 = persons.Where (person => person.Age < 20).First ();
Console.WriteLine (personUnder20.Name);

Saburoと表示されたと思います。
さて、実はFirstメソッド/FirstOrDefaultメソッドも引数をとらないオーバーロードとFunc<TSource, Boolean>を引数にとるオーバーロードがあります。後者を使ってみます。

Firstメソッドで20未満を取得(その2)
Person personUnder20 = persons.First (person => person.Age < 20);
Console.WriteLine (personUnder20.Name);

引数を取らないFirstメソッドを使う場合、Whereをした後にFirstメソッドを書く必要がありました。

しかし引数ありのオーバーロードを用いれば、Firstメソッドのみで目的を達成することがあります。引数に、要素が満たして欲し条件を渡せばOKです。

条件を満たす要素があるかチェックはAnyだけでいい、Whereはいらない

Anyメソッドは、IEnumerable<T>に要素が一つでも存在すればtrueを、空ならばfalseを返します。

特定の条件を満たす要素が存在するかどうかを調べるには、Whereで条件を指定しフィルタリングした後、Anyで存在チェックをすれば可能です。

さて、このメソッドもFunc<TSource, Boolean>を引数にとるオーバーロードもあります。前の例と同じ用にこちらを使えば、更に簡潔に記述できます。

WhereとAnyで条件を満たす要素が存在するかチェック
bool isThereOver30 = persons.Where (person => person.Age > 30).Any ();

Any単体でも記述できます。

Anyで条件を満たす要素が存在するかチェック
bool isThereOver30 = persons.Any (person => person.Age > 30);

すっきりしました。Whereはいりませんね。

まとめ

LINQの多くのメソッドはオーバーロードを持っています。
Count、First、Anyメソッドは、それ単体である条件を満たす要素を数えたり、先頭要素を取得したり、存在するかを調べることができます。
その前に、Whereメソッドは必要ありません。

これらの他にもLINQは便利なオーバーロードがありますので、確認してみたら、さらにコードをスッキリ記述できることもあるかと思います。