.NET 9でLINQに追加されたメソッド

# 概要

2024年11月に[.NET 9](https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/)と[C# 13](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13)がリリースされました。

この投稿では、.NET 9でLINQに追加された

* Index
* CountBy
* AggregateBy

について紹介します。

過去の追加されたLINQメソッドの記事はこちら

https://qiita.com/RyotaMurohoshi/items/aa57e96a01c6627ec08d

https://qiita.com/RyotaMurohoshi/items/71a84fe7a5cf8df57054

https://qiita.com/RyotaMurohoshi/items/14efd4492ac14ee635cd

# Index

Indexメソッドは、`IEnumerable<TSource>`の各要素をインデックスと合わせてタプルにした、`IEnumerable<(int Index, TSource Item)>`を返すメソッドです。


```csharp
IEnumerable<string> lines = LoadLines();

foreach ((int index, string line) in lines.Index())
{
    Console.WriteLine($"index: {index}, line: {line}");
}
```

* [公式サイト](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.index?view=net-9.0)
* [実装PR](https://github.com/dotnet/runtime/pull/95947)
* [提案issue](https://github.com/dotnet/runtime/issues/95563)
* [ソース](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Linq/src/System/Linq/Index.cs)


なお.NET 9より前でも、`Select`のindexを引数にとるオーバーロードを使えば、同じようなことができます。


```csharp
foreach ((int index, string line) in lines.Select((string line, int index) => (index, line)))
{
    Console.WriteLine($"line:{index}, line:{line}");
}
```        

また、Whereにもindexを引数にとるオーバーロードがあります。

```csharp
foreach(string line in lines.Where((string line, int index) => index %2 == 0)) {
    Console.WriteLine($"line:{line}");
}
```

微妙に仕様が違いますがC#の人気LINQ拡張ライブラリ、[MoreLINQ](https://github.com/morelinq/MoreLINQ/tree/master?)でも[Index](https://github.com/morelinq/MoreLINQ/tree/master?tab=readme-ov-file#index)メソッドがあります。


また。他のプログラミング言語にも、似たようなメソッドがあります。他のプログラミング言語に慣れ親しんだ人にとって、Indexメソッドの追加はそこそこ嬉しい追加ではないでしょうか。

* F#: [indexed](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#indexed)
* Kotlin: [withIndex](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/with-index.html)
* Groovy: [withIndex](https://docs.groovy-lang.org/latest/html/groovy-jdk/java/lang/Iterable.html#withIndex)
* Scala: [zipWithIndex](https://www.scala-lang.org/api/3.x/scala/collection/Iterator.html#zipWithIndex-0)

# CountBy

CountByメソッドは、`IEnumerable<TSource>`をグループ分けして、グループごとの個数を求めるメソッドです。

```csharp
IEnumerable<string> lines = GetLines();

IEnumerable<KeyValuePair<int, int>> counted = lines.CountBy(it => it.Length);

foreach ((int length, int count) in counted)
{
    Console.WriteLine($"length:{length}, count:{count}");
}
```

* [公式サイト](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.countby?view=net-9.0)
* [実装PR](https://github.com/dotnet/runtime/pull/91507)
* [提案issue](https://github.com/dotnet/runtime/issues/77716)
* [ソース](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Linq/src/System/Linq/CountBy.cs)

返値型が`Dictionary<TKey, int>`ではなくて、`IEnumerable<KeyValuePair<TKey, int>>`なことに注意してください。もし`Dictionary<TKey, int>`が欲しかったら、追加でToDictionaryメソッドを使いましょう。

```csharp
Dictionary<int, int> counted = lines.CountBy(it => it.Length).ToDictionary();
```

CountByは返り値型が`IEnumerable<KeyValuePair<TKey, int>>`ですが、メソッドの性質上、返り値を一つでも要素列挙したらソースの`IEnumerable<TSource>`が全件列挙されることに注意してください。

C#の人気LINQ拡張ライブラリ[MoreLINQ](https://github.com/morelinq/MoreLINQ/tree/master?)でも、ほぼ同じ仕様で[CountBy](https://github.com/morelinq/MoreLINQ/tree/master?tab=readme-ov-file#countBy)メソッドがあります。

このメソッドも、別のプログログラミング言語のコレクションライブラリに、同じ様なメソッドが比較的存在します。F#にも同名の[countBy](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#countBy)メソッドが、Groovyにも同名の[countBy](https://docs.groovy-lang.org/latest/html/groovy-jdk/java/lang/Iterable.html#countBy(groovy.lang.Closure))メソッドがあります。

# AggregateBy

AggregateByは[GroupBy](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0)と[Aggregate](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.aggregate?view=net-9.0)を合体させたようなメソッドです。`IEnumerable<TSource>`をグルーピングしつつ、グループごとにアグリゲーション（まとめる）メソッドです。

```csharp
public record struct Input(int PlayerId, Direction Move);

public enum Direction
{
    Up,
    Right,
    Down,
    Left
}

public record struct Position(int X, int Y)
{
    public Position Move(Direction direction)
    {
        return direction switch
        {
            Direction.Up => this with { Y = Y + 1 },
            Direction.Right => this with { X = X + 1 },
            Direction.Down => this with { Y = Y - 1 },
            Direction.Left => this with { X = X - 1 },
            _ => throw new ArgumentOutOfRangeException(nameof(direction), $"Not expected direction value: {direction}")
        };
    }
}
```

↑こんなクラスを使って、↓こんな使い方ができます。

```csharp
IEnumerable<Input> lines = GetInputs();

IEnumerable<KeyValuePair<int, Position>> aggregated = lines.AggregateBy(
    keySelector: input => input.PlayerId,
    seed: new Position(0, 0),
    func: (position, input) => position.Move(input.Move)
);

foreach ((int playerId, Position position) in aggregated)
{
    Console.WriteLine($"playerId:{playerId}, position:{position}");
}
```

* [公式サイト](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.aggregateby?view=net-9.0)
* [実装PR](https://github.com/dotnet/runtime/pull/92089)
* [提案issue](https://github.com/dotnet/runtime/issues/91533)
* [ソース](https://github.com/dotnet/runtime/blob/v9.0.0/src/libraries/System.Linq/src/System/Linq/AggregateBy.cs)

AggregateByは次の2つのオーバーロードがあります。

* 全グループで共通のseedを引数に渡すオーバーロード
* グループごとに異なるseedを引数に設定できるseedSelectorを指定するオーバーロード

AggregateByは、GroupByのkeySelectorとresultSelectorを引数にとるオーバーロードで似たようなことができます。

```csharp
IEnumerable<(int playerId, Position position)> aggregated = lines.GroupBy(
    keySelector: input => input.PlayerId,
    resultSelector: (int playerId, IEnumerable<Input> moves) =>
        (
            playerId: playerId, 
            position: moves.Aggregate(new Position(0, 0), (position, input) => position.Move(input.Move)
        )
    )
);
```

似たようなことはGroupByを使ってできますが、GroupByは計算の途中で全部の要素を保持する必要がある点に注意してください。AggregateByは計算の途中で全部の要素を保持せず、グループごとに途中のまとめた値だけを保持すれば良い、という違いがあります。使い方・状況によっては、パフォーマンス効率で大きく差が出るかもしれません。

AggregateByは返り値型が`IEnumerable<KeyValuePair<TKey,TAccumulate>>`ですが、メソッドの性質上、返り値を一つでも要素列挙したらソースの`IEnumerable<TSource>`が全件列挙されることに注意してください。

# まとめ

この投稿では、.NET 9で追加されたLINQメソッドを紹介しました。.NET 9プロジェクトで、どんどん活用していきましょう！