1. 入门
操作集合的时候,咱们经常不只是遍历每个元素做点啥,而是想对整个数据集来个简短的“总结”。比如:
- 统计有多少学生拿了5分。
- 想知道学校里一共有多少学生。
- 算一下所有学生考试总分是多少。
- 找出最高分和最低分。
- 算一下全班的平均分。
当然,这些事用普通循环加个计数变量也能搞定。但说实话,没人喜欢为这种小事写二十行代码。
LINQ 提供了一套聚合函数——现成的方法,直接拿集合,把所有元素过一遍,给你答案:总和、平均、最大、最小,有时候还能玩点更骚的。
来个简明“聚合表”:
| 方法 | 干嘛用的 | 返回啥 |
|---|---|---|
|
统计元素个数 | |
|
求数值的总和 | 看元素类型(、等) |
|
算平均值 | 或其他数值类型 |
|
找最大元素 | 集合里的元素 |
|
找最小元素 | 集合里的元素 |
这些方法都是实现了 IEnumerable<T> 的集合的扩展方法。详细看 LINQ聚合方法官方文档。
2. 实战前准备数据
我们继续用学生管理的小例子。假设有这么个类:
// 学生模型
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // 五分制成绩
public string Email { get; set; }
}
还有个列表:
// 学生基础列表
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($"5分学生数: {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表达式),对每个元素返回一个值。
直接对数字集合求和
如果你有个纯数字数组,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("没有数据,没法算平均值!");
对数字数组求平均
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); // 其实永远是5,但举例说明
// 只算分数不低于4的学生总分
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// 统计邮箱去重后的数量(以防万一)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
这时候LINQ就“开挂”了:各种操作随便组合,代码又短又清楚,连你家猫都能看懂(如果你家猫是Junior C# Developer)。
8. 和“手写”循环对比:为啥用聚合?
来对比下LINQ和普通循环,比如算平均分:
普通写法:
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语法吗?
新手经常问:“这些能用query语法写吗?” 简单说:query语法本身没聚合关键字,但你可以和method syntax混用:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
括号里是query-syntax,后面直接用聚合方法。其实大家都这么写!
10. LINQ常见错误
错误1:对空集合用 Average()、Max() 或 Min()。
空集合时,Sum() 和 Count() 会返回 0,但 Average()、Max()、Min() 会抛异常。用这些方法前,先确保集合有元素。
错误2:lambda签名写错。
比如你传了字符串给聚合函数(Sum、Max等),会编译报错。用匿名方法时尤其容易写错。
错误3:用不高效的计数方式。
Where(...).Count() 先生成新集合再计数。用 Count(predicate) 直接统计,效率高多了。
错误4:聚合nullable类型时没注意细节。
比如你对 int? 求和,Sum() 会忽略 null。这本身没问题,但有时会让你结果和预期不一样,提前想好就行。
GO TO FULL VERSION