跳过 .NET 7 与 .NET 8,因为这两版没有新增方法【注1】,因此我们直接来到 .NET9
本集提要
- 框架 : .NET 9
- 功能 : CountBy、AggregateBy 与 Index
CountBy
CountBy 用于取代过去 GroupBy 后分别计数 (Count) 的功能,可以缩短程式码的长度以及更明白的表示意图。
举个例子比较容易体会,例如我们有以下的序列,目的是要取得『各个城市的人口数』:
static IEnumerable<Person> GetPersons()
{
yield return new Person { Name = "Bill", City = "Seattle", Age = 18 };
yield return new Person { Name = "Mark", City = "Taipei", Age = 21 };
yield return new Person { Name = "Steve", City = "New York", Age = 32 };
yield return new Person { Name = "James", City = "Taipei", Age = 16 };
yield return new Person { Name = "John", City = "Seattle", Age = 52 };
yield return new Person { Name = "Tom", City = "Taipei", Age = 37 };
yield return new Person { Name = "David", City = "New York", Age = 26 };
yield return new Person { Name = "Peter", City = "Seattle", Age = 23 };
yield return new Person { Name = "Paul", City = "Taipei", Age = 45 };
yield return new Person { Name = "Mary", City = "New York", Age = 38 };
}
过去我们可能会这么写:
var oldCountByCity = persons.GroupBy(p => p.City)
.Select(g => new { City = g.Key, Count = g.Count() });
有了 CountBy 可就方便多了,程式码看起来很直觉:
var newCountByCity = persons.CountBy(p => p.City);
但是有一点要注意的是这两个的回传值型别其实不一样,当我们用 GroupBy → Select 的时候,由于使用了匿名型别所以回传值是 IEnumerable<匿名型别> (在 VS 上通常标示成 IEnumerable<'a>);但我们用 CountBy 的时候,回传值是 IEnumerable<KeyValuePair<string, int>>。
AggregateBy
AggregateBy 的情况和 CountBy 差不多,也是因应简化 GroupBy 而生。
原始资料同上面的 CountBy 範例,现在我们想要取得的是各城市的年龄总和,一般我们会这么写:
var oldAggregateByCity = persons.GroupBy(p => p.City)
.Select(g => new { City = g.Key, Age = g.Sum(p => p.Age) });
为了对照上的原因,把 Sum 改成 Aggregate:
oldAggregateByCity = persons.GroupBy(p => p.City)
.Select(g => new { City = g.Key, Age = g.Aggregate(0, (acc, p) => acc + p.Age) });
写起来还挺麻烦一把,但有了 AggregateBy,直接融合 GroupBy ,就会简洁许多:
var newAggregateByCity = persons.AggregateBy(p => p.City, 0, (acc, p) => acc + p.Age);
AggregateBy 回传值的部分和 CountBy 一样,是 IEnumerable<KeyValuePair<string, int>>。
Index
新增的 Index 方法主要是用来取代 Select 的其中一个多载 – IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> selector),这个方法的用途很简单,就是『利用迭代製造出整数索引值』。
以前会这么写 (为了刻意和新的 Index 回传型别看起来一样,所以刻意在 ValueTuple 的栏位命名上动点手脚):
IEnumerable<(int Index, Person Item)> oldIndex = persons.Select((p, index) => (Index: index, Item: p));
有了 Index 后,就没这么麻烦了:
IEnumerable<(int Index, Person Item)> newIndex = persons.Index();
Benchmark
CountBy 和 AggregateBy 除了让程式码更好写且看起来更简洁以外,在效能上也有不错的表现,在效能测试程式码中为求公平都换成使用 KeyValuePair<string, int> 作为回传型别 :
// * Summary *
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4890/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-1265U, 1 CPU, 12 logical and 10 physical cores
.NET SDK 9.0.200-preview.0.25057.12
[Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------------------- |----------:|----------:|----------:|-------:|----------:|
| CountBy | 1.975 ns | 0.0242 ns | 0.0226 ns | - | - |
| GroupByAndCount | 20.195 ns | 0.4122 ns | 0.4411 ns | 0.0217 | 136 B |
| AggregateBy | 2.306 ns | 0.0257 ns | 0.0228 ns | - | - |
| GroupByAndSum | 20.600 ns | 0.2657 ns | 0.2486 ns | 0.0217 | 136 B |
| GroupByAndAggregate | 20.782 ns | 0.1831 ns | 0.1623 ns | 0.0217 | 136 B |
本篇範例连结,效能测试连结。
注1:虽说 .NET 7 没有增加新的方法,但是在效能上的改善是有的,而且某些函式的效能增进很惊人。