概要
2024年11月に.NET 9とC# 13がリリースされました。
この投稿では、.NET 9でLINQに追加された
- Index
- CountBy
- AggregateBy
について紹介します。
過去の追加されたLINQメソッドの記事はこちら
Index
Indexメソッドは、IEnumerable<TSource>
の各要素をインデックスと合わせてタプルにした、IEnumerable<(int Index, TSource Item)>
を返すメソッドです。
IEnumerable<string> lines = LoadLines();
foreach ((int index, string line) in lines.Index())
{
Console.WriteLine($"index: {index}, line: {line}");
}
なお.NET 9より前でも、Select
のindexを引数にとるオーバーロードを使えば、同じようなことができます。
foreach ((int index, string line) in lines.Select((string line, int index) => (index, line)))
{
Console.WriteLine($"line:{index}, line:{line}");
}
また、Whereにもindexを引数にとるオーバーロードがあります。
foreach(string line in lines.Where((string line, int index) => index %2 == 0)) {
Console.WriteLine($"line:{line}");
}
微妙に仕様が違いますがC#の人気LINQ拡張ライブラリ、MoreLINQでもIndexメソッドがあります。
また。他のプログラミング言語にも、似たようなメソッドがあります。他のプログラミング言語に慣れ親しんだ人にとって、Indexメソッドの追加はそこそこ嬉しい追加ではないでしょうか。
- F#: indexed
- Kotlin: withIndex
- Groovy: withIndex
- Scala: zipWithIndex
CountBy
CountByメソッドは、IEnumerable<TSource>
をグループ分けして、グループごとの個数を求めるメソッドです。
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}");
}
返値型がDictionary<TKey, int>
ではなくて、IEnumerable<KeyValuePair<TKey, int>>
なことに注意してください。もしDictionary<TKey, int>
が欲しかったら、追加でToDictionaryメソッドを使いましょう。
Dictionary<int, int> counted = lines.CountBy(it => it.Length).ToDictionary();
CountByは返り値型がIEnumerable<KeyValuePair<TKey, int>>
ですが、メソッドの性質上、返り値を一つでも要素列挙したらソースのIEnumerable<TSource>
が全件列挙されることに注意してください。
C#の人気LINQ拡張ライブラリMoreLINQでも、ほぼ同じ仕様でCountByメソッドがあります。
このメソッドも、別のプログログラミング言語のコレクションライブラリに、同じ様なメソッドが比較的存在します。F#にも同名のcountByメソッドが、Groovyにも同名のcountByメソッドがあります。
AggregateBy
AggregateByはGroupByとAggregateを合体させたようなメソッドです。IEnumerable<TSource>
をグルーピングしつつ、グループごとにアグリゲーション(まとめる)メソッドです。
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}")
};
}
}
↑こんなクラスを使って、↓こんな使い方ができます。
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}");
}
AggregateByは次の2つのオーバーロードがあります。
- 全グループで共通のseedを引数に渡すオーバーロード
- グループごとに異なるseedを引数に設定できるseedSelectorを指定するオーバーロード
AggregateByは、GroupByのkeySelectorとresultSelectorを引数にとるオーバーロードで似たようなことができます。
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プロジェクトで、どんどん活用していきましょう!