LINQのAllとAnyが空のシーケンスに対して返す値

LINQのAllとAnyが空のシーケンスに対して返す値についてのまとめです。仕様、実装、仕様の考察、C#での検証結果を記載します。忘れたときに見返すためのメモです。

言語

結論

LINQのAllAnyが空のシーケンスに対して返す値は、AlltrueAnyfalseです。

メソッド戻り値動作の解釈
Alltrueシーケンスに条件を満たさない要素が含まれている場合はfalse、それ以外の場合はtrueを返す。
空のシーケンスに条件を満たさない要素は存在しないのでtrueを返す。
Anyfalseシーケンスに条件を満たす要素が含まれている場合はtrue、それ以外の場合はfalseを返す。
空のシーケンスに条件を満たす要素は存在しないのでfalseを返す。
Any
(引数
なし)
falseシーケンスに要素が含まれている場合はtrue、それ以外の場合はfalseを返す。
空のシーケンスに要素は存在しないのでfalseを返す。

仕様

Allの仕様

APIリファレンスの英語版に記述があります。日本語版は記述が漏れています。

Ja: https://docs.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.all?view=netcore-3.1
En: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.all?view=netcore-3.1

true if every element of the source sequence passes the test in the specified predicate, or if the sequence is empty; otherwise, false.

Anyの仕様

Ja: https://docs.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.any?view=netcore-3.1
En: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any?view=netcore-3.1

Any(引数なし)

ソース シーケンスに要素が含まれている場合は true。それ以外の場合は false

Any(引数あり)

ソース シーケンスが空ではないか、指定された述語で少なくともその 1 つの要素がテストに合格する場合は true。それ以外の場合は false

実装

Allの実装

https://github.com/microsoft/referencesource/blob/4.6.2/System.Core/System/Linq/Enumerable.cs#L1182

Enumerable.cs
public static bool All<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) {
        if (!predicate(element)) return false;
    }
    return true;
}

Anyの実装

https://github.com/microsoft/referencesource/blob/4.6.2/System.Core/System/Linq/Enumerable.cs#L1165

Enumerable.cs
public static bool Any<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) return true;
    }
    return false;
}

https://github.com/microsoft/referencesource/blob/4.6.2/System.Core/System/Linq/Enumerable.cs#L1173

Enumerable.cs
public static bool Any<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    foreach (TSource element in source) {
        if (predicate(element)) return true;
    }
    return false;
}

仕様の考察

仕様の考察の目的

仕様を単純に覚えるのであればAll=trueAny=falseのように暗記することになると思いますが、たまにしか使わないと、どちらがどうだったか、あやふやになります。

仕様に必然性があれば思い出す助けになるので、仕様の意味するところを考察してみました。 目的上、あくまでこう解釈できるという話です。

仕様を思い出すという目的からは、空のシーケンスに対するAllAnyの結果は否定の関係になるので、どちらかが分かればもう一方はその否定で導けます。 しっくりくる解釈が一つあればいいと思います。

論理演算との整合性

AllAnyはシーケンスの要素に対する論理演算だと解釈できます。

  • All : 論理積(AND)
  • Any : 論理和(OR)

Allと論理演算の整合性

次のコードで変数r1r2には同じ結果を期待すると思います。実際にそうなります。

C#
static void ConfirmAnd(IEnumerable<bool> a, IEnumerable<bool> b)
{
    bool r1 = a.All(x => x) && b.All(x => x);
    bool r2 = a.Concat(b).All(x => x);
    Console.WriteLine($"{r1} : {r2}");
}

次のようなパラメータでこの関数が呼び出される場合を考えます。

C#
var empty = new bool[0];
var allTrue = new[] { true };
ConfirmAnd(empty, allTrue);

仮に空のシーケンスに対してAllfalseを返す場合、r1falseになりますがr2trueになり整合性がとれません。

空のシーケンスとの結合がシーケンスの内容に影響しないのと同様に、空のシーケンスに対する論理演算も他のシーケンスの論理演算結果に影響を与えないようにすることで整合性がとれます。 つまり、All(論理積)が空のシーケンスに対してtrue(論理積の単位元)を返すことを意味します。

