CodeGym /课程 /C# SELF /进阶聚合,玩转 Aggregate

进阶聚合,玩转 Aggregate

C# SELF
第 32 级 , 课程 4
可用

1. 入门

LINQ的老方法就像食堂的饭:选个 "求和""最小值""平均值",吃完就走。但有时候你想来点特别的,这时候 Aggregate 就像大厨,按你自己的配方做菜。

如果和函数式编程对比,Aggregate 就是 Reduce(或者 fold):你遍历集合,一步步把它“折叠”成一个值。怎么折叠?你说了算。完全自由,随便发挥。

Aggregate 很适合解决这些问题:

  • 搞复杂的求和:比如所有数字的乘积,只加偶数/奇数,或者按特殊规则加。
  • 字符串拼接,自己定逻辑(比如偶数和奇数下标用不同分隔符)。
  • 生成字符串报告(比如Markdown列表、HTML、各种自定义格式)。
  • 构建带状态变化的集合(比如按特殊规则把对象列表变成字典)。
  • 任何标准聚合函数搞不定的复杂统计。

2. Aggregate 方法的签名和原理

来看看微软官方文档里 Enumerable.Aggregate 的签名:

public static TAccumulate Aggregate<TSource, TAccumulate>(
    this IEnumerable<TSource> source, 
    TAccumulate seed, 
    Func<TAccumulate, TSource, TAccumulate> func
)

这里每个参数是啥意思:

  • source —— 你的原始集合。
  • seed —— “初始值”(可以理解为起始累加器)。
  • func —— 接收两个参数的函数:累积值(acc)、当前元素,返回新的累积值。

还有个简单点的重载:

public static TSource Aggregate<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, TSource, TSource> func
)

这个版本里,“初始值”就是集合的第一个元素,然后 func 从第二个元素开始一路算到最后。

3. Aggregate 的最基础用法

先来个小悬念:你知道 Aggregate 其实能替代 SumProduct 吗?

int[] numbers = { 2, 3, 4 };

// 求和
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = 累加,val = 下一个元素

// 求乘积
int product = numbers.Aggregate((acc, val) => acc * val);

Console.WriteLine(sum);     // 9
Console.WriteLine(product); // 24

看起来像 SumMultiply,但其实是自己实现的!可以开个玩笑:Sum 只能算和,Aggregate 能算和、能算“反和”,还能算“平方根之和”。

4. 用 Aggregate 拼接字符串

把所有字符串拼成一个,用逗号隔开(最后没多余逗号):

string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// 结果: "C#, LINQ, rocks"

如果集合可能是空的,初始值(seed)就很有用了:

// 从空字符串开始
string report = words.Aggregate(
    "技术: ",
    (acc, word) => acc + word + "; ",
    acc => acc.TrimEnd(' ', ';') // 最后去掉多余的 ";"
);

Console.WriteLine(report); // "技术: C#; LINQ; rocks"

注意第三个参数——结果转换函数(result selector),只有带seed的重载才有。就像“最后来个甜点”一样处理最终结果。

5. Aggregate 在实际项目里的用法

还记得我们天天折腾的那个学习小项目吗?假设有个 Student 类:

public class Student
{
    public string Name { get; set; }
    public int Grade { get; set; }
}

学生列表:

var students = new List<Student>
{
    new Student { Name = "阿丽萨", Grade = 5 },
    new Student { Name = "鲍勃", Grade = 4 },
    new Student { Name = "瓦夏", Grade = 3 },
    new Student { Name = "玛丽亚", Grade = 5 }
};

目标:得到类似
"最佳: 阿丽萨, 玛丽亚"
—— 也就是所有 Grade == 5 的学生。

新手一般这么写:

