首页 / C# 入门教程 / 聚合与元素操作

C# 入门教程

聚合与元素操作

本教程共 100 篇 · 第 71 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程LINQ聚合操作集合

71. 聚合与元素操作

本节目标:学完你能用 LINQ 一键统计数量、求和、求平均,并安全地取出集合中第一个元素或判断是否存在满足条件的元素。

前面几章你学会了筛选、投影、排序、分组。这一章讲”收口”——把一串数据压成一个结果。这类方法叫聚合(aggregation)与元素操作(element operation),是写报表、算总分、判空最常用的一把刀。

Note

聚合与元素方法都是立即执行的:调用当下就算出结果,而不是像 Where/Select 那样延迟到遍历时才算。所以每写一次就真的跑一遍。

为什么需要聚合算子

你当然可以手写一个 foreach 去累加、去数个数。但 LINQ 的聚合算子把”常见统计意图”封装成一眼能读懂的名字:看见 Sum 就知道在求和,看见 Average 就知道在求平均。这正是 LINQ 的设计初衷——让”对数据的描述”胜过”对步骤的指挥”。

打个比方:聚合算子就像 Excel 里的 SUMAVERAGECOUNT 函数。你不用告诉 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 亿)时,结果会”绕回”成负数而不报错。若数据量或单值可能很大,先投影成 longnumbers.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。这是因为 decimaldouble 之间不存在隐式转换,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。它更灵活,但也更难一眼读懂。等到现成算子确实表达不了时再用。

空集合上的聚合陷阱

聚合方法对空集合的态度不一样,新手最容易在这里栽跟头。记住下面三条:

  • CountSum 对空集返回 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 会抛异常,提前 AnyDefaultIfEmpty 兜底。

小结

聚合算子(Count / Sum / Average / Min / Max / Aggregate)把一序列压成一个标量;元素算子(First / FirstOrDefault / Any / All)关心单个元素或存在性。空集合取首个用 FirstOrDefault,判存在用 Any,能少踩很多运行时异常。下一章我们看更复杂的 Join、GroupJoin、Zip 与 SelectMany。