まとめ

  • Allは論理演算との整合性を保つため、空のシーケンスに対して他の論理積の結果に影響を与えないtrueを返す

Anyと論理演算の整合性

次のコードで変数r1r2には同じ結果を期待すると思います。実際にそうなります。

C#
static void ConfirmAny(IEnumerable<bool> a, IEnumerable<bool> b)
{
    bool r1 = a.Any(x => x) || b.Any(x => x);
    bool r2 = a.Concat(b).Any(x => x);
    Console.WriteLine($"{r1} : {r2}");
}

次のようなパラメータでこの関数が呼び出される場合を考えます。

C#
var empty = new bool[0];
var allFalse = new[] { false };
ConfirmAny(empty, allFalse);

仮に空のシーケンスに対してAnytrueを返す場合、r1trueになりますがr2falseになり整合性がとれません。

空のシーケンスとの結合がシーケンスの内容に影響しないのと同様に、空のシーケンスに対する論理演算も他のシーケンスの論理演算結果に影響を与えないようにすることで整合性がとれます。 つまり、Any(論理和)が空のシーケンスに対してfalse(論理和の単位元)を返すことを意味します。

まとめ

  • Anyは論理演算との整合性を保つため、空のシーケンスに対して他の論理和の結果に影響を与えないfalseを返す

Anyの仕様の必然性

Anyには、シーケンスに要素が含まれているかどうかを判断する、引数なしのオーバーロードが存在します(これがAllではなくAnyに存在することは単語の意味から連想できます)。 定義から明らかなように、この関数は空のシーケンスに対してfalseを返します。

この「空のシーケンスに対してfalseを返す」振る舞いはAny関数のオーバーロードで一貫しています。

まとめ

  • Anyはシーケンスに要素が含まれているかどうかを判断する役割(空のシーケンスに対してfalseを返す)を持っている
  • Anyに条件(predicate引数)を付加しても空のシーケンスに対する役割(空のシーケンスに対してfalseを返す)は変わらない

Allの仕様の妥当性

APIリファレンスのAllの説明:

シーケンスのすべての要素が条件を満たしているかどうかを判断します。

この説明からは、空のシーケンスに対する振る舞いについて2つの解釈ができます。

  1. 空のシーケンスは条件を満たす要素を持たないのでfalseを返す
  2. 空のシーケンスは条件を満たさない要素を持たないのでtrueを返す

言い回しでは1が素直な解釈に思えますが、論理上は同等です。 結論を出すためには、すべての要素が条件を満たすとはどういうことか定義する必要があります。

単純な定義は次のようになると思います。

  • すべての要素が条件を満たすとは、N個の要素のうち条件を満たす要素がN個あることをいう

空のシーケンス(N=0)の場合、0個の要素のうち条件を満たす要素が0個あれば、すべての要素が条件を満たしたといえることになり、常に成立します。

まとめ

  • Allは、要素の全量と条件を満たす要素の数が一致する、空のシーケンスに対してtrueを返す

実装上の理由

これらの関数がシーケンスの途中であっても結果が確定した時点で処理を切り上げて結果を返すことは、通常期待される動作だと思います。

その場合、前記の実装が自然な実装であり、これだけ具象的な処理であれば想像できる実装の振る舞いを仕様とするのも妥当だと思います。

まとめ

  • Allの自然な実装は、シーケンスを走査して条件を満たさない要素が含まれている場合にfalseを返して処理を抜け、それ以外の時にtrueを返す
  • Allの実装では、シーケンスが空であればループに入らないのでtrueを返す
  • あえて自然な実装に反した仕様にする理由がない

※Anyについても同様のことが言えるが割愛します。

検証

C#
var empty = new bool[0];
Console.WriteLine("All: " + empty.All(x => x));
Console.WriteLine("Any: " + empty.Any());
Console.WriteLine("Any: " + empty.Any(x => x));
出力
All: True
Any: False
Any: False