4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINQのGroupByメソッドについて

Last updated at Posted at 2025-02-23

C#のLINQのメソッドの一つ、GroupByメソッドはシーケンス(IEnumerable<TSource>)の要素をグループ化するメソッドです。

このメソッドは8個のオーバーロードがあります。公式リファレンスを読めば、それぞれのオーバーロードの違い・使い分けはわかりますが、人によっては混乱するかもしれません。本記事では、公式リファレンスよりもわかりやすい解説を目指して、GroupByメソッドを紹介します。

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

本記事で使う型とデータ

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

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

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

// 本記事で使う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>)

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

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

コードです。

// 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>)

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

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

コードです。

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>)

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

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

次のコードは、

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

コードです。

// 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>)

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

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

次のコードは、

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

コードです。

// 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メソッドの補足

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

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

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


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メソッドかもしれません。

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を使うことも検討してください。
こちらの記事もどうぞ。

4
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?