聚合与元素操作
本教程共 100 篇 · 第 71 篇 · 更新于 2026-07-31 · 约 11 分钟阅读
71. 聚合与元素操作
本节目标:学完你能用 LINQ 一键统计数量、求和、求平均,并安全地取出集合中第一个元素或判断是否存在满足条件的元素。
前面几章你学会了筛选、投影、排序、分组。这一章讲”收口”——把一串数据压成一个结果。这类方法叫聚合(aggregation)与元素操作(element operation),是写报表、算总分、判空最常用的一把刀。
Note聚合与元素方法都是立即执行的:调用当下就算出结果,而不是像 Where/Select 那样延迟到遍历时才算。所以每写一次就真的跑一遍。
为什么需要聚合算子
你当然可以手写一个 foreach 去累加、去数个数。但 LINQ 的聚合算子把”常见统计意图”封装成一眼能读懂的名字:看见 Sum 就知道在求和,看见 Average 就知道在求平均。这正是 LINQ 的设计初衷——让”对数据的描述”胜过”对步骤的指挥”。
打个比方:聚合算子就像 Excel 里的 SUM、AVERAGE、COUNT 函数。你不用告诉 Excel “从第一行往下走、把每个格子加起来”,只要写 =SUM(A1:A10) 它就知道要做什么。LINQ 的 numbers.Sum() 同理,把”怎么做”藏起来,你只管说”要什么”。
Tip新手容易陷入”先 foreach 再累加”的惯性。下意识问自己:这是不是数个数、求和、求平均、取最值?是的话,优先用聚合算子,代码更短、意图更清楚。
聚合算子:把多压成一
聚合方法对一整个序列算出一个标量值。最常用的是五个:Count、Sum、Average、Min、Max。
Count:数一数有几个
Count 返回集合里元素的个数。它还可以带一个谓词(predicate),只数满足条件的元素。
namespace CSharpDemo;
int[] numbers = [1, 2, 3, 4, 5, 6];
int total = numbers.Count();
int evens = numbers.Count(n => n % 2 == 0);
Console.WriteLine($"总数 {total},偶数 {evens}");
Tip如果只想知道”有没有/有几个”,用 Count 很直观。但要判断”是否存在”,优先用下面讲的 Any——它找到第一个就停,不用数完全部。
Sum:求和
Sum 对数值序列求和,也支持先投影再求和。比如先取出每个商品的价格再加总。
namespace CSharpDemo;
record Product(int Id, string Name, decimal Price);
decimal[] prices = [9.99m, 19.99m, 5.00m];
decimal sum = prices.Sum();
Console.WriteLine($"总价 {sum}");
List<Product> products =
[
new Product(1, "笔记本", 9.99m),
new Product(2, "键盘", 199.00m),
];
decimal totalPrice = products.Sum(p => p.Price);
Console.WriteLine($"商品总价 {totalPrice}");
Note金额务必用 decimal 而不是 double,避免浮点误差。本教程统一用
9.99m这种带m后缀的写法。
Warning对
int序列用Sum要警惕溢出。当累加和超过int上限(约 21 亿)时,结果会”绕回”成负数而不报错。若数据量或单值可能很大,先投影成long:numbers.Sum(n => (long)n),或用long数组。
Average:求平均
Average 返回平均值,而且类型会升精度——对 int 序列求平均,得到的是 double,否则会丢掉小数。
namespace CSharpDemo;
record Student(int Id, string Name, int Score);
List<Student> students =
[
new Student(1, "小红", 95),
new Student(2, "小刚", 82),
new Student(3, "小美", 90),
];
double avg = students.Average(s => s.Score);
Console.WriteLine($"平均分 {avg:F1}");
Note对
decimal序列求平均,结果仍是decimal,不会变成double。这是因为decimal与double之间不存在隐式转换,C# 选择保留更精确的类型。
Min 与 Max:最小值和最大值
Min / Max 找出最值,也支持按某个字段比较。C# 还提供了更顺手的 MaxBy / MinBy,直接按某个键取出”那个元素”。
namespace CSharpDemo;
record Product(int Id, string Name, decimal Price);
int[] numbers = [3, 7, 1, 9, 4];
Console.WriteLine($"最小 {numbers.Min()},最大 {numbers.Max()}");
List<Product> products =
[
new Product(1, "笔记本", 9.99m),
new Product(2, "键盘", 199.00m),
];
Product? priciest = products.MaxBy(p => p.Price);
Console.WriteLine($"最贵的是 {priciest?.Name}");
Tip想要”最便宜/最贵那个商品”本身,用 MinBy / MaxBy 比先 Max 再反查更省事。注意结果可能为 null,用
?.兜底。
元素操作:取出或判断单个元素
聚合给出”一个值”,元素操作关心”某个具体元素”或”是否存在”。
First 与 FirstOrDefault
First 取序列第一个元素;FirstOrDefault 在序列为空时返回默认值(引用类型为 null),而不是抛异常。
namespace CSharpDemo;
List<string> names = ["Ann", "Bob", "Cindy"];
string first = names.First();
Console.WriteLine(first);
// 可能为空时,用 FirstOrDefault 更安全
List<string> empty = [];
string? safe = empty.FirstOrDefault();
Console.WriteLine(safe ?? "列表为空");
Note新手常踩的坑:对空集合调
First()会抛 InvalidOperationException。拿不准集合有没有元素,一律用 FirstOrDefault。
Any 与 All:判断而非取出
Any 问”是否有任意一个满足条件”,All 问”是否全部都满足”。两者都返回 bool,而且都会”短路”——一旦能下结论就立刻停。
namespace CSharpDemo;
record Student(int Id, string Name, int Score);
List<Student> students =
[
new Student(1, "小红", 95),
new Student(2, "小刚", 82),
];
bool hasHigh = students.Any(s => s.Score >= 90);
bool allPass = students.All(s => s.Score >= 60);
Console.WriteLine($"有高分同学:{hasHigh}");
Console.WriteLine($"全部及格:{allPass}");
Tip
Any()不带参数可以判断”集合是否非空”,它比Count() > 0高效,因为不必遍历整个集合。
Count 与 Any 的真实取舍
前面提到”判存在用 Any”,这里再展开说清为什么。假设有 100 万个元素的集合,问”有没有大于 1000 的”:
Count(n => n > 1000) > 0:会把 100 万个元素全过一遍,数出到底有几个,再和 0 比较。Any(n => n > 1000):找到第一个满足条件的就立刻返回true,可能只看前几个元素就停。
两者结果一样,但 Any 在”只要知道有没有”的场景下快得多。经验法则:判断存在性用 Any,要具体数字才用 Count。
namespace CSharpDemo;
int[] big = [5, 8, 3, 12, 1];
bool exists = big.Any(n => n > 10); // 命中 12 立刻返回 true
int howMany = big.Count(n => n > 10); // 数出满足条件的个数
Console.WriteLine($"存在大于10的:{exists},个数为:{howMany}");
聚合与元素联用
实际代码里经常把过滤和聚合串起来:先 Where 过滤,再聚合或取元素。
namespace CSharpDemo;
record Student(int Id, string Name, int Score);
List<Student> students =
[
new Student(1, "小红", 95),
new Student(2, "小刚", 82),
new Student(3, "小美", 58),
];
// 不及格人数
int failCount = students.Count(s => s.Score < 60);
// 及格者里的最高分
int? topPass = students.Where(s => s.Score >= 60)
.Select(s => s.Score)
.Max();
Console.WriteLine($"不及格 {failCount} 人,及格最高 {topPass} 分");
Aggregate:万能聚合兜底
五个聚合算子覆盖不了所有”自定义累加”需求。比如”把字符串列表拼成用逗号分隔的一行”,或”算一串数的连乘”。这时用 Aggregate(累加器模式)自己定义怎么合并:
namespace CSharpDemo;
string[] words = ["C#", "很", "好玩"];
string joined = words.Aggregate("", (acc, w) => acc == "" ? w : acc + "、" + w);
Console.WriteLine(joined);
int[] nums = [2, 3, 4];
int product = nums.Aggregate(1, (acc, n) => acc * n);
Console.WriteLine($"连乘结果:{product}");
Aggregate 第一个参数是”初始累加值”,第二个参数是”如何把当前元素合并进累加值”。它把 Count/Sum/Average 的底层思想暴露给你,让你自己拼任意算法。
Tip大多数统计用现成算子就够了,别一上来就写
Aggregate。它更灵活,但也更难一眼读懂。等到现成算子确实表达不了时再用。
空集合上的聚合陷阱
聚合方法对空集合的态度不一样,新手最容易在这里栽跟头。记住下面三条:
Count、Sum对空集返回 0,很安全。Average对空集会抛 InvalidOperationException——“没有元素就没法算平均”。Min/Max对空集同样会抛异常。
namespace CSharpDemo;
int[] empty = [];
Console.WriteLine($"Count={empty.Count()},Sum={empty.Sum()}");
// empty.Average(); // 抛 InvalidOperationException
// empty.Max(); // 抛 InvalidOperationException
Tip拿不准集合是否为空,先用
Any()探一下;或给 Max / Min 设兜底:先DefaultIfEmpty(默认值)铺个底,再取最值就不会炸。
聚合前先投影
聚合方法大多支持”先投影再算”。你不必为了求和先 Select 一次,直接把键传进去即可,代码更短。
namespace CSharpDemo;
record Person(string Name, int Age);
List<Person> people =
[
new Person("小明", 18),
new Person("小红", 22),
];
int maxAge = people.Max(p => p.Age);
double avgAge = people.Average(p => p.Age);
Console.WriteLine($"最大年龄 {maxAge},平均年龄 {avgAge:F1}");
与其它写法对比:foreach 还是 LINQ
手写循环和 LINQ 聚合结果相同,怎么选?看可读性。
| 写法 | 优点 | 缺点 |
|---|---|---|
foreach 累加 | 过程透明,易打断点调试 | 啰嗦,意图藏在步骤里 |
| LINQ 聚合 | 一行说清意图,可链式组合 | 不熟悉时略抽象 |
一句经验:统计意图清楚时用 LINQ,逻辑特别复杂、需要边算边做很多事时用手写循环。 两者没有高下,只有合不合适。
一张速查表
| 方法 | 作用 | 空集行为 |
|---|---|---|
| Count | 计数 | 返回 0 |
| Sum | 求和 | 返回 0 |
| Average | 平均 | 抛异常 |
| Min / Max | 最值 | 抛异常 |
| First | 首个 | 抛异常 |
| FirstOrDefault | 首个或默认 | 返回默认 |
| Any | 是否存在 | 返回 false |
| All | 是否全部 | 返回 true |
| Aggregate | 自定义累加 | 视初始值而定 |
Note留意 Any / All 对空集的”反直觉”结果:空集合上
All(...)返回 true——“全部满足”在没有元素时逻辑上视为成立,这是约定不是 bug。空集合上Any(...)则返回 false,同样符合”没有任何元素满足”的直觉。
综合小例子:一张成绩报表
把本章方法串起来,做一张简易成绩报表:
namespace CSharpDemo;
record Student(int Id, string Name, int Score);
List<Student> students =
[
new Student(1, "小红", 95),
new Student(2, "小刚", 82),
new Student(3, "小美", 58),
new Student(4, "小强", 99),
];
Console.WriteLine($"人数:{students.Count()}");
Console.WriteLine($"平均分:{students.Average(s => s.Score):F1}");
Console.WriteLine($"最高分:{students.Max(s => s.Score)}");
Console.WriteLine($"及格人数:{students.Count(s => s.Score >= 60)}");
Console.WriteLine($"是否全部及格:{students.All(s => s.Score >= 60)}");
常见疑问解答
问:Sum 对空集合返回 0,会不会掩盖”其实是空”的问题?
有可能。如果业务上”空集合”本身代表异常,应先用 Any() 判断,再决定是否 Sum;或改用 DefaultIfEmpty 给出明确的语义默认值。
问:Average 想要保留两位小数怎么办?
Average 返回的是 double/decimal,格式化交给输出:avg:F2。不要为了保留两位小数而手动乘 100 再除 100,容易引入误差。
问:First 和 FirstOrDefault 我该用哪个?
能确定集合非空(比如刚从数据库查出来的必含数据)用 First 更直白;来源不确定(用户输入、外部接口)一律 FirstOrDefault 并用 ?. 处理。拿不准就选 FirstOrDefault,更稳。
最佳实践速记
- 判存在用
Any,要数字才用Count。 - 取首个用
FirstOrDefault防空集合炸。 - 金额用
decimal,大整数求和防int溢出。 - 现成算子够用就别上
Aggregate,可读性优先。 - 空集合上 Average/Min/Max 会抛异常,提前
Any或DefaultIfEmpty兜底。
小结
聚合算子(Count / Sum / Average / Min / Max / Aggregate)把一序列压成一个标量;元素算子(First / FirstOrDefault / Any / All)关心单个元素或存在性。空集合取首个用 FirstOrDefault,判存在用 Any,能少踩很多运行时异常。下一章我们看更复杂的 Join、GroupJoin、Zip 与 SelectMany。