跳過 .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 沒有增加新的方法,但是在效能上的改善是有的,而且某些函式的效能增進很驚人。