LINQのAllとAnyが空のシーケンスに対して返す値
LINQのAllとAnyが空のシーケンスに対して返す値についてのまとめです。仕様、実装、仕様の考察、C#での検証結果を記載します。忘れたときに見返すためのメモです。
結論
LINQのAllとAnyが空のシーケンスに対して返す値は、Allがtrue、Anyがfalseです。
| メソッド | 戻り値 | 動作の解釈 |
|---|---|---|
| All | true | シーケンスに条件を満たさない要素が含まれている場合はfalse、それ以外の場合はtrueを返す。空のシーケンスに条件を満たさない要素は存在しないので trueを返す。 |
| Any | false | シーケンスに条件を満たす要素が含まれている場合は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
trueif 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
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
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
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=trueやAny=falseのように暗記することになると思いますが、たまにしか使わないと、どちらがどうだったか、あやふやになります。
仕様に必然性があれば思い出す助けになるので、仕様の意味するところを考察してみました。 目的上、あくまでこう解釈できるという話です。
仕様を思い出すという目的からは、空のシーケンスに対するAllとAnyの結果は否定の関係になるので、どちらかが分かればもう一方はその否定で導けます。
しっくりくる解釈が一つあればいいと思います。
論理演算との整合性
AllとAnyはシーケンスの要素に対する論理演算だと解釈できます。
All: 論理積(AND)Any: 論理和(OR)
Allと論理演算の整合性
次のコードで変数r1とr2には同じ結果を期待すると思います。実際にそうなります。
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}");
}次のようなパラメータでこの関数が呼び出される場合を考えます。
var empty = new bool[0];
var allTrue = new[] { true };
ConfirmAnd(empty, allTrue);仮に空のシーケンスに対してAllがfalseを返す場合、r1はfalseになりますがr2はtrueになり整合性がとれません。
空のシーケンスとの結合がシーケンスの内容に影響しないのと同様に、空のシーケンスに対する論理演算も他のシーケンスの論理演算結果に影響を与えないようにすることで整合性がとれます。
つまり、All(論理積)が空のシーケンスに対してtrue(論理積の単位元)を返すことを意味します。
まとめ
Allは論理演算との整合性を保つため、空のシーケンスに対して他の論理積の結果に影響を与えないtrueを返す
Anyと論理演算の整合性
次のコードで変数r1とr2には同じ結果を期待すると思います。実際にそうなります。
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}");
}次のようなパラメータでこの関数が呼び出される場合を考えます。
var empty = new bool[0];
var allFalse = new[] { false };
ConfirmAny(empty, allFalse);仮に空のシーケンスに対してAnyがtrueを返す場合、r1はtrueになりますがr2はfalseになり整合性がとれません。
空のシーケンスとの結合がシーケンスの内容に影響しないのと同様に、空のシーケンスに対する論理演算も他のシーケンスの論理演算結果に影響を与えないようにすることで整合性がとれます。
つまり、Any(論理和)が空のシーケンスに対してfalse(論理和の単位元)を返すことを意味します。
まとめ
Anyは論理演算との整合性を保つため、空のシーケンスに対して他の論理和の結果に影響を与えないfalseを返す
Anyの仕様の必然性
Anyには、シーケンスに要素が含まれているかどうかを判断する、引数なしのオーバーロードが存在します(これがAllではなくAnyに存在することは単語の意味から連想できます)。
定義から明らかなように、この関数は空のシーケンスに対してfalseを返します。
この「空のシーケンスに対してfalseを返す」振る舞いはAny関数のオーバーロードで一貫しています。
まとめ
Anyはシーケンスに要素が含まれているかどうかを判断する役割(空のシーケンスに対してfalseを返す)を持っているAnyに条件(predicate引数)を付加しても空のシーケンスに対する役割(空のシーケンスに対してfalseを返す)は変わらない
Allの仕様の妥当性
APIリファレンスのAllの説明:
シーケンスのすべての要素が条件を満たしているかどうかを判断します。
この説明からは、空のシーケンスに対する振る舞いについて2つの解釈ができます。
- 空のシーケンスは条件を満たす要素を持たないので
falseを返す - 空のシーケンスは条件を満たさない要素を持たないので
trueを返す
言い回しでは1が素直な解釈に思えますが、論理上は同等です。 結論を出すためには、すべての要素が条件を満たすとはどういうことか定義する必要があります。
単純な定義は次のようになると思います。
- すべての要素が条件を満たすとは、N個の要素のうち条件を満たす要素がN個あることをいう
空のシーケンス(N=0)の場合、0個の要素のうち条件を満たす要素が0個あれば、すべての要素が条件を満たしたといえることになり、常に成立します。
まとめ
Allは、要素の全量と条件を満たす要素の数が一致する、空のシーケンスに対してtrueを返す
実装上の理由
これらの関数がシーケンスの途中であっても結果が確定した時点で処理を切り上げて結果を返すことは、通常期待される動作だと思います。
その場合、前記の実装が自然な実装であり、これだけ具象的な処理であれば想像できる実装の振る舞いを仕様とするのも妥当だと思います。
まとめ
Allの自然な実装は、シーケンスを走査して条件を満たさない要素が含まれている場合にfalseを返して処理を抜け、それ以外の時にtrueを返すAllの実装では、シーケンスが空であればループに入らないのでtrueを返す- あえて自然な実装に反した仕様にする理由がない
※Anyについても同様のことが言えるが割愛します。
検証
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