LINQのGroupByメソッドについて

C#のLINQのメソッドの一つ、[GroupBy](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0)メソッドはシーケンス（`IEnumerable<TSource>`）の要素をグループ化するメソッドです。

このメソッドは8個のオーバーロードがあります。[公式リファレンス](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0)を読めば、それぞれのオーバーロードの違い・使い分けはわかりますが、人によっては混乱するかもしれません。本記事では、[公式リファレンス](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0)よりもわかりやすい解説を目指して、GroupByメソッドを紹介します。

※ 本記事では、LINQ to ObjectsのGroupByメソッドについて説明します。
※ 本記事では、.NET 9時点の情報に基づいて執筆しています。

# 本記事で使う型とデータ

本記事では、次の型を使います。

```csharp
// 本記事で使うPurchase型
public record Purchase(
    string Name,
    decimal PriceYen,
    string CategoryId
);
```

また、次のデータを使います。

```csharp
// 本記事で使うpurchaseList
var purchaseList = new List<Purchase>
{
    new("PC", 300000, "id_electronics"),
    new("乾電池", 600, "id_electronics"),
    new("靴下", 800, "id_clothing"),
    new("少年漫画J", 500, "id_books"),
    new("コーラ", 150, "id_groceries"),
    new("青年漫画YJ", 600, "id_books"),
    new("雑誌", 1200, "id_books"),
    new("技術書", 5000, "id_books"),
    new("牛乳", 180, "id_groceries"),
    new("Tシャツ", 2500, "id_clothing"),
};
```

# 基本のオーバーロード

まずは、GroupByメソッドの一番簡単で基本的なオーバーロード

[`GroupBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-8.0#system-linq-enumerable-groupby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))))

を紹介します。次のコードは、この基本のオーバーロードを使い

「シーケンスpurchaseListから、CategoryIdが同じ要素ごとにグループ化したシーケンスを作る」

コードです。
        
```csharp
// GroupByメソッドの基本のオーバーロードの返値型は、IEnumerable<IGrouping<TKey, TSource>>
// ここでは、TKeyはstringで、TSourceはPurchase
IEnumerable<IGrouping<string, Purchase>> groups = purchaseHistory.GroupBy(
    // CategoryIdが同じものをまとめて一つのグループにしたい。
    // 元の要素を引数に取り、どのグループにまとめるかの判定を行うための「キー」を返すデリゲート、
    // 「キーセレクター」を引数に取る
    keySelector: purchase => purchase.CategoryId
);

foreach (IGrouping<string, Purchase> grouping in groups)
{
    // IGrouping<TKey, TElement>インターフェースはKeyプロパティーを持つ
    // グループ化で使ったキーである
    Console.WriteLine(grouping.Key);

    // IGrouping<TKey, TElement>インターフェースはIEnumerable<TElement>を実装している
    // foreachでまわせるし、LINQも使える
    foreach (Purchase purchase in grouping)
    {
        // IGrouping内の順番は、もとのシーケンスの順序を保つ
        Console.WriteLine($"  {purchase}");
    }
}
```

↑のコードでは、同じCateoryIdをもつPurchaseを同じグループにまとめています。keySelectorは「シーケンスの要素を引数に取り、グループにまとめる判定を行うための"キー"を返すデリゲート」です。

実行結果は次のようになります。

```
id_electronics
  Purchase { Name = PC, PriceYen = 300000, CategoryId = id_electronics }
  Purchase { Name = 乾電池, PriceYen = 600, CategoryId = id_electronics }
id_clothing
  Purchase { Name = 靴下, PriceYen = 800, CategoryId = id_clothing }
  Purchase { Name = Tシャツ, PriceYen = 2500, CategoryId = id_clothing }
id_books
  Purchase { Name = 少年漫画J, PriceYen = 500, CategoryId = id_books }
  Purchase { Name = 青年漫画YJ, PriceYen = 600, CategoryId = id_books }
  Purchase { Name = 雑誌, PriceYen = 1200, CategoryId = id_books }
  Purchase { Name = 技術書, PriceYen = 5000, CategoryId = id_books }
id_groceries
  Purchase { Name = コーラ, PriceYen = 150, CategoryId = id_groceries }
  Purchase { Name = 牛乳, PriceYen = 180, CategoryId = id_groceries }
```

```
「指定されたキーセレクター関数に従ってシーケンスの要素をグループ化します。」
```

という公式リファレンスの説明も、こうやってみると納得ですね。

GroupByメソッドの基本のオーバーロードの返値型は、

「`IEnumerable<IGrouping<TKey, TSource>>`」型

です。（この例だと、`IEnumerable<IGrouping<string, Purchase>`型）

`IGrouping<TKey, TElement>`インターフェースは、`IEnumerable<TElement>`を実装していて、foreachでまわせますし、LINQも使えます。

# 元シーケンスと違う型でグループ化するオーバーロード

次は、元のシーケンスとは違う型のグループを作るGroupByメソッドのオーバーロード

[`GroupBy<TSource,TKey,TElement>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TSource,TElement>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-3(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-0-2))))

