この投稿はC#アドベントカレンダー2023の1日目の記事です。
C# アドベントカレンダー 2023 2日目の明日は @advanceboy さんの「VSCode で C# のブロック {} 前後の改行の設定を変更する 2023」です。
2023年の5月末頃、
配列において、全ての要素が条件を満たすならtrueを返すメソッドを定義します。
このメソッドは、空の配列を渡したらfalseを返しますか?trueを返しますか?
という感じの話題が、SNSで盛り上がっていましたね。
C#のLINQに、「IEnumerable<T>
の全ての要素が条件を満たすならtrueを返す」というAllメソッドが存在します。Allメソッドは、IEnumerable<T>
が空の場合、trueを返すでしょうか?それともfalseを返すでしょうか?
空のIEnumerable<T>
でAllメソッドを呼び出すと、trueを返します。もしかしたら「あれ、この場合ってどうなるんだっけ?」と疑問に思った方もいるのではないでしょうか?
All以外にも、LINQにはさまざまなメソッドがあり「LINQのこのメソッド、この場合ってどうなるんだっけ?」という疑問を持つ方もいるかもしれません。この投稿では、そのような「LINQのこのメソッド、この場合どうなるっけ?」を筆者の独断でピックアップして紹介します。
なお、本投稿は.NET 8時点の仕様・挙動・ドキュメントに基づいています。
空のIEnumerable<T>でAll
空のIEnumerable<T>
でAllメソッドを呼び出すと、trueを返します。
[Test]
public void TestAll()
{
IEnumerable<int> numbers = [];
bool isAllEven = numbers.All(num => num % 2 == 0);
Assert.True(isAllEven);
}
公式ドキュメントには、次のような記載があります。
Returns
Boolean
true if every element of the source sequence passes the test in the specified predicate, or if the sequence is empty; otherwise, false.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.all?view=net-8.0
空のIEnumerable<T>でAny
空のIEnumerable<T>
でAnyメソッドを呼び出すと、falseを返します。
[Test]
public void TestAny()
{
IEnumerable<int> numbers = [];
bool isAnyEven = numbers.Any(num => num % 2 == 0);
Assert.False(isAnyEven);
}
公式ドキュメントには、次のような記載があります。
Returns
Boolean
true if the source sequence contains any elements; otherwise, false.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any?view=net-8.0
余りがあるChunk
要素数 6 のIEnumerable<int>
に対して、引数 sizeを4でChunkメソッドを呼び出します。
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
// chunkedNumbersは、[[3, 1, 4, 1], [5, 9]] みたいな感じ?
// もしくは
// chunkedNumbersは、[[3, 1, 4, 1]] みたいな感じ?
IEnumerable<int[]> chunkedNumbers = numbers.Chunk(4);
要素数6に対して、引数 sizeが4だとぴったり割り切れず、余りが出ます。
[[3, 1, 4, 1], [5, 9]]
みたいになるでしょうか?
それとも[[3, 1, 4, 1]]
みたいになるでしょうか?
[Test]
public void TestChunkSize0()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int[]> chunkedNumbers = numbers.Chunk(4);
IEnumerable<int[]> expected = [[3, 1, 4, 1, ], [5, 9]];
Assert.That(chunkedNumbers, Is.EqualTo(expected));
}
[[3, 1, 4, 1], [5, 9]]
みたいになります。最後の要素は「size」未満になる場合があることに注意してください。
公式ドキュメントには、次のように記載があります。
Each chunk except the last one will be of size size. The last chunk will contain the remaining elements and may be of a smaller size.
なお、IEnumerable<int>
の要素数よりも大きいsizeでChunkメソッドを呼び出した場合、次のようになります。
[Test]
public void TestChunkSize1()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int[]> chunkedNumbers = numbers.Chunk(10);
IEnumerable<int[]> expected = [[3, 1, 4, 1, 5, 9]];
Assert.That(chunkedNumbers, Is.EqualTo(expected));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-8.0
1未満のsizeでChunk
引数sizeに1未満を渡してChunkメソッドを呼び出した場合、即座にArgumentOutOfRangeExceptionがスローされます。
[Test]
public void TestChunkArgumentOutOfRangeE()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
Assert.Catch<ArgumentOutOfRangeException>(() => { numbers.Chunk(0); });
}
公式ドキュメントには、次のように記載があります。
ArgumentOutOfRangeException
size is below 1.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-8.0
要素数が違うZip
要素数が異なるIEnumerable<TFirst>
とIEnumerable<TSecond>
でZipメソッドを呼び出した場合、短い方のシーケンスと同じ要素数のIEnumerable<(TFirst, TSecond)>
が生成されます。
[Test]
public void TestZip()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<string> names = ["Taro", "Jiro", "Saburo"];
IEnumerable<(int, string)> zipped = numbers.Zip(names);
IEnumerable<(int, string)> expected = [(3, "Taro"), (1, "Jiro"), (4, "Saburo")];
Assert.That(zipped, Is.EqualTo(expected));
}
公式ドキュメントには、次のような記載があります。
If the sequences do not have the same number of elements, the method merges sequences until it reaches the end of one of them.
これは、引数にresultSelectorを取るオーバーロードでも同様です。
record Person(string Name, string Language);
[Test]
public void TestZipResultSelector()
{
IEnumerable<string> names = ["Taro", "Jiro", "Saburo"];
IEnumerable<string> languages = ["C#"];
var zipped = names.Zip(languages, (name, language) => new Person(name, language));
var expected = new List<Person> {new("Taro", "C#")};
Assert.That(zipped, Is.EqualTo(expected));
}
2個以上のIEnumerable<T>
をZipするオーバーロードの場合、最も要素数の小さいシーケンスと同じ要素数を持つ、シーケンスを生成します。
[Test]
public void TestZipTriple()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<string> names = ["Taro", "Jiro", "Saburo"];
IEnumerable<string> languages = ["C#"]; // 一番小さい要素数 1
IEnumerable<(int, string, string)> zipped = numbers.Zip(names, languages);
IEnumerable<(int, string, string)> expected = [(3, "Taro", "C#")]; // 要素数 1
Assert.That(zipped, Is.EqualTo(expected));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.zip?view=net-8.0
同点が複数いるMaxBy
MaxByメソッドは引数で渡したデリゲートで「最大の評価値」となる要素を返すメソッドです。では、最大の評価値となる要素が複数あって、同点となった場合はどうなるでしょう?考えられるのは、
- 先勝ちで、最初に「最大の評価値」となった要素を返す
- 後勝ちで、最後に「最大の評価値」となった要素を返す
- 例外を投げる
- 実装依存
ドキュメントには同点だった場合の記載がありません。テストには「キーが全てnullだった場合、最初の要素となる」というテストが存在しますが、厳密に「同点だった場合」のテストはありません。
私は「実装依存」だと思います。(.NET 8の時点で)
ちなみに、.NET 8の現状の実装だと、MaxByメソッドで同点が複数いた場合、最初に「最大の評価値」となった要素を返します。
record Player(string Name, int Level);
[Test]
public void TestMaxByTies()
{
IEnumerable<Player> players = [
new Player(Name: "Taro", Level: 36),
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Hanako", Level: 36),
];
Player? maxLevelPlayer = players.MaxBy(p => p.Level);
Assert.That(maxLevelPlayer, Is.EqualTo(new Player(Name: "Taro", Level: 36)));
}
そういう実装になっているとはいえ、ドキュメントに記載もないので現時点で、実装に依存した処理は避けるべきでしょう。
またMinByも同じように、.NET 8の実装だと、同点が複数いた場合、最初に「最小の評価値」となった要素を返します。
[Test]
public void TestMinByTies()
{
IEnumerable<Player> players = [
new Player(Name: "Taro", Level: 36),
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Hanako", Level: 36),
];
Player? minLevelPlayer = players.MinBy(p => p.Level);
Assert.That(minLevelPlayer, Is.EqualTo(new Player(Name: "Jiro", Level: 30)));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0
空のIEnumerable<T>でMaxBy
空のIEnumerable<T>
でMaxByメソッドを呼び出した場合、Tのdefaultがnullなのか、そうでないのかで挙動が異なります。
次のようにTのdefaultがnullな場合、空のIEnumerable<T>
でMaxByメソッドを呼び出すと、nullを返します。
record Record(int value);
[Test]
public void TestMaxByEmptyReference()
{
IEnumerable<Record> records = [];
Record? record = records.MaxBy(p => p.value);
Assert.Null(record);
}
次のようにTのdefaultがnullではない場合、空のIEnumerable<T>
でMaxByメソッドを呼び出すと、例外InvalidOperationExceptionがスローされます。
record struct StructRecord(int value);
[Test]
public void TestMaxByEmptyStruct()
{
IEnumerable<StructRecord> records = [];
Assert.Catch<InvalidOperationException>(() =>
{
StructRecord minValueRecord = records.MaxBy(p => p.value);
});
}
公式ドキュメントには、次のような記載があります。
If the source sequence is empty and TSource is a nullable type, this method returns null. If the source sequence is empty and TSource is a non-nullable struct, such as a primitive type, an InvalidOperationException is thrown.
MinByも同様です。
[Test]
public void TestMinByEmptyReference()
{
IEnumerable<Record> records = [];
Record? record = records.MinBy(p => p.value);
Assert.Null(record);
}
[Test]
public void TestMinByEmptyStruct()
{
IEnumerable<StructRecord> records = [];
Assert.Catch<InvalidOperationException>(() =>
{
StructRecord minValueRecord = records.MinBy(p => p.value);
});
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0
int.MaxValue(2147483647)を超えてCount
IEnumerable<T>
でCountメソッドを呼び出し、結果がint.MaxValue(2147483647)を超えると、OverflowExceptionがスローされます。
[Test]
public void TestCountException()
{
// int.MaxValueは、2147483647
// intの表現範囲を1超える
static IEnumerable<int> Repeat2147483648()
{
for (long i = 0; i < int.MaxValue + 1L; i++)
{
yield return 0;
}
}
IEnumerable<int> a = Repeat2147483648();
Assert.Throws<OverflowException>(() =>
{
int count = a.Count(it => it == 0);
});
}
コンパイルオプションでCheckForOverflowUnderflowをfalseにしても、OverflowExceptionがスローされることに注意してください。
もしCountメソッドの結果が、int.MaxValueを超える可能性がある場合は、LongCountを使いましょう。
なお、LongCountメソッドの結果が、Int64.MaxValue(9223372036854775807)を超えると、同様にOverflowExceptionがスローされることに注意してください。
公式ドキュメントには、次のような記載があります。
OverflowException
The number of elements in source is larger than Int32.MaxValue.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.count?view=net-8.0
同点がいた場合のOrderByは?
LINQには、次のようなソートを行うメソッドが存在します。
このようなOrderByなどのメソッドにおいて、同点(比較を行う際、要素のキーが同じ)な場合、どのような並び順になるでしょうか?
- 元の順序で先にあった要素が、先に整列されるでしょうか?(安定ソートでしょうか)
- 元の順序で後にあった要素が、先に整列されるでしょうか?
- 特に決まっておらず場合によるでしょうか?
OrderBy、OrderByDescending、ThenBy、ThenByDescendingは「安定ソート」です。
次のコードのように、同点(2つの要素のキーが等しい)な場合、要素の順序は保持され、元の順序で先にあった要素が先に整列されます。
[Test]
public void TestOrderByStableSort()
{
IEnumerable<Player> players = [
new Player(Name: "Taro", Level: 36),
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Hanako", Level: 36),
];
var orderedByLevel = players.OrderBy(it => it.Level);
IEnumerable<Player> expected = [
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Taro", Level: 36),
new Player(Name: "Hanako", Level: 36),
];
Assert.That(orderedByLevel, Is.EqualTo(expected));
}
公式ドキュメントには、次のような記載があります。
This method performs a stable sort; that is, if the keys of two elements are equal, the order of the elements is preserved.
キーが重複してしまったToDictionaryは?
ToDictionaryメソッドを呼び出し、キーが重複してしまうと、ArgumentExceptionがスローされます。
[Test]
public void TestToDictionary()
{
IEnumerable<Player> players = [
new Player(Name: "Taro", Level: 36),
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Hanako", Level: 36),
];
Assert.Throws<ArgumentException>(() =>
{
var levelDictionary = players.ToDictionary(it => it.Level);
});
}
公式ドキュメントの記載は、次のとおりです。
ArgumentException
source contains one or more duplicate keys.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.todictionary?view=net-8.0
もしキーが重複することがありえる場合やDictionary<TKey, List<TElement>>
のような結果が欲しいのであれば、ToDictonaryメソッドではなく、ToLookupメソッドを使うことを検討しましょう。
[Test]
public void TestToLookup()
{
IEnumerable<Player> players = [
new Player(Name: "Taro", Level: 36),
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30),
new Player(Name: "Hanako", Level: 36),
];
var levelLookup = players.ToLookup(it => it.Level);
Assert.That(levelLookup.Count, Is.EqualTo(2));
Assert.That(levelLookup[30], Is.EqualTo(new List<Player>
{
new Player(Name: "Jiro", Level: 30),
new Player(Name: "Saburo", Level: 30)
}
));
Assert.That(levelLookup[36], Is.EqualTo(new List<Player>
{
new Player(Name: "Taro", Level: 36),
new Player(Name: "Hanako", Level: 36)
}
));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tolookup?view=net-8.0
IEnumerable<T>の要素数より大きい数でSkip
IEnumerable<T>の要素数より大きい数でSkipメソッドを呼び出した場合、空のIEnumerable<T>
を返します。
例外はスローされないことに注意してください。
[Test]
public void TestSkipOverLength()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int> actual = numbers.Skip(100);
IEnumerable<int> expected = Enumerable.Empty<int>();
Assert.That(actual, Is.EqualTo(expected));
}
公式ドキュメントには、次のような記載があります。
If source contains fewer than count elements, an empty IEnumerable is returned.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skip?view=net-8.0
0以下の数でSkip
0以下の数でSkipメソッドを呼び出した場合、元の要素を全て持ったIEnumerable<T>
を返します。
例外はスローされないことに注意してください。
[Test]
public void TestSkipUnder0()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int> actual = numbers.Skip(-1);
IEnumerable<int> expected = [3, 1, 4, 1, 5, 9];
Assert.That(actual, Is.EqualTo(expected));
}
公式ドキュメントの記載は、次のとおりです。
If count is less than or equal to zero, all elements of source are yielded.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skip?view=net-8.0
IEnumerable<T>の要素数より大きい数でTake
IEnumerable<T>の要素数より大きい数でTakeメソッドを呼び出した場合、元の要素を全て持ったIEnumerable<T>
を返します。
例外はスローされないことに注意してください。
[Test]
public void TestTakeOverLength()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int> actual = numbers.Take(100);
IEnumerable<int> expected = [3, 1, 4, 1, 5, 9];
Assert.That(actual, Is.EqualTo(expected));
}
公式ドキュメントには、次のような記載があります。
If count exceeds the number of elements in source, all elements of source are returned.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0
0以下の数でTake
0以下の数でTakeメソッドを呼び出した場合、空のIEnumerable<T>
を返します。
例外はスローされないことに注意してください。
[Test]
public void TestTakeUnder0()
{
IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
IEnumerable<int> actual = numbers.Take(-1);
IEnumerable<int> expected = Enumerable.Empty<int>();
Assert.That(actual, Is.EqualTo(expected));
}
公式ドキュメントの記載は、次のとおりです。
If count is less than or equal to zero, source is not enumerated and an empty IEnumerable is returned.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0
Rangeが要素数を超えたTake
TakeにはRangeを引数にとるオーバーロードも存在します。
次のコードのようにIEnumerable<T>
の要素を超えるRangeを引数に渡しても、例外はスローされません。
[Test]
public void TestTakeRangeO()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = ..100;
IEnumerable<int> expected = [3, 1, 4, 1, 5, 9];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
[Test]
public void TestTakeRange1()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = ^100..;
IEnumerable<int> expected = [3, 1, 4, 1, 5, 9];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
[Test]
public void TestTakeRange2()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = 100..;
IEnumerable<int> expected = [];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
[Test]
public void TestTakeRange3()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = ..^100;
IEnumerable<int> expected = [];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
[Test]
public void TestTakeRange4()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = ^100..100;
IEnumerable<int> expected = [3, 1, 4, 1, 5, 9];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
[Test]
public void TestTakeRange5()
{
int[] numbers = [3, 1, 4, 1, 5, 9];
Range range = 100..^100;
IEnumerable<int> expected = [];
Assert.That(numbers.Take(range), Is.EqualTo(expected));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0
空またはnullしか持たないIEnumerable<T>のSum
空のIEnumerable<T>
でSumメソッドを呼び出した場合、0になります。
[Test]
public void TestSumEmptyNullable()
{
IEnumerable<int?> nullableInts = [];
int? sum = nullableInts.Sum();
Assert.That(sum, Is.EqualTo(0));
}
[Test]
public void TestSumEmptyNonNull()
{
IEnumerable<int> ints = [];
int sum = ints.Sum(it => 0);
Assert.That(sum, Is.EqualTo(0));
}
nullしか要素に持たないIEnumerable<T>
でSumメソッドを呼び出した場合、0になります。nullでないことに注意して下しさい。
[Test]
public void TestSumNullNullable()
{
IEnumerable<int?> nullableInts = [null];
int? sum = nullableInts.Sum();
Assert.That(sum, Is.EqualTo(0));
}
公式ドキュメントには、次のような記載があります。
This method returns zero if source contains no elements or all elements are null.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.sum?view=net-8.0
計算結果がint.MaxValueやdouble.MaxValueを超えた場合のSum
Sumメソッドを呼び出して、計算結果がint.MaxValueやdouble.MaxValueなどそれぞれの型の最大値を超えてしまった場合、OverflowExceptionがスローされます。また、int.MinValueやdouble.MinVaueなどの最小値を下回ってしまった場合も同様です。
[Test]
public void TestSumOverflow()
{
IEnumerable<int> ints = [int.MaxValue, 1];
Assert.Throws<OverflowException>(() =>
{
int sum = ints.Sum();
});
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.sum?view=net-8.0
空またはnullしか持たないIEnumerable<T>のAverage、Max、Min
空のIEnumerable<int>
やIEnumerabel<int?>
でAverageやMax、Minを呼び出したらどうなるでしょうか?
これはオーバーロードによって結果が異なります。つまりIEnumerable<T>
のTの型(数値型)によって結果が異なります。
※ Average、Max、Minは、IEnumerable<int>
、IEnumerable<int?>
、IEnumerable<double>
、IEnumerable<double?>
などなど、数値型ごとに異なるオーバーロードで実装されています。
まずIEnumerable<int>
やIEnumerable<double>
など、Null許容値型ではない普通の数値型のオーバーロードでは、空でAverageメソッドなどを呼び出すと、InvalidOperationExceptionがスローされます。
[Test]
public void TestAverageThrowException()
{
IEnumerable<int> ints = [];
Assert.Throws<InvalidOperationException>(() =>
{
double average = ints.Average();
});
}
公式ドキュメントには、次のような記載があります。
InvalidOperationException
source contains no elements.
一方でIEnumerable<int?>
やIEnumerable<double?>
など、Null許容値型のオーバーロードでは、空でAverageメソッドなどを呼び出すと、nullを返します。
[Test]
public void TestAverageNull()
{
IEnumerable<int?> ints = [];
double? average = ints.Average();
Assert.Null(average);
}
またIEnumerable<int?>
やIEnumerable<double?>
など、Nullableな型のオーバーロードで、nullしか要素に持たない場合でAverageメソッドなどを呼び出しても、nullを返します。
[Test]
public void TestAverageOnlyNull()
{
IEnumerable<int?> ints = [null];
double? average = ints.Average();
Assert.Null(average);
}
公式ドキュメントの記載は次の通りです。
The average of the sequence of values, or null if the source sequence is empty or contains only values that are null.
MinやMaxも同様です。
平均計算途中で最大値を超えたAverage
long.MaxValueは「9223372036854775807」です。
long.MinValueは「-9223372036854775808」です。
では、「long.MaxValueとlong.MinValueと1」の3個の平均はいくつでしょうか?そう0.0ですね。
IEnumerableのAverageメソッドでも「次のコード」では、次の示すように、平均は0.0になります。
[Test]
public void TestAverageNotOverflow()
{
IEnumerable<long> longs = [long.MaxValue, long.MinValue, 1];
double average = longs.Average();
Assert.That(average, Is.EqualTo(0.0));
}
少し順番を変えてみましょう。似たようなコードですが、「次のコード」では、OverflowExceptionが発生します。
[Test]
public void TestAverageOverflow()
{
IEnumerable<long> longs = [1, long.MaxValue, long.MinValue];
Assert.Throws<OverflowException>(() =>
{
double average = longs.Average();
});
}
実は、平均計算途中でlongやdecimalの表現範囲を超えた場合、OverflowExceptionが発生します。
公式ドキュメントの記載は次のとおりです。
OverflowException
The sum of the elements in the sequence is larger than Int64.MaxValue.
ドキュメントの記載はちょっと不正確で、次のようにInt64.MinValue(long.MinValue)を下回っても例外が発生します。
[Test]
public void TestAverageOverflow2()
{
IEnumerable<long> longs = [long.MinValue, long.MinValue];
Assert.Throws<OverflowException>(() =>
{
double average = longs.Average();
});
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-8.0
要素がint.MaxValueを超えるRange
要素がint.MaxValueを超えるような引数startとcountを渡してRangeメソッドを呼び出すと、ArgumentOutOfRangeExceptionがスローされます。
※ 要素がint.MaxValueを超えるのは、start+count-1が、int.MaxValueを超えるような時
[Test]
public void TestRangeOverElementIntMaxValue()
{
Assert.Throws<ArgumentOutOfRangeException>(() => {
Enumerable.Range(start: int.MaxValue, count: 2);
});
Assert.Throws<ArgumentOutOfRangeException>(() => {
Enumerable.Range(start: 2, count: int.MaxValue);
});
}
公式ドキュメントには、次のような記載があります。
ArgumentOutOfRangeException
略
start + count -1 is larger than Int32.MaxValue.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.range?view=net-8.0
0未満でRange
引数countに0未満の数を渡してRangeメソッドを呼び出すと、ArgumentOutOfRangeExceptionがスローされます。
[Test]
public void TestRangeCountUnder0()
{
Assert.Throws<ArgumentOutOfRangeException>(() => {
Enumerable.Range(start: 10, count: -1);
});
}
公式ドキュメントには、次のような記載があります。
ArgumentOutOfRangeException
count is less than 0.
なお、引数countに0を渡してRangeメソッドを呼び出すと、空のIEnumerable<int>
になります。
[Test]
public void TestRangeCount0()
{
IEnumerable<int> range = Enumerable.Range(start: 10, count: 0);
Assert.That(range, Is.EqualTo(Enumerable.Empty<int>()));
}
0未満でRepeat
引数countに0未満を渡してRepeatメソッドを呼び出すと、ArgumentOutOfRangeExceptionがスローされます。
[Test]
public void TestRepeatUnder0()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
IEnumerable<string> messages = Enumerable.Repeat("Hello World", -1);
});
}
公式ドキュメントにも記載があります。
ArgumentOutOfRangeException
count is less than 0.
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.repeat?view=net-8.0
なお、引数countに0を渡してRepeatメソッドを呼び出すと、空のIEnumerable<T>
を返します。
[Test]
public void TestRepeat0()
{
IEnumerable<string> messages = Enumerable.Repeat("Hello World", 0);
Assert.That(messages, Is.EqualTo(Enumerable.Empty<string>()));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.repeat?view=net-8.0
順番どうなるIntersect
LINQには集合演算を行うメソッドも存在します。Intersectは積集合を生成するメソッドです。
公式ドキュメントには、次のような記載があります。
When the object returned by this method is enumerated, Intersect yields distinct elements occurring in both sequences in the order in which they appear in first.
つまり生成されたIEnumerable<T>
の順番は、拡張メソッドの第一引数(次のコードではfirst)において要素が出現する順番になります。
[Test]
public void TestIntersect()
{
IEnumerable<string> first = ["Taro", "Jiro", "Saburo", "Hanako"];
IEnumerable<string> second = ["Hanako", "Matsuko", "Takeko", "Umeko", "Taro"];
IEnumerable<string> expected = ["Taro", "Hanako"];
Assert.That(first.Intersect(second), Is.EqualTo(expected));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.intersect?view=net-8.0
空のIEnumerable<T>でAggregate
空のIEnumerable<T>
で、引数として初期値seedを取らないオーバーロードのAggregateメソッドを呼び出すと、InvalidOperationExceptionをスローします。
[Test]
public void TestAggregateProduct()
{
IEnumerable<int> numbers = [];
Assert.Throws<InvalidOperationException>(() =>
{
int product = numbers.Aggregate((lhs, rhs) => lhs * rhs);
});
}
公式ドキュメントには、次のように記載があります。
InvalidOperationException
source contains no elements.
一方、空のIEnumerable<T>
で、引数として初期値seedをとる2つのオーバーロードのAggregateメソッドを呼び出しても、InvalidOperationExceptionはスローされません。
[Test]
public void TestAggregateProductWithSeed()
{
IEnumerable<int> numbers = [];
int product = numbers.Aggregate(1, (lhs, rhs) => lhs * rhs);
Assert.That(product, Is.EqualTo(1));
}
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.aggregate?view=net-8.0
innerに対応する要素がなかった場合のouterはどうなる、GroupJoin
GroupJoinの公式ドキュメントに次のような記載があります。
Note
If there are no correlated elements in inner for a given element of outer, the sequence of matches for that element will be empty but will still appear in the results.
このNoteの内容を、例を用いて補足します。次のようなレコードがあります。
public record User(string Id, string Name);
public record Order(string OrderId, string UserId, string Name);
public record UserDisplayInfo(string UserName, int OrderCount);
IEnumerable<Users>
のusersとIEnumerable<Order>
のordersでGroupJoinメソッドを用いて、User型のIdとOrder型のUserIdで突合させて、一致するusers内のUser要素とorders内の複数のOrder要素から、UserDisplayInfoを生成します。
[Test]
public void TestGroupJoin()
{
IEnumerable<User> users = [
new User(Name: "Taro", Id: "abc"),
new User(Name: "Jiro", Id: "def"),
new User(Name: "Saburo", Id: "ghi"),
new User(Name: "Hanako", Id: "jkl"),
];
IEnumerable<Order> orders = [
new Order(Name: "Order0", UserId: "abc", OrderId: "123"),
new Order(Name: "Order1", UserId: "def", OrderId: "612"),
new Order(Name: "Order2", UserId: "def", OrderId: "422"),
new Order(Name: "Order3", UserId: "jkl", OrderId: "532"),
new Order(Name: "Order4", UserId: "xyz", OrderId: "698"),
];
var userDisplayInfos = users
.GroupJoin(inner: orders,
outerKeySelector: u => u.Id,
innerKeySelector: o => o.UserId,
resultSelector: (u, orders) =>
new UserDisplayInfo(UserName: u.Name, OrderCount: orders.Count())
);
IEnumerable<UserDisplayInfo> expected = [
new UserDisplayInfo(UserName: "Taro", OrderCount: 1),
new UserDisplayInfo(UserName: "Jiro", OrderCount: 2),
new UserDisplayInfo(UserName: "Saburo", OrderCount: 0),
new UserDisplayInfo(UserName: "Hanako", OrderCount: 1),
];
Assert.That(userDisplayInfos, Is.EqualTo(expected));
}
ここで、
- Nameが"Saburo"のusers要素(Idは"ghi")に対応する、Order要素はorders中に存在しないこと
- UserNameが"Saburo"のUserDisplayInfo要素は、GroupJoinの結果中に存在すること
に注目してください。
Note
If there are no correlated elements in inner for a given element of outer, the sequence of matches for that element will be empty but will still appear in the results.
とあるように、
Nameが"Saburo"のusers(outer)の要素のように、users(outer)の要素に対応するoreders(inner)の要素が存在しなくても、最終結果の要素を(resultSelectorにより)生成する際は、innerのリストは空で提供され、結果の要素(UserNameが"Saburo"のUserDisplayInfo)が生成されます。
see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.groupjoin?view=net-8.0
まとめ
この投稿で紹介したLINQの挙動、みなさん正確に把握していましたか?
C#のLINQに限らず、さまざまなプログラミング言語の標準コレクションAPIは、使う機会が非常に多いです。だからこそ、その仕様・挙動を正確に理解していないと、思わぬ不具合を発生させてしまいます。正しく仕様・挙動を把握して、やっていきましょう!