sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

空のシーケンスで Min/Max/Average を使うときの注意点

空のシーケンスで Min/Max/Average を使うときの注意点

コレクションの要素が Where 句の内容などの絞り込みの結果、要素が空になってしまったとき、 Min(), Max(), Average() といったシーケンスの最小値、最大値、平均値を計算すると InvalidOperationException の例外が発生してしまいます。

具体的には、以下のコードのとおり。
where 句の結果、要素が0件になってしまい Average の実行で InvalidOperationException が発生します。
これをどう対処するのか、というのがこの記事の内容です。

public class Hello
{
    public static void Main()
    {
        var samples = new List<int>()
        {
            1,2,3,4,5,6,7,8,9,10
        };

        // System.InvalidOperationException: 
        // Sequence contains no elements
        var avg = samples.Where(p => p > 10)
                         .Average(p => p);

        System.Console.WriteLine($"{avg}");
    }
}

補足として、シーケンスの要素の型が null を許容する型の場合は例外ではなく null の値を返却します。以下の場合だと avg の値は null になり、最後の出力は -1 。
つまり、この記事は、値型だけの話になります。

public class Hello
{
    public static void Main()
    {
        var samples = new List<int?>()
        {
            1,2,3,4,5,6,7,8,9,10
        };

        var avg = samples.Where(p => p > 10)
                         .Average(p => p);

        System.Console.WriteLine($"{avg ?? -1}");
    }
}

対策

要素が存在しない可能性があるときは DefaultIfEmpty() を間にはさみます。
事前の LINQ 結果が空になったとき、要素型のデフォルト値、または、指定した値を渡します。

以下の場合だとコレクションの型は int 型なのでデフォルト値は「0」です。

public class Hello
{
    public static void Main()
    {
        var samples = new List<int>()
        {
            1,2,3,4,5,6,7,8,9,10
        };

        var avg = samples.Where(p => p > 10)
                         .DefaultIfEmpty()
                         .Average(p => p);

        System.Console.WriteLine($"{avg}");
    }
}

その他(補足)

いちろぐ - LINQ - 空のシーケンスでMin/Max/Averageを使ったときのメモ」では、try-catch 句で例外をキャッチする対策を指摘しています。

public class Hello
{
    public static void Main()
    {
        var samples = new List<int>()
        {
            1,2,3,4,5,6,7,8,9,10
        };

        try
        {
            var avg = samples.Where(p => p > 10)
                             .Average(p => p);

            System.Console.WriteLine($"{avg}");
        }
        catch(Exception e)
        {
            // ERROR
            System.Console.WriteLine($"{e}");
        }
    }
}

大体の場合では、例外処理は、すこし大げさではないかと思います。

例外は「通常はありえない」ことが起きたときに処理されるブロックです。別の言い方だと「メソッドの定める結果を達成できないなら例外を投げる」というのもあります。

where 句の結果、0件というのは、「想定できる」こと、または、「通常ありえる」なら DefaultIfEmpty() を利用したほうが適切である可能性は高そうです。
where 句の結果、0件になると、「定める結果を達成できない」なら例外でもよいのかもしれません。

気軽な例外にならないように注意。

参考