【C#】Where(predicate).Count () > 0じゃなくて、Any(predicate)を使おう【LINQ】


はじめに

 「Where(predicate).Count () > 0じゃなくて、Any(predicate)を使おう」というお話です。

 LINQ最高ですよね!ですよね、ですよね!!!さて自分が最初に使い始めたのものの中に、WhereとCountがあります。これらを組み合わせて次のような事をやれます。

 「リストや配列の中に特定の条件を満たす要素が一つでも存在するか調べる」

 例えば、List<int>型のリストの中に0以上の数が少なくとも一つ存在するかどうか調べるなどですね。これをWhereとCountを用いて次のように書く事"も"できます。(下記よりもっといい書き方がありますが。)

List`型のリストの中に0以上の数が少なくとも一つ存在するかどうか調べる(1)
List<int> intList = LoadIntList ();

bool isExist = intList
    .Where (num => num >= 0) // Whereで0以上の要素のみにフィルタリング
    .Conut () > 0; // Countでフィルタリング後の要素数を数えて0より大きければ、対象が存在する

 上記のコード中のコメントにも記載しましたが、まずWhereメソッドで0以上の要素のみにフィルタリングします。フィルタリング後の要素数をCountで数えます。数が0より大きければ、対象が存在するということになりisExistはtrueになります。

 さきほどのコードに比べて、次に示すAnyというLINQのメソッドを使う方がいいです。

List`型のリストの中に0以上の数が少なくとも一つ存在するかどうか調べる(2)
List<int> intList = LoadIntList ();

bool isExist = intList.Any (num => num >= 0);

 Anyメソッドは引数で渡した条件を満たす要素が一つでも存在すればtrueを返し、存在しない場合falseを返します。上記のコードではAnyメソッド一つで、リストの中に0以上の数が少なくとも一つ存在するかどうか調べることが可能です。

 Where(predicate).Count () > 0Any(predicate)、どっちがいいと思いますか。自分は断然Any(predicate)の方を推します。この投稿では、Where(predicate).Count () > 0でなくて、Any(predicate)を使うべき理由を紹介します。

簡潔だし何をやりたいか伝えやすい

 LINQを使うメリットに「何がしたいのかが読みやすい」ということがあります。LINQは「どう処理するかでなくて、何をしたいかが書いてある」のです。

 Where(predicate).Count () > 0を使ってやりたいことってなんでしょうか?言い方はいろいろでしょうが、「ある条件を満たす要素が一つでも存在するか調べる」ではないでしょうか?

 Where(predicate).Count () > 0を使って書いたコードを再掲します。

List`型のリストの中に0以上の数が少なくとも一つ存在するかどうか調べる(1)
List<int> intList = LoadIntList ();

bool isExist = intList
    .Where (num => num >= 0)
    .Conut () > 0;

 どう読めますか?確かに、やっている事は0以上の要素が存在するかを調べていますね。ですがこれを読むときは、「Whereメソッドで0以上の要素のみにフィルタリングし、その結果の要素数をCountで数え、0より大きければ対象が存在する」と読んだ後、それから頭のなかでそれを変換して、「ああ、ここでは0以上の要素が存在するかを調べてるのね」と理解するのではないでしょうか?

 次にAnyを使ったコードを再掲します。

List`型のリストの中に0以上の数が少なくとも一つ存在するかどうか調べる(2)
List<int> intList = LoadIntList ();

bool isExist = intList.Any (num => num >= 0);

 Anyメソッドを知ってさえすれば、これは素直に「リストの中に0以上の数が少なくとも一つ存在するかどうか調べる」と読めます。

 for、foreach、ifで書いたコードは、どう処理するか書かれています。コードを読む際はそれを変換して意図を理解する必要があります。一方でLINQを使って書いたコードは「何をしたいかが書いてある」コードになります。

 ところがWhere(predicate).Count () > 0はLINQで書かれてはいますが微妙に「何をしたいか」が伝わりづらいのです。Where(predicate).Count () > 0はfor、foreach、ifで書かれたコードほどではありませんが、どのように処理するかを述べているのです。確かに条件を満たすかどうか調べることができます。しかしどちらかと言うと、どう調べるかが書いてあるのです。

 「ある条件を満たす要素が一つでも存在するか調べる」というのはAnyメソッドそれだけを用いて記述する事ができます。そしてAnyメソッドで書いてあれば、「ある条件を満たす要素が一つでも存在するか調べる」という意図を明示的に伝える事ができます。

 Anyメソッドで記述する方が読みやすいでしょう?

