LINQ 新时代:CountBy、AggregateBy 深度解析(含对比 GroupBy)

洛溪
洛溪
发布时间:2026-08-24 | 39人浏览过
原创

前言

在很多 LINQ 代码里,开发者明明只想拿到“每组几个”或“每组累计多少”,最后还是得先写一段 GroupBy。.NET 9(C# 13) 新增的 CountBy 和 AggregateBy,就是专门为这类结果型统计准备的;本文会结合签名、示例、性能特点和与 GroupBy 的边界,帮你判断什么时候该换用新写法。

在 .NET 8 以前,开发者做分组计数或分组聚合,通常都要从 GroupBy 起步。问题不在于它不能用,而在于很多“只想要结果”的场景,其实并不需要完整的分组对象,写起来也显得偏重。

到了 .NET 9(C# 13),LINQ 正式加入 CountBy 和 AggregateBy 两个扩展方法。下面就围绕“什么时候该用它们、和 GroupBy 差在哪、真实代码里该怎么选”这几个问题展开,顺手把版本要求、性能判断和常见边界条件一起理清。

为什么 .NET 9 要补上 CountBy 和 AggregateBy

传统写法里,简单的分类统计通常是这样写:

var query = list.GroupBy(x => x.Category)
                .Select(g => new { Category = g.Key, Count = g.Count() });

这类代码当然可读,但对“只需要键和统计结果”的任务来说,还是有两个明显问题:

  • 代码链路偏长,表达目标不够直接;
  • 为了最终的计数或聚合,先创建中间 IGrouping 对象,开销并不总是必要。

因此,.NET 9 增加了两个更聚焦的 API:

  • CountBy:按键快速计数;
  • AggregateBy:按键快速聚合。

它们都直接返回键值对结果,适合频率统计、求和、最大值、字符串拼接这类明确的结果型场景。

CountBy 和 AggregateBy 到底解决什么问题

CountBy:把分组计数压缩成一步

CountBy 用于按键分组,并统计每个键出现的次数。它本质上相当于 GroupBy(...).Select(...g.Count()) 的直接版。

方法签名如下:

public static IEnumerable<KeyValuePair<TKey, int>> CountBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey>? comparer = null)
  • source:输入集合,类型为 IEnumerable<TSource>;
  • keySelector:从元素中提取分组键;
  • comparer:可选键比较器,未提供时使用 EqualityComparer<TKey>.Default;
  • 返回值:IEnumerable<KeyValuePair<TKey, int>>。

AggregateBy:保留按键分组,但把聚合逻辑交给你

AggregateBy 用于按键分组,并对每个分组执行自定义聚合。它适合的不只是计数,还包括求和、最大值、最小值、字符串拼接,甚至构建复杂对象。

方法签名如下:

public static IEnumerable<KeyValuePair<TKey, TResult>> AggregateBy<TSource, TKey, TAccumulate, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func,
    Func<TKey, TAccumulate, TResult> resultSelector,
    IEqualityComparer<TKey>? comparer = null)

参数含义可以直接对应到普通 Aggregate 的思路:

  • source:输入集合;
  • keySelector:分组键选择器;
  • seed:每个分组的初始累积值;
  • func:聚合函数,定义如何累加当前元素;
  • resultSelector:将键和最终累积值转换为结果;
  • comparer:可选键比较器。

和 GroupBy 相比,这两个 API 的共同点是:目标更明确,且直接生成键值对结果。

CountBy 怎么用,适合哪些计数场景

基础计数:水果出现次数

var fruits = new[] { "apple", "banana", "apple", "orange", "banana", "apple" };

var result = fruits.CountBy(f => f);

foreach (var kv in result)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

输出:

apple: 3
banana: 2
orange: 1

它等价于:

fruits.GroupBy(f => f).Select(g => new KeyValuePair<string,int>(g.Key, g.Count()));

如果你的目标只是“按键统计次数”,CountBy 的表达就更直接。

自定义键:按奇偶分组计数

var numbers = new[] { 1, 2, 3, 4, 5, 6 };
var result = numbers.CountBy(n => n % 2 == 0 ? "Even" : "Odd");

输出:

Odd: 3
Even: 3

这里的重点不是元素本身,而是 keySelector 可以把原始数据投影成新的分类标准。

使用比较器:忽略大小写

var names = new[] { "apple", "Apple", "APPLE", "banana" };
var counts = names.CountBy(name => name, StringComparer.OrdinalIgnoreCase);

foreach (var kvp in counts)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// 输出:
// apple: 3
// banana: 1

这里通过 StringComparer.OrdinalIgnoreCase 把大小写不同的字符串视为同一个键,这也是 CountBy 在文本统计里很常见的用法。

AggregateBy 的核心价值:不止求和,还能构建结果

按类别求和

var orders = new[]
{
    new { Category = "Book", Price = 10 },
    new { Category = "Book", Price = 20 },
    new { Category = "Food", Price = 5 },
    new { Category = "Food", Price = 7 },
};

var result = orders.AggregateBy(
    keySelector: o => o.Category,
    seed: 0m,
    accumulator: (sum, item) => sum + item.Price
);

foreach (var kv in result)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

输出:

Book: 30
Food: 12

等价写法是:

orders.GroupBy(o => o.Category)
      .Select(g => new KeyValuePair<string,decimal>(g.Key, g.Sum(x => x.Price)));

在这种场景下,AggregateBy 就是把“按键分组 + 求和”合成一步。

字符串拼接

var names = new[]
{
    new { Group = "A", Name = "Alice" },
    new { Group = "A", Name = "Alex" },
    new { Group = "B", Name = "Bob" },
};

var result = names.AggregateBy(
    keySelector: n => n.Group,
    seed: "",
    accumulator: (s, n) => s == "" ? n.Name : $"{s}, {n.Name}"
);

