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 — 決定分組依據的函數,比如 ClassName 或 City 屬性。
重點!
結果不是單純的物件陣列,而是一個特殊的 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 都一樣)。
GO TO FULL VERSION