Linq 新功能 (6) CountBy、AggregateBy 與 Index

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