1. 前言
在處理集合時,常常不只是要一個一個元素處理,而是想要拿到整個資料集的「摘要」。比如說:
- 算一下有多少學生拿到五分。
- 知道學校裡總共有多少學生。
- 算出所有學生考試總共拿了多少分。
- 找出最高分和最低分。
- 算出全班的平均分數。
當然啦,這些都可以用一般 for 迴圈加個計數器來解決。但說真的,誰想為了這種小事寫二十行 code 啦?
LINQ 給你一組聚合函式——現成的方法,直接把集合丟進去,幫你跑過所有元素,直接給你答案:總和、平均、最大、最小,有時還能做更酷的事。
這裡有個「聚合函式速查表」:
| 方法 | 功能 | 回傳值 |
|---|---|---|
|
計算元素數量 | |
|
計算數值總和 | 依元素型別而定(、 ...) |
|
計算平均值 | 或其他數值型別 |
|
找出最大元素 | 集合中的元素 |
|
找出最小元素 | 集合中的元素 |
這些方法都是給有實作 IEnumerable<T> 的集合用的 extension method。更多細節可以看LINQ 聚合方法官方文件。
2. 準備練習用的資料
我們繼續用一個簡單的學生管理小程式。假設有這樣一個 class:
// 學生模型
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // 五分制成績
public string Email { get; set; }
}
還有一個 list:
// 學生基本清單
List<Student> students = new List<Student>
{
new Student { Name = "伊凡", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "奧莉嘉", Grade = 4, Email = "olga@example.com" },
new Student { Name = "阿爾喬姆", Grade = 3, Email = "artem@example.com" },
new Student { Name = "達莉亞", Grade = 5, Email = "darya@example.com" },
new Student { Name = "彼得", Grade = 2, Email = "petr@example.com" }
};
3. 計算元素數量:Count()
最基本的統計
假設你想知道總共有幾個學生:
int totalStudents = students.Count(); // 5
Console.WriteLine($"總學生數:{totalStudents}");
LINQ 魔法:超簡單!Count() 直接回傳集合裡的元素數量。
條件計數
那班上有幾個優等生?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"五分學生數:{excellentStudents}");
這裡我們給 Count 傳一個 lambda——只會算出符合條件(s.Grade == 5)的學生。在 LINQ 裡這等於 Where(...).Count(),但寫法更短也更有效率。
如果集合是空的?
如果集合是空的,Count 會老實回傳 0——不會報錯。
4. 數值加總:Sum()
算出所有成績總和
假設你想知道全班總共拿了多少分:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"所有成績總和:{sumOfGrades}");
Sum 會吃一個 selector(lambda),每個元素回傳一個值。
直接對數字集合加總
如果你只有一個數字 array,selector 就不用寫:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
常見錯誤和細節
如果集合是空的,數值型別的 Sum() 會回傳 0。但如果集合是 nullable 型別(int?、double?),Sum 也會正常運作:null 會被忽略。
5. 平均值:Average()
算出班級平均分
經典問題:學生平均拿幾分?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"平均分數:{averageGrade:F2}");
Average 超適合做統計。注意:回傳值永遠是 double,就算原本是 int,這樣才不會把小數點砍掉。
如果沒有任何元素
如果集合是空的,呼叫 Average() 會丟出 InvalidOperationException。這超常見!如果你不確定集合有沒有東西,記得先檢查一下!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("沒有資料可以算平均!");
數字 array 的平均值
double avg = numbers.Average();
6. 最大值和最小值:Max() 和 Min()
誰是班上之星,誰在墊底
想知道誰把全班分數拉高,誰讓老師頭痛?很簡單:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"最高分:{maxGrade}");
Console.WriteLine($"最低分:{minGrade}");
那這個英雄到底是誰?
有時候不只要分數,還想知道是誰拿的。找出最高分的學生物件:
// 先照分數遞減排序,拿第一個
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"最強學生:{bestStudent.Name} ({bestStudent.Grade})");
新版 .NET 有 MaxBy,可以更優雅地寫。從 .NET 6 開始有,.NET 9 又更方便(看MaxBy 官方文件)。但就算沒 MaxBy,用排序加 .First() 也很夠用啦。
陷阱和差異
如果集合是空的,呼叫 Max() 和 Min() 也會丟 InvalidOperationException。所以最好先檢查一下(尤其你沒先檢查集合的話):
if (students.Any())
Console.WriteLine($"最高分:{students.Max(s => s.Grade)}");
else
Console.WriteLine("沒有學生可以找最大值。");
7. 聚合方法「串接」範例
聚合方法常常會跟篩選、投影一起用:
// 五分學生的平均分(當然永遠是 5,但只是舉例)
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade);
// 分數不低於 4 的學生總分
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// 不重複 email 數量(以防萬一)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
這時 LINQ 就超有感:簡單組合操作,寫出來的 code 超直觀,連你家的貓都看得懂(如果你家貓是 Junior C# Developer 的話)。
8. 跟「手動」寫法比:為什麼要用聚合?
我們來對比一下 LINQ 跟傳統 for 迴圈,拿平均分數來舉例:
傳統寫法:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
就算要處理空集合,也更簡單!
9. 有用的小細節
簡單聊聊效能和實作特性
LINQ 聚合方法跟大多數 LINQ 操作不一樣,是馬上執行的!也就是說你呼叫 Sum()、Count()、Average()、Max()、Min() 時,集合就會直接被跑一遍。這些方法回傳的不是集合,而是一個最終結果。
這很重要:如果你在聚合前做了很重的操作,比如複雜的篩選或轉換——這些只會在聚合呼叫時執行一次。
聚合方法支援 query-syntax 嗎?
新手常問:「這些能不能用 query-syntax 寫?」簡單說:query-syntax 本身沒聚合關鍵字,但你可以跟 method syntax 混用:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
括號裡是 query-syntax,然後直接呼叫聚合方法。這種寫法其實很常見!
10. LINQ 常見錯誤
錯誤一:對空集合用 Average()、Max() 或 Min()。
如果集合是空的,Sum() 和 Count() 會乖乖回傳 0,但 Average()、Max()、Min() 會丟例外。呼叫這些方法前,記得先確定集合有東西。
錯誤二:lambda 寫錯型別。
比如你傳了字串給聚合函式(Sum、Max 等),會直接編譯錯誤。用匿名方法時特別容易出錯。
錯誤三:用不夠優的方式算數量。
Where(...).Count() 會先產生新集合再算數量。其實用 Count(predicate) 就好,直接計算符合條件的數量,效率更高。
錯誤四:聚合 nullable 型別時沒注意細節。
如果你對 int? 用 Sum(),Sum() 會自動忽略 null。這是正常行為,但有時你沒注意到,結果會跟你想的不一樣。
GO TO FULL VERSION