【Allって】LINQ、この場合どうなる？【空配列は？】

この投稿は[C#アドベントカレンダー2023](https://qiita.com/advent-calendar/2023/csharplang)の1日目の記事です。

C# アドベントカレンダー 2023 2日目の明日は @advanceboy さんの「[VSCode で C# のブロック {} 前後の改行の設定を変更する 2023](https://aquasoftware.net/blog/?p=1975)」です。

---

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を返します。

```csharp
[Test]
public void TestAll()
{
    IEnumerable<int> numbers = [];
    bool isAllEven = numbers.All(num => num % 2 == 0);
    Assert.True(isAllEven);
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.all?view=net-8.0)には、次のような記載があります。

> 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を返します。

```csharp
[Test]
public void TestAny()
{
    IEnumerable<int> numbers = [];
    bool isAnyEven = numbers.Any(num => num % 2 == 0);
    Assert.False(isAnyEven);
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any?view=net-8.0)には、次のような記載があります。

> 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メソッドを呼び出します。


```csharp
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]]`みたいになるでしょうか？


```csharp
[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」未満になる場合があることに注意してください。

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-8.0#remarks)には、次のように記載があります。

> 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メソッドを呼び出した場合、次のようになります。

```csharp
[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がスローされます。

```csharp
[Test]
public void TestChunkArgumentOutOfRangeE()
{
    IEnumerable<int> numbers = [3, 1, 4, 1, 5, 9];
    Assert.Catch<ArgumentOutOfRangeException>(() => { numbers.Chunk(0); });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk?view=net-8.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)>`が生成されます。

```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.zip?view=net-8.0#system-linq-enumerable-zip-3(system-collections-generic-ienumerable((-0))-system-collections-generic-ienumerable((-1))-system-func((-0-1-2))))には、次のような記載があります。

>  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を取るオーバーロードでも同様です。

```csharp
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するオーバーロードの場合、最も要素数の小さいシーケンスと同じ要素数を持つ、シーケンスを生成します。

```csharp
[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メソッドは引数で渡したデリゲートで「最大の評価値」となる要素を返すメソッドです。では、最大の評価値となる要素が複数あって、同点となった場合はどうなるでしょう？考えられるのは、

* 先勝ちで、最初に「最大の評価値」となった要素を返す
* 後勝ちで、最後に「最大の評価値」となった要素を返す
* 例外を投げる
* 実装依存

[ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0)には同点だった場合の記載がありません。テストには「キーが全てnullだった場合、最初の要素となる」という[テスト](https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Linq/tests/MaxTests.cs#L999-L1021)が存在しますが、厳密に「同点だった場合」のテストはありません。

私は「実装依存」だと思います。（.NET 8の時点で）

ちなみに、.NET 8の現状の実装だと、MaxByメソッドで同点が複数いた場合、最初に「最大の評価値」となった要素を返します。

```csharp
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の実装だと、同点が複数いた場合、最初に「最小の評価値」となった要素を返します。

```csharp
[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を返します。

```csharp
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がスローされます。


```csharp
record struct StructRecord(int value);

[Test]
public void TestMaxByEmptyStruct()
{
    IEnumerable<StructRecord> records = [];

    Assert.Catch<InvalidOperationException>(() =>
    {
        StructRecord minValueRecord = records.MaxBy(p => p.value);
    });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.maxby?view=net-8.0)には、次のような記載があります。

> 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も同様です。

```csharp
[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がスローされます。

```csharp
[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がスローされることに注意してください。

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.count?view=net-8.0)には、次のような記載があります。

> 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](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderby?view=net-8.0)
* [OrderByDescending](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderbydescending?view=net-8.0)
* [ThenBy](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.thenby?view=net-8.0)
* [ThenByDescending](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.thenbydescending?view=net-8.0)

このようなOrderByなどのメソッドにおいて、同点（比較を行う際、要素のキーが同じ）な場合、どのような並び順になるでしょうか？

* 元の順序で先にあった要素が、先に整列されるでしょうか？（安定ソートでしょうか）
* 元の順序で後にあった要素が、先に整列されるでしょうか？
* 特に決まっておらず場合によるでしょうか？

OrderBy、OrderByDescending、ThenBy、ThenByDescendingは「安定ソート」です。

次のコードのように、同点（2つの要素のキーが等しい）な場合、要素の順序は保持され、元の順序で先にあった要素が先に整列されます。

```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderby?view=net-8.0)には、次のような記載があります。

> 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がスローされます。

```csharp
[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);
    });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.todictionary?view=net-8.0)の記載は、次のとおりです。

> 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](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tolookuph?view=net-8.0)メソッドを使うことを検討しましょう。

```csharp
[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>`を返します。

例外はスローされないことに注意してください。

```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skip?view=net-8.0)には、次のような記載があります。

> If source contains fewer than count elements, an empty IEnumerable<T> 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>`を返します。

例外はスローされないことに注意してください。

```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skip?view=net-8.0)の記載は、次のとおりです。

> 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>`を返します。

例外はスローされないことに注意してください。

```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0)には、次のような記載があります。

> 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>`を返します。

例外はスローされないことに注意してください。


```csharp
[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));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0)の記載は、次のとおりです。

> If count is less than or equal to zero, source is not enumerated and an empty IEnumerable<T> is returned.

see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.take?view=net-8.0

# Rangeが要素数を超えたTake