を紹介ます。次のコードは、

「シーケンスpurchaseListから、"CategoryIdが同じ要素ごとにグループ化した、商品名Nameのグループ"のシーケンスを作る」

コードです。

```csharp
IEnumerable<IGrouping<string, string>> groups = purchaseList.GroupBy(
    // 元の要素を、どのグループにまとめるかの判定を行うための「キー」を返すデリゲート
    keySelector: purchase => purchase.CategoryId,
    // 元の要素を、どのように変換（射影）するかのデリゲート
    elementSelector: purchase => purchase.Name
);

foreach (IGrouping<string, string> grouping in groups)
{
    Console.WriteLine(grouping.Key);
    // IGrouping<string, string>の要素は、Purchaseではなくてstring。
    // PurchaseのNameが入っている
    foreach (var name in grouping)
    {
        Console.WriteLine($"  {name}");
    }
}
```

このオーバーロードでは、keySelectorに加えて、引数として「グループ要素をどのように変換するかを司る、elementSelector」を取っていますね。

実行結果は次のようになります。

```
id_electronics
  PC
  乾電池
id_clothing
  靴下
  Tシャツ
id_books
  少年漫画J
  青年漫画YJ
  雑誌
  技術書
id_groceries
  コーラ
  牛乳
```

基本のオーバーロードと違い、このオーバーロードでは、グループ要素の型を変更することができますね。

```
指定されたキーセレクター関数に従ってシーケンスの要素をグループ化し、指定された関数を使用して各グループの要素を射影します。
```

という公式リファレンスの説明も、こうやってみると納得ですね。

# キーとグループ要素から結果の要素を生成するオーバーロード

次は、返値型として`IGrouping<TKey, TSource>`のシーケンスではなく、`TResult`なシーケンスを返すオーバーロード

[`GroupBy<TSource,TKey,TResult>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TKey,IEnumerable<TSource>,TResult>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-3(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-1-system-collections-generic-ienumerable((-0))-2))))

を紹介ます。新たに次のような型を定義します。

```csharp
public record CategoryGroupedPurchase(
    string CategoryId,
    List<Purchase> Purchases
);
```

次のコードは、

「シーケンスpurchaseListから、CategoryIdが同じ要素ごとにグループ化し、そのグループのキーとグループ要素から、CategoryGroupedPurchaseのシーケンスを作る」

コードです。

```csharp
// keySelectorとresultSelectorを引数にとるオーバーロードでは、
// 返値型が`IEnumerable<IGrouping<TKey, TSource>>`ではなくて、`IEnumerable<TResult>`。
// ここでは、IEnumerable<CategoryGroupedPurchase>。
IEnumerable<CategoryGroupedPurchase> groups = purchaseList.GroupBy(
    // 元の要素を、どのグループにまとめるかの判定を行うための「キー」を返すデリゲート
    keySelector: purchase => purchase.CategoryId,
    // グループのキーとグループ要素から、返値の要素を生成するデリゲート
    resultSelector: (categoryId, purchases) => new CategoryGroupedPurchase(
        CategoryId: categoryId,
        Purchases: purchases.ToList()
    )
);

// groupsは、IEnumerable<CategoryGroupedPurchase>だから
// その要素は、CategoryGroupedPurchase型
foreach (CategoryGroupedPurchase group in groups)
{
    Console.WriteLine(group.CategoryId);

    foreach (Purchase purchase in group.Purchases)
    {
        Console.WriteLine($"  {purchase}");
    }
}
```

このオーバーロードでは、keySelectorに加えて、引数として「グループキーとグループ要素から、結果の要素を生成するデリゲート、resultSelector」を取っていますね。

また、返値型が`IEnumerable<CategoryGroupedPurchase>`であることにも注目してください。`IEnumerable<IGrouping<TKey, TSource>`ではありません。

実行結果は次のようになります。

```
id_electronics
  Purchase { Name = PC, PriceYen = 300000, CategoryId = id_electronics }
  Purchase { Name = 乾電池, PriceYen = 600, CategoryId = id_electronics }
id_clothing
  Purchase { Name = 靴下, PriceYen = 800, CategoryId = id_clothing }
  Purchase { Name = Tシャツ, PriceYen = 2500, CategoryId = id_clothing }
id_books
  Purchase { Name = 少年漫画J, PriceYen = 500, CategoryId = id_books }
  Purchase { Name = 青年漫画YJ, PriceYen = 600, CategoryId = id_books }
  Purchase { Name = 雑誌, PriceYen = 1200, CategoryId = id_books }
  Purchase { Name = 技術書, PriceYen = 5000, CategoryId = id_books }
id_groceries
  Purchase { Name = コーラ, PriceYen = 150, CategoryId = id_groceries }
  Purchase { Name = 牛乳, PriceYen = 180, CategoryId = id_groceries }
```

```
指定されたキーセレクター関数に従ってシーケンスの要素をグループ化し、各グループとそのキーから結果値を作成します。
```

という公式リファレンスの説明も、こうやってみると納得ですね。

# 一度要素を変換しグループ化し、キーとグループ要素をさらに変換するオーバーロード

次は一度要素を変換してグループ化し、キーとグループ要素をさらに変換するオーバーロード

[`GroupBy<TSource,TKey,TElement,TResult>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TSource,TElement>, Func<TKey,IEnumerable<TElement>,TResult>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-4(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-0-2))-system-func((-1-system-collections-generic-ienumerable((-2))-3))))