foreach (var kv in result)
{
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

输出:

A: Alice, Alex
B: Bob

这说明 AggregateBy 的适用面远不止数值聚合,任何“按组累计生成结果”的逻辑都可以放进去。

自定义结果:统计分组最大值

var items = new[]
{
    new { Category = "A", Value = 10 },
    new { Category = "B", Value = 20 },
    new { Category = "A", Value = 15 }
};

var maxValues = items.AggregateBy(
    item => item.Category,          // 按类别分组
    seed: int.MinValue,             // 初始值为最小整数
    (max, item) => Math.Max(max, item.Value), // 取最大值
    (key, max) => max);             // 返回最大值

foreach (var kvp in maxValues)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// 输出:
// A: 15
// B: 20

这类需求如果用 GroupBy 写也能完成,但用 AggregateBy 更能一眼看出“我要按键累计出一个最大值”。

和 GroupBy 怎么选:先看你需不需要分组本身

CountBy vs AggregateBy

特性CountByAggregateBy
功能仅计数自定义任何聚合操作
返回类型IEnumerable<KeyValuePair<TKey,int>>IEnumerable<KeyValuePair<TKey,TAccumulate>>
复杂度更简洁更灵活,但需提供 seed 和 accumulator
适用场景频率统计求和、平均值、拼接字符串、自定义聚合等

什么情况下 GroupBy 仍然更合适

CountBy 和 AggregateBy 并不是要取代所有 GroupBy 用法。判断标准很简单:

对比 CountBy、AggregateBy 与 GroupBy 在结果形态、使用门槛和适用任务上的差异
三种分组方案怎么选把三种写法放在同一张图里,更容易判断该选“直接出结果”还是“保留完整分组”。
  • 如果你只需要“键 + 结果”,优先考虑 CountBy 或 AggregateBy;
  • 如果你需要访问每个分组里的全部元素,继续使用 GroupBy。

也就是说,它们替代的是结果型统计,不是完整的分组建模。

性能和实战价值体现在哪

为什么通常比 GroupBy 更轻

与 GroupBy 相比,CountBy / AggregateBy 的性能优势主要来自三点:

展示 CountBy 与 AggregateBy 在日志统计中的数量结果和累计大小结果
日志统计:数量与大小两类结果同一份日志数据,用 CountBy 和 AggregateBy。
  • 只执行一次遍历;
  • 内部使用哈希表累积结果;
  • 避免创建中间 IGrouping 对象,减少内存分配。

因此在大数据集、频繁统计或在线处理场景中,这两个方法通常更值得优先考虑。

实战示例:日志数量与大小统计

record Log(string Level, int Size);

var logs = new[]
{
    new Log("Info", 10),
    new Log("Error", 5),
    new Log("Info", 20),
    new Log("Error", 15),
    new Log("Warning", 7)
};

// 统计不同 Level 的日志数量
var count = logs.CountBy(l => l.Level);

// 统计不同 Level 的总 Size
var size = logs.AggregateBy(l => l.Level, 0, (sum, log) => sum + log.Size);

输出:

---Count---
Info: 2
Error: 2
Warning: 1

---Size---
Info: 30
Error: 20
Warning: 7

这正好对应两个典型需求:一个是“每类有多少条”,另一个是“每类累计多少值”。前者天然适合 CountBy,后者天然适合 AggregateBy。

统计单词频率并排序

var text = "the quick brown fox jumps over the lazy dog the quick fox";
var words = text.Split(' ');
var wordCounts = words.CountBy(word => word, StringComparer.OrdinalIgnoreCase)
                      .OrderByDescending(kvp => kvp.Value);

foreach (var kvp in wordCounts)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// 输出:
// the: 3
// quick: 2
// fox: 2
// brown: 1
// jumps: 1
// over: 1
// lazy: 1
// dog: 1

这类文本分析任务里,CountBy 和比较器、排序链式组合起来会非常顺手。

复杂聚合:按客户构建汇总对象

var orders = new[]
{
    new { Customer = "Alice", Amount = 100, Item = "Laptop" },
    new { Customer = "Bob", Amount = 50, Item = "Mouse" },
    new { Customer = "Alice", Amount = 200, Item = "Phone" }
};

var summaries = orders.AggregateBy(
    order => order.Customer,
    seed: new { Total = 0, Items = new List<string>() },
    (acc, order) => new { Total = acc.Total + order.Amount, Items = acc.Items.Append(order.Item).ToList() },
    (key, acc) => new { Customer = key, acc.Total, acc.Items });

foreach (var summary in summaries)
{
    Console.WriteLine($"{summary.Customer}: Total = {summary.Total}, Items = {string.Join(", ", summary.Items)}");
}
// 输出:
// Alice: Total = 300, Items = Laptop, Phone
// Bob: Total = 50, Items = Mouse

如果你需要的不是单个数字,而是一个按键整理后的结果对象,AggregateBy 的价值就会更明显。

上手前要注意的版本和边界条件

版本要求

  • CountBy 和 AggregateBy 是 C# 13(.NET 9) 的新特性,目标框架需要是 .NET 9.0 或更高;
  • 较低版本仍可用 GroupBy + Count 或 Aggregate 替代,只是写法和性能都不如新 API 直接。

比较器、延迟执行与异常处理

  • 默认使用 EqualityComparer<TKey>.Default;
  • 对自定义类型或特殊相等规则,需提供 IEqualityComparer<TKey>;
  • 返回的 IEnumerable<KeyValuePair<TKey, TValue>> 是延迟求值的,如需持久化可调用 ToList() 或 ToDictionary();
  • 如果 source 为 null,会抛出 ArgumentNullException;
  • 如果 keySelector 或 func 内部抛出异常,需要在调用方处理。

结论:把 GroupBy 留给分组本身,把新 API 留给结果型统计

方法用途替代旧写法场景示例
CountBy按键分组计数GroupBy().Select(g => g.Count())商品销量、用户角色人数
AggregateBy按键分组并执行自定义聚合GroupBy().Aggregate() 或 GroupBy().Sum()日志大小总和、字符串拼接

如果你只关心每个键对应的最终结果,CountBy 和 AggregateBy 会比 GroupBy 更贴切。只有在你确实需要分组中的完整元素集合时,GroupBy 才是更自然的选择。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表