無駄に数えない、無駄なことしない

 まず前提として、LINQのメソッドは配列やリストだけが使えるわけではありません。セットやディクショナリでも使えます。IEnumerable<T>のインターフェースを実装していればLINQのメソッドが使えます。(正確にはLINQ to Objects。また一部IEnumerableを実装しているメソッドで使えたり、Enumerableクラスのクラスメソッドだったりします)

 さて、質問です。

 無限に0がつづくシーケンス(IEnumerable<int>)があります。この要素の中に0以上の要素は存在しますか?

 ちょっとバカらしい質問ですね。答えは当然、「存在する」でしょう。じゃあこれをLINQで書いてみましょう。

要素が0の無限に続くシーケンス中に、0以上の要素が存在するか調べる
// 0が無限に続くシーケンスを生成する
static IEnumerable<int> CreateInfiniteZeroSequence ()
{
    while(true) {
        yield return 0;
    }
}

public static void Main (string[] args)
{
    // 要素が0の無限に続くシーケンス中に、0以上の要素が存在するか調べる
    bool isExist = CreateInfiniteZeroSequence().Any(num => num >= 0);
}

 isExistは当然trueになります。

 上記のコードはAnyを使っていますが、WhereとCountを使った場合はどうでしょうか?

public static void Main (string[] args)
{
    // 要素が0の無限に続くシーケンス中に、0以上の要素が存在するか調べる
    bool isExist = CreateInfiniteZeroSequence()
        .Where(num => num >= 0)
        .Count() > 0;
}

 どうなると思いますか?trueになると思いますか?先頭の要素が0ですよね。だから0以上の要素が存在するので、trueになりそうですよね。

 しかしWhere+Countを使った場合、System.OverflowExceptionが発生します。Whereでフィルタリングしますが全ての要素が条件に合致します。そしてそれをCountで数えます。無限に続くシーケンスを数えようとしたらどうなると思いますか?数えるの要素は無限に続きます。いつまでたっても数える行為は終わりません。確かにSystem.OverflowExceptionが発生しそうですよね?

 では、なぜAnySystem.OverflowExceptionが発生しないのでしょうか?実はAnyは条件を満たす要素が存在した時点で、処理を終えるのです。先ほどの例では、先頭要素が0ですよね。そのため先頭要素を調べた時点で処理を終えるです。無限に続くシーケンスですが、Anyは先頭を調べた時点で終えることができるのです。

 別の例を示します。次の二つのコードは、あるパス(path)に対象ファイル名(targetFileName)のファイルが存在するかどうか調べるコードです。片方はWhere+Countを使っていて、もう片方はAnyを使っています。どちらがいいコードでしょうか?

 ここでのミソは、Directory.EnumerateFilesメソッドは対象パスのファイルを一気に読み込むのではなくて、一つずつ列挙するメソッドであるということです。

Where+Count版
string path = GetTargetPath();
string targetFileName = GetTargetFileName();

bool isExistTargetFileOnPath = Directory
    .EnumerateFiles(path)
    .Where(fileName => fileName == targetFileName)
    .Count () > 0;
Any版
string path = GetTargetPath();
string targetFileName = GetTargetFileName();

bool isExistTargetFileOnPath = Directory
    .EnumerateFiles(path)
    .Any(fileName => fileName == targetFileName);

 このコードでもAnyを使うべきです。Where+Countは対象のパスの全てのファイルを読み込む必要があります。ですがAnyでは目的のファイルがあった段階で処理を終えることができ、無駄にファイルを読み込む必要がありません。

 対象のパスに100000個ファイルがあったとして、目的のファイルが1個目で見つかったら、残り99999個を読み込むのは無駄でしょう?Where+Count版はその無駄なことをしてしまうのです。

まとめ

 ここまで読んで、「Where(predicate).Count () > 0じゃなくて、Any(predicate)を使おう」ということに納得していただけたでしょうか?

 まずAnyの方が意図を明確に伝えられますよね。

 そしてAnyであれば、対象の要素が存在した時点で処理が終わるので無駄な処理をする必要がありませんね。

 ご意見・ご質問・指摘、待っています!