を紹介します。新たに次のような型を定義します。

```csharp
public record CategoryPriceStatistics(
    string CategoryId,
    decimal TotalPriceYen,
    decimal AveragePriceYen,
    decimal MaxPriceYen,
    decimal MinPriceYen,
    int Count
);
```

次のコードは、

シーケンスpurchaseListから、
CategoryIdが同じ要素ごとにグループ化し、
グループ化する際に要素はdecimal型のPriceYenに変換（射影）して、
そのグループのキーとグループ要素からCategoryPriceStatisticsのシーケンスを作る

コードです。

```csharp
// keySelector、elementSelector、resultSelectorを引数にとるオーバーロードでは、
// 返値型が`IEnumerable<IGrouping<TKey, TSource>>`ではなくて、`IEnumerable<TResult>`。
// ここでは、IEnumerable<CategoryPriceStatistics>。
IEnumerable<CategoryPriceStatistics> groups = purchaseList.GroupBy(
    // 元の要素を、どのグループにまとめるかの判定を行うための「キー」を返すデリゲート
    keySelector: purchase => purchase.CategoryId,
    // 元の要素から、グループ要素に変換するデリゲート
    elementSelector: purchase => purchase.PriceYen,
    // グループのキーとグループ要素から、返値の要素を生成するデリゲート
    resultSelector: (categoryId, prices) => new CategoryPriceStatistics(
        CategoryId: categoryId,
        TotalPriceYen: prices.Sum(),
        AveragePriceYen: prices.Average(),
        MaxPriceYen: prices.Max(),
        MinPriceYen: prices.Min(),
        Count: prices.Count()
    )
);

// groupsは、IEnumerable<CategoryPriceStatistics>だから
// その要素は、CategoryPriceStatistics型
foreach (CategoryPriceStatistics group in groups)
{
    Console.WriteLine(group.CategoryId);
    Console.WriteLine($"  {group}");
}
```

先に説明した

* 元シーケンスと違う型でグループ化するオーバーロード
* キーとグループ要素から結果の要素を生成するオーバーロード

を組み合わせたようなオーバーロードですね。

```
指定されたキーセレクター関数に従ってシーケンスの要素をグループ化し、各グループとそのキーから結果値を作成します。 各グループの要素は、指定された関数を使用して射影されます。
```

という公式リファレンスの説明も・・・。まぁ、うん。なんとなく納得できますね。

# 追加で`IEqualityComparer<TKey>`を引数にとる4つのオーバーロード

次の4個のGroupByメソッドのオーバーロードを紹介しました。

* 基本のオーバーロード
* 元シーケンスと違う型でグループ化するオーバーロード
* キーとグループ要素から結果の要素を生成するオーバーロード
* 一度要素を変換しグループ化し、キーとグループ要素をさらに変換するオーバーロード

それらのオーバーロードそれぞれに、`IEqualityComparer<TKey>`も追加で引数にとるオーバーロードが4個あります。