Takeには[Range](https://learn.microsoft.com/en-us/dotnet/api/system.range?view=net-8.0)を引数にとるオーバーロードも存在します。

次のコードのように`IEnumerable<T>`の要素を超えるRangeを引数に渡しても、例外はスローされません。

```csharp
[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になります。

```csharp
[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でないことに注意して下しさい。

```csharp
[Test]
public void TestSumNullNullable()
{
    IEnumerable<int?> nullableInts = [null];
    int? sum = nullableInts.Sum();
    Assert.That(sum, Is.EqualTo(0));
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.sum?view=net-8.0#system-linq-enumerable-sum(system-collections-generic-ienumerable((system-nullable((system-int32))))))には、次のような記載があります。

> 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などの最小値を下回ってしまった場合も同様です。

```csharp
[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がスローされます。

```csharp
[Test]
public void TestAverageThrowException()
{
    IEnumerable<int> ints = [];
    Assert.Throws<InvalidOperationException>(() =>
    {
        double average = ints.Average();
    });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-8.0#system-linq-enumerable-average(system-collections-generic-ienumerable((system-int32))))には、次のような記載があります。

> InvalidOperationException
> source contains no elements.

see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-8.0#system-linq-enumerable-average(system-collections-generic-ienumerable((system-int32)))


一方で`IEnumerable<int?>`や`IEnumerable<double?>`など、Null許容値型のオーバーロードでは、空でAverageメソッドなどを呼び出すと、nullを返します。

```csharp
[Test]
public void TestAverageNull()
{
    IEnumerable<int?> ints = [];
    double? average = ints.Average();
    Assert.Null(average);
}
```

また`IEnumerable<int?>`や`IEnumerable<double?>`など、Nullable<T>な型のオーバーロードで、nullしか要素に持たない場合でAverageメソッドなどを呼び出しても、nullを返します。

```csharp
[Test]
public void TestAverageOnlyNull()
{
    IEnumerable<int?> ints = [null];
    double? average = ints.Average();
    Assert.Null(average);
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-8.0#system-linq-enumerable-average(system-collections-generic-ienumerable((system-nullable((system-int32))))))の記載は次の通りです。

> The average of the sequence of values, or null if the source sequence is empty or contains only values that are null.

see : https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-8.0#system-linq-enumerable-average(system-collections-generic-ienumerable((system-nullable((system-int32)))))

MinやMaxも同様です。


# 平均計算途中で最大値を超えたAverage

long.MaxValueは「9223372036854775807」です。
long.MinValueは「-9223372036854775808」です。

では、「long.MaxValueとlong.MinValueと1」の3個の平均はいくつでしょうか？そう0.0ですね。

IEnumerable<long>のAverageメソッドでも「次のコード」では、次の示すように、平均は0.0になります。

```csharp
[Test]
public void TestAverageNotOverflow()
{
    IEnumerable<long> longs = [long.MaxValue, long.MinValue, 1];
    double average = longs.Average();
    Assert.That(average, Is.EqualTo(0.0));
}
```

少し順番を変えてみましょう。似たようなコードですが、「次のコード」では、OverflowExceptionが発生します。

```csharp
[Test]
public void TestAverageOverflow()
{
    IEnumerable<long> longs = [1, long.MaxValue, long.MinValue];
    Assert.Throws<OverflowException>(() =>
    {
        double average = longs.Average();
    });
}
```

実は、平均計算途中でlongやdecimalの表現範囲を超えた場合、OverflowExceptionが発生します。

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.average?view=net-7.0)の記載は次のとおりです。

> OverflowException
> The sum of the elements in the sequence is larger than Int64.MaxValue.

ドキュメントの記載はちょっと不正確で、次のようにInt64.MinValue(long.MinValue)を下回っても例外が発生します。

```csharp
[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を超えるような時

```csharp
[Test]
public void TestRangeOverElementIntMaxValue()
{
    Assert.Throws<ArgumentOutOfRangeException>(() => { 
        Enumerable.Range(start: int.MaxValue, count: 2);
    });

    Assert.Throws<ArgumentOutOfRangeException>(() => { 
        Enumerable.Range(start: 2, count: int.MaxValue);
    });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.range?view=net-8.0)には、次のような記載があります。

> 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がスローされます。

```csharp
[Test]
public void TestRangeCountUnder0()
{
    Assert.Throws<ArgumentOutOfRangeException>(() => {
        Enumerable.Range(start: 10, count: -1);
    });
}
```    

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.range?view=net-8.0)には、次のような記載があります。

> ArgumentOutOfRangeException
> count is less than 0.

なお、引数countに0を渡してRangeメソッドを呼び出すと、空の`IEnumerable<int>`になります。

```csharp
[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がスローされます。

```csharp
[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>`を返します。

```csharp
[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は積集合を生成するメソッドです。

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.intersect?view=net-8.0)には、次のような記載があります。

> 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）において要素が出現する順番になります。

```csharp
[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をスローします。


```csharp
[Test]
public void TestAggregateProduct()
{
    IEnumerable<int> numbers = [];
    Assert.Throws<InvalidOperationException>(() =>
    {
        int product = numbers.Aggregate((lhs, rhs) => lhs * rhs);
    });
}
```

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.repeat?view=net-8.0)には、次のように記載があります。

>　InvalidOperationException
> source contains no elements.

一方、空の`IEnumerable<T>`で、引数として初期値seedをとる2つのオーバーロードのAggregateメソッドを呼び出しても、InvalidOperationExceptionはスローされません。

```csharp
[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の[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.groupjoin?view=net-8.0)に次のような記載があります。

> 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の内容を、例を用いて補足します。次のようなレコードがあります。

```csharp
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を生成します。


```csharp
[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の結果中に存在すること

に注目してください。

[公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.groupjoin?view=net-8.0#remarks)の

> 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は、使う機会が非常に多いです。だからこそ、その仕様・挙動を正確に理解していないと、思わぬ不具合を発生させてしまいます。正しく仕様・挙動を把握して、やっていきましょう！