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
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
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