* [`GroupBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>, IEqualityComparer<TKey>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-2(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-collections-generic-iequalitycomparer((-1))))
* [`GroupBy<TSource,TKey,TElement>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TSource,TElement>, IEqualityComparer<TKey>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-3(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-0-2))-system-collections-generic-iequalitycomparer((-1))))
* [`GroupBy<TSource,TKey,TResult>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TKey,IEnumerable<TSource>,TResult>, IEqualityComparer<TKey>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-3(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-1-system-collections-generic-ienumerable((-0))-2))-system-collections-generic-iequalitycomparer((-1))))
* [`GroupBy<TSource,TKey,TElement,TResult>(IEnumerable<TSource>, Func<TSource, TKey>, Func<TSource,TElement>, Func<TKey,IEnumerable<TElement>, TResult>, IEqualityComparer<TKey>)`](https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.groupby?view=net-9.0#system-linq-enumerable-groupby-4(system-collections-generic-ienumerable((-0))-system-func((-0-1))-system-func((-0-2))-system-func((-1-system-collections-generic-ienumerable((-2))-3))-system-collections-generic-iequalitycomparer((-1))))

# GroupByメソッドの補足

GroupByメソッドを呼び出すだけでは、シーケンス要素のグループ化は実行されません。

現在の実装では、GroupByメソッドの戻り値の1つ目を列挙した際に、元シーケンスの要素が全て列挙される点に注意してください。（全部の要素をすべて読み込まないとグループ化できないですよね。）

例に使ったデータと、基本のオーバーロードのコード例を再掲します。

```csharp

var purchaseList = new List<Purchase>
{
    new("PC", 300000, "id_electronics"),
    new("乾電池", 600, "id_electronics"),
    new("靴下", 800, "id_clothing"),
    new("少年漫画J", 500, "id_books"),
    new("コーラ", 150, "id_groceries"),
    new("青年漫画YJ", 600, "id_books"),
    new("雑誌", 1200, "id_books"),
    new("技術書", 5000, "id_books"),
    new("牛乳", 180, "id_groceries"),
    new("Tシャツ", 2500, "id_clothing"),
};

IEnumerable<IGrouping<string, Purchase>> groups = purchaseHistory.GroupBy(
    keySelector: purchase => purchase.CategoryId
);

foreach (IGrouping<string, Purchase> grouping in groups)
{
    Console.WriteLine(grouping.Key);

    foreach (Purchase purchase in grouping)
    {
        Console.WriteLine($"  {purchase}");
    }
}
```

↑の実行結果を再掲します。

```
id_electronics
  Purchase { Name = PC, PriceYen = 300000, CategoryId = id_electronics }
  Purchase { Name = 乾電池, PriceYen = 600, CategoryId = id_electronics }
id_clothing
  Purchase { Name = 靴下, PriceYen = 800, CategoryId = id_clothing }
  Purchase { Name = Tシャツ, PriceYen = 2500, CategoryId = id_clothing }
id_books
  Purchase { Name = 少年漫画J, PriceYen = 500, CategoryId = id_books }
  Purchase { Name = 青年漫画YJ, PriceYen = 600, CategoryId = id_books }
  Purchase { Name = 雑誌, PriceYen = 1200, CategoryId = id_books }
  Purchase { Name = 技術書, PriceYen = 5000, CategoryId = id_books }
id_groceries
  Purchase { Name = コーラ, PriceYen = 150, CategoryId = id_groceries }
  Purchase { Name = 牛乳, PriceYen = 180, CategoryId = id_groceries }
```

GroupByの結果の順序は、元のシーケンスの要素において、グループのキーが登場した順序を保持します。
元のシーケンスでは、まずid_electronics、次にid_clothing、そしてid_books・・・と登場しており、確かにその順序を保持していますね。

GroupByの結果のIGrouping内の要素の順序は、元のシーケンスの要素の順序を保持します。
キーがid_booksのグループに注目すると、少年漫画J、青年漫画YJ、雑誌、技術書と確かに元のシーケンスの順序を保持していますね。

# まとめ

GroupByのオーバーロード、これでもう迷いませんね！

# あなたが求めているのはToLookupかもしれない

本記事ではGroupByメソッドを紹介しましたが、あなたが本当に必要なのはToLookupメソッドかもしれません。

```csharp
ILookup<string, Purchase> purchaseLookup = purchaseList.ToLookup(purchase => purchase.CategoryId);

// 2と表示（id_groceriesグループの要素数）
Console.WriteLine(purchaseLookup["id_groceries"].Count());

// 4と表示（id_booksグループの要素数）
Console.WriteLine(purchaseLookup["id_books"].Count());

foreach (var category in purchaseLookup)
{
    // 各グループのキーを表示
    Console.WriteLine(category.Key);
    foreach (var purchase in category)
    {
        Console.WriteLine($"  {purchase}");
    }
}
```

GroupByメソッドは、メソッドを呼び出すだけでは、シーケンス要素のグループ化は実行されません。GroupByの戻値は、グループ化の実行に必要なすべての情報を格納するオブジェクトです（遅延実行）。また、戻値を複数回列挙（foreachで回したり、LINQのメソッドを使ったり）すると、複数回グループ化の処理が実行されます。

一方で、ToLookupメソッドはメソッド呼び出し時に、即座にグループ化の処理を行い、すぐに`ILookup<TKey, TSource>`を返します。また、グループ化した結果を複数回列挙しても、グループ化処理は一度だけ実行されます。

グループ化した結果を複数回列挙するのであればToLookupを使うことも検討してください。
こちらの記事もどうぞ。

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