C# の LINQ でグループ化する GroupBy

GroupBy はシーケンスの要素をキーでグループ化するメソッドだ。SQL の GROUP BY と同様の機能を提供する。

基本的な使い方

同じキーを持つ要素をまとめる。

var people = new List<Person>
{
    new Person { Name = "Alice", City = "Tokyo" },
    new Person { Name = "Bob", City = "Osaka" },
    new Person { Name = "Charlie", City = "Tokyo" },
    new Person { Name = "Diana", City = "Osaka" },
    new Person { Name = "Eve", City = "Tokyo" }
};

var byCity = people.GroupBy(p => p.City);

foreach (var group in byCity)
{
    Console.WriteLine($"--- {group.Key} ---");
    foreach (var person in group)
    {
        Console.WriteLine($"  {person.Name}");
    }
}

出力結果は以下のようになる。

--- Tokyo ---
  Alice
  Charlie
  Eve
--- Osaka ---
  Bob
  Diana

GroupBy の戻り値は IEnumerable<IGrouping<TKey, TElement>> だ。各グループは Key プロパティでグループのキー値にアクセスでき、グループ自体が IEnumerable<TElement> として要素を列挙できる。

グループの集計

グループ化した後に集計を行うのは非常によくあるパターンだ。

var orders = new List&lt;Order&gt;
{
    new Order { Product = "Apple", Quantity = 5 },
    new Order { Product = "Banana", Quantity = 3 },
    new Order { Product = "Apple", Quantity = 2 },
    new Order { Product = "Cherry", Quantity = 10 },
    new Order { Product = "Banana", Quantity = 7 }
};

var summary = orders
    .GroupBy(o => o.Product)
    .Select(g => new 
    {
        Product = g.Key,
        TotalQuantity = g.Sum(o => o.Quantity),
        OrderCount = g.Count()
    });

foreach (var item in summary)
{
    Console.WriteLine($"{item.Product}: {item.TotalQuantity}個 ({item.OrderCount}件)");
}
// Apple: 7個 (2件)
// Banana: 10個 (2件)
// Cherry: 10個 (1件)

商品ごとにグループ化し、各グループの合計数量と注文件数を計算している。

複数キーでのグループ化

匿名型を使えば、複数のプロパティをキーにできる。

var sales = new List&lt;Sale&gt;
{
    new Sale { Year = 2023, Month = 1, Amount = 100 },
    new Sale { Year = 2023, Month = 1, Amount = 200 },
    new Sale { Year = 2023, Month = 2, Amount = 150 },
    new Sale { Year = 2024, Month = 1, Amount = 300 }
};

var byYearMonth = sales
    .GroupBy(s => new { s.Year, s.Month })
    .Select(g => new 
    {
        g.Key.Year,
        g.Key.Month,
        Total = g.Sum(s => s.Amount)
    });

foreach (var item in byYearMonth)
{
    Console.WriteLine($"{item.Year}/{item.Month}: {item.Total}");
}
// 2023/1: 300
// 2023/2: 150
// 2024/1: 300

年と月の組み合わせでグループ化している。匿名型のプロパティは Key.YearKey.Month でアクセスできる。

要素の変換

グループ化と同時に、要素を変換することもできる。

var people = new List&lt;Person&gt;
{
    new Person { Name = "Alice", City = "Tokyo", Age = 25 },
    new Person { Name = "Bob", City = "Osaka", Age = 30 },
    new Person { Name = "Charlie", City = "Tokyo", Age = 35 }
};

// 都市でグループ化し、名前だけを保持
var byCity = people.GroupBy(
    p => p.City,
    p => p.Name
);

foreach (var group in byCity)
{
    Console.WriteLine($"{group.Key}: {string.Join(", ", group)}");
}
// Tokyo: Alice, Charlie
// Osaka: Bob

第二引数のラムダ式で、グループに含める要素の形を指定できる。この例では Person オブジェクト全体ではなく、名前だけをグループに含めている。

クエリ構文での表現

クエリ構文では group ... by を使う。

var people = new List&lt;Person&gt;
{
    new Person { Name = "Alice", City = "Tokyo" },
    new Person { Name = "Bob", City = "Osaka" },
    new Person { Name = "Charlie", City = "Tokyo" }
};

var byCity = from p in people
             group p by p.City;

foreach (var group in byCity)
{
    Console.WriteLine($"{group.Key}: {group.Count()}人");
}
// Tokyo: 2人
// Osaka: 1人

into を使えば、グループ化した結果をさらに操作できる。

var byCity = from p in people
             group p by p.City into cityGroup
             where cityGroup.Count() >= 2
             select new { City = cityGroup.Key, Count = cityGroup.Count() };

ToDictionary との違い

GroupBy と似た機能に ToDictionary があるが、用途が異なる。

GroupBy

同じキーを持つ複数の要素をグループ化。遅延評価。

ToDictionary

各要素をキーと値のペアに変換。キーの重複は例外。即時評価。

キーが重複する可能性がある場合は GroupBy、一意のキーでマッピングする場合は ToDictionary を使う。