var best = "";
foreach (var s in students)
{
    if (s.Grade == 5)
        best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("最佳: " + best);

来点LINQ风格,用 Aggregate

var bestStr = students
    .Where(s => s.Grade == 5)
    .Select(s => s.Name)
    .Aggregate("最佳: ", (acc, name) => acc + name + ", ")
    .TrimEnd(',', ' ');

Console.WriteLine(bestStr);

优点:代码最少,阅读性最好。就算你老板不懂LINQ,也能看懂你想干嘛(或者至少能看出你很努力)。

6. 更复杂的场景

其实 Aggregate 的累加器不仅能是数字或字符串,啥都行:字典、自定义类、结构体都可以。

比如:统计每个分数有多少学生:

var gradeCounts = students.Aggregate(
    new Dictionary<int, int>(),
    (dict, student) => {
        if (dict.ContainsKey(student.Grade))
            dict[student.Grade]++;
        else
            dict[student.Grade] = 1;
        return dict;
    }
);

// 输出:
foreach (var pair in gradeCounts)
{
    Console.WriteLine($"分数 {pair.Key}: {pair.Value} 个学生");
}

其实这个做法本质上就是手动实现 GroupBy。为啥不用 GroupBy?有时候你需要特殊的聚合,比如只统计不是瓦夏的学生,或者要生成特殊的报告。

7. 可视化:Aggregate 怎么工作的(流程图)

假设有个数组 { 2, 4, 3 },我们要累加求和:


acc: 2 (第一个元素)
      |
      v
val: 4
acc = acc + val = 2 + 4 = 6
      |
      v
val: 3
acc = acc + val = 6 + 3 = 9
      |
      v
[所有元素处理完]
      |
      v
结果: 9

带seed的重载流程类似:


seed: 0
      |
      v
val: 2
acc = 0 + 2 = 2
      |
      v
val: 4
acc = 2 + 4 = 6
      |
      v
val: 3
acc = 6 + 3 = 9
      |
      v
结果: 9

Aggregate 和其他聚合方法对比

方法 标准行为 灵活性 空集合时 示例
Sum
数字求和 返回0
[1,2,3].Sum() = 6
Count
计数元素 返回0
[a,b].Count() = 2
Aggregate
任意统计 需要seed
[1,2,3].Aggregate((a,b)=>a*b)=6
string.Join
拼接字符串 空集合=""
string.Join(", ", arr)

8. 实用建议、常见坑和注意事项

因为 Aggregate 太灵活了,用错了容易踩坑。最常见的错误有:

有时候程序员忘了seed(初始值),没注意如果集合是空的,不带seed的重载会直接抛异常(InvalidOperationException)。所以空集合时一定要用带seed的重载:

var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // 没问题!返回0

如果你用 Aggregate 拼接字符串,很容易最后多一个分隔符(比如最后多了个逗号)。最好用 .TrimEnd(',', ' ') 去掉,或者如果只是简单拼接字符串,直接用 string.Join

累加器如果是可变类型(比如 ListDictionary),在 Aggregate 里经常用,但要注意:如果你是原地修改(in-place),每一步其实都是同一个对象的引用。并发操作或者你以为会复制时,可能会出奇怪的bug。所以纯函数式风格下,最好每步都返回新对象,不要改老的。

写给别人用的代码,别为了“炫技”乱用 Aggregate:新手觉得它比 foreach 或普通聚合难懂。如果只是普通统计——用 SumCountJoin。但要“按特殊模板拼文本”—— Aggregate 就是你的好帮手!

9. 和实际工作、面试、框架的关系

在业界,Aggregate 经常出现在需要特殊统计或数据折叠的地方,比如生成复杂报告、统计、图结构、算hash、生成唯一id,甚至按特殊逻辑从集合构建UI组件。

面试时LINQ相关问题几乎必考,比如:“怎么求数组元素的和?”、“怎么把字符串列表变成一个字符串?”、“怎么统计唯一元素个数?”——很多时候都希望你用LINQ,甚至 Aggregate。有时还会出点花活:“你能用LINQ算出所有偶数的平方和,然后返回描述这个过程的字符串吗?”

在很多流行的.NET库和框架里,比如Entity Framework、Dapper、RavenDB,Aggregate 很少直接在数据库端用,但在代码里做内存聚合时非常好用。

1
调查/小测验
数据分组第 32 级,课程 4
不可用
数据分组
进阶 LINQ:分组和聚合函数
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION