CodeGym /課程 /C# SELF /group by 來分組資料

group by 來分組資料

C# SELF
等級 32 , 課堂 0
開放

1. 前言

分組資料,就是把集合裡的元素根據某個共通特徵分到不同的「籃子」裡。想像你在把蘋果按顏色分類:紅色的放一籃,綠色的放另一籃,黃色的再放一籃。在程式裡這就叫做分組

為什麼要分組?
在實際任務裡,常常會需要把人按城市分組、把商品按類別分組、把交易按日期分組等等。這樣可以做很多有用的分析:比如知道每個班有多少學生,或哪些城市學生最多。

LINQ 為這個目的提供了一個特別的方法:GroupBy。另外在 query syntax 裡也有一個很方便的運算子,就是 group by。我們來用我們的學生和班級管理系統做例子,拆解這個流程。

2. GroupBy 在 method syntax 裡的用法

主要簽名和回傳結果

GroupBy 方法看起來有點嚇人:


IEnumerable<IGrouping<TKey, TElement>> GroupBy<TElement, TKey>(
    this IEnumerable<TElement> source,
    Func<TElement, TKey> keySelector
)

說明:

  • source — 原始集合(比如學生的 list)。
  • keySelector — 決定分組依據的函數,比如 ClassNameCity 屬性。

重點!
結果不是單純的物件陣列,而是一個特殊的 group 集合(IGrouping<TKey, TElement>)。每個 group 包含:

  • group 的 key(Key — 把元素聚在一起的那個值),
  • 屬於這個 group 的元素集合。

圖解:

Key(關鍵值) Group 裡的元素(物件群)
"9A" 伊凡、奧列格、瑪麗亞
"10Б" 斯維特蘭娜、彼得
... ...

範例 1:按班級名稱分組學生

假設我們有一個 Student 類別:


public class Student
{
    public string Name { get; set; }
    public string ClassName { get; set; }
    public int Grade { get; set; }
}

還有一個學生集合:


var students = new List<Student>
{
    new Student { Name = "伊凡", ClassName = "9A", Grade = 5 },
    new Student { Name = "奧列格", ClassName = "9A", Grade = 4 },
    new Student { Name = "瑪麗亞", ClassName = "10Б", Grade = 5 },
    new Student { Name = "斯維特蘭娜", ClassName = "10Б", Grade = 3 }
};

把學生按班級分組:


var studentsByClass = students.GroupBy(s => s.ClassName);

foreach (var group in studentsByClass)
{
    Console.WriteLine($"班級: {group.Key}"); // group 的 key
    foreach (var student in group)
    {
        Console.WriteLine($" - {student.Name}, 分數: {student.Grade}");
    }
}

發生了什麼?
GroupBy 幫我們建立了兩個 group:一個是 "9A",另一個是 "10Б"。你可以遍歷每個 group,輸出每一「堆」的聚合資訊。

視覺化:GroupBy 之後集合的樣子

可以這樣想:


students.GroupBy(s => s.ClassName)
        ├─ group "9A" { 伊凡, 奧列格 }
        └─ group "10Б" { 瑪麗亞, 斯維特蘭娜 }

每個「分支」就是一個用 key 聚合起來的小學生清單。

按分數分組學生:用任意條件分組

我們來試試不是按班級,而是按分數分組!


var studentsByGrade = students.GroupBy(s => s.Grade);

foreach (var group in studentsByGrade)
{
    Console.WriteLine($"分數: {group.Key}");
    foreach (var student in group)
    {
        Console.WriteLine($" - {student.Name} 來自班級 {student.ClassName}");
    }
}

輸出:


分數: 5
 - 伊凡 來自班級 9A
 - 瑪麗亞 來自班級 10Б
分數: 4
 - 奧列格 來自班級 9A
分數: 3
 - 斯維特蘭娜 來自班級 10Б

3. LINQ 裡 group 的後續處理:GroupBy 之後可以做什麼

分組後我們通常不只想印出 group,還想拿到一些聚合資訊——比如每個 group 有幾個元素、平均分數、名字清單等等。

範例 2:計算每個班級的學生數


var classCounts = students
    .GroupBy(s => s.ClassName)
    .Select(g => new { Class = g.Key, Count = g.Count() });

foreach (var cc in classCounts)
{
    Console.WriteLine($"班級 {cc.Class} — {cc.Count} 位學生");
}

結果:


班級 9A — 2 位學生
班級 10Б — 2 位學生
  • 先把學生分組。
  • 然後對每個 group 建立匿名物件,key 是班級名稱,count 用 Count() 算出來。

範例 3:收集每班的優秀學生名字清單


var excellentByClass = students
    .Where(s => s.Grade == 5)
    .GroupBy(s => s.ClassName)
    .Select(g => new 
    {
        Class = g.Key,
        ExcellentStudents = g.Select(s => s.Name).ToList()
    });

foreach (var group in excellentByClass)
{
    Console.WriteLine($"班級 {group.Class} 的優秀學生: {string.Join(", ", group.ExcellentStudents)}");
}

4. group by 在 query syntax 裡的用法

如果你比較習慣 SQL,或是你本來就做資料庫的,LINQ 也支援 group by ... into ... 這種 query syntax。

基本分組範例


var studentsByClass =
    from s in students
    group s by s.ClassName into classGroup
    select classGroup;

foreach (var group in studentsByClass)
{
    Console.WriteLine($"班級: {group.Key}");
    foreach (var student in group)
    {
        Console.WriteLine($" - {student.Name}");
    }
}

先「group」依據某個條件,然後給 group 取個名字(into classGroup)——之後你就可以用這個名字做任何事。

加上聚合的投影

假設我們想知道每個城市有多少學生:


var countsByCity =
    from s in students
    group s by s.ClassName into classGroup
    select new { Class = classGroup.Key, Count = classGroup.Count() };

foreach (var item in countsByCity)
{
    Console.WriteLine($"班級 {item.Class} — {item.Count} 位學生");
}

——語法一樣,但 select 後面可以做任何投影(聚合、清單、平均值等等)。

5. 進階情境

多條件分組(複合 key)

有時候你會想根據多個屬性分組,比如班級和分數一起:


var byClassAndGrade = students
    .GroupBy(s => new { s.ClassName, s.Grade });

foreach (var group in byClassAndGrade)
{
    Console.WriteLine($"班級: {group.Key.ClassName}, 分數: {group.Key.Grade}");
    foreach (var student in group)
    {
        Console.WriteLine($" - {student.Name}");
    }
}

group 的 key 現在是一個匿名型別(多個欄位的組合)。

group 實際上是怎麼運作的?

LINQ 的內部運作

  • 當你用 GroupBy,LINQ 會在內部建立一個「表格」:每個獨特的 key 對應一個 group。
  • group 實作了 IGrouping<TKey, TElement> 介面。你可以用 Key 屬性拿到分組依據的值。

Lazy evaluation 跟效能

  • group 不是馬上算出來的,只有你開始遍歷(foreach)時才會真的建立。所以即使集合很大,也不用怕「創建」 group,只有用到時才會存在。
  • 如果你打算多次用到 group,可以呼叫 .ToList().ToArray() 來「固定」結果。

實際應用

分組可以讓你做很複雜的報表、統計和分析。舉幾個實際例子:

  • 在網路商店:把訂單按用戶分組,顯示每個人的購買紀錄。
  • 在教育平台:把學生按老師或科目分組。
  • 在人資系統:把員工按部門分組,計算 headcount。
  • 面試時:有時會要你寫一個分組加聚合的真實資料查詢。

LINQ 讓這些任務 prototype 超快,會看會寫分組,對 .NET 工程師來說是大加分。

圖解流程

這是一個用 GroupBy 處理集合的簡單流程圖:


[學生集合] 
      |
[GroupBy 依 ClassName]
      |
[Group "9A"] ---> [伊凡, 奧列格]
[Group "10Б"] --> [瑪麗亞, 斯維特蘭娜]

每個 group 就是一個有 key 的小集合。

6. 常見錯誤和陷阱

有些同學一開始會被 GroupBy 的結果搞混:

  • GroupBy 之後,你拿到的不是 List<List<Student>> 這種「group」,而是 IGrouping<TKey, TElement> 集合。要操作 group,請用 Key 屬性和遍歷 group 裡的元素。
  • 如果要用複合 key(多個欄位分組):記得用匿名型別或自己建一個 class,這樣 key 才有結構。
  • 只想知道 group 裡有幾個元素——記得用 Select(g => g.Count()),不然就要自己一個個算。
  • 有時候會被巢狀迴圈搞混:第一層是 group,第二層才是 group 裡的元素。
  • 如果 lambda 或 key 寫錯,你會拿到奇怪的 group(或全部都在同一個 group,如果 key 都一樣)。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION