簡介
在 .NET 8 之前,LINQ 沒有內置 CountBy 和 AggregateBy 方法,但在 .NET 9(C# 13) 中,LINQ 正式引入了這兩個新擴展方法,極大簡化了數據分組和聚合的寫法。
背景
傳統的分組統計一般使用 GroupBy:
var query = list.GroupBy(x => x.Category)
.Select(g => new { Category = g.Key, Count = g.Count() });
但 GroupBy:
- 代碼冗長
- 對簡單的計數/聚合任務過於複雜
為此,.NET 9 引入:
CountBy→ 按鍵快速計數AggregateBy→ 按鍵快速聚合
什麼是 CountBy 和 AggregateBy?
CountBy:用於按鍵(key)對集合進行分組並統計每個鍵的出現次數,返回一個鍵值對集合,其中鍵是分組依據,值是該鍵的計數。AggregateBy:用於按鍵對集合進行分組並對每個分組應用自定義聚合函數,返回一個鍵值對集合,其中鍵是分組依據,值是聚合結果。
這兩個方法類似於 GroupBy 後接 Count 或 Aggregate,但它們更高效、更簡潔,減少了中間分組對象的創建,優化了性能。
關鍵特點:
- 高效性:直接生成鍵值對結果,避免
GroupBy創建中間IGrouping對象的開銷。 - 簡潔性:將分組和統計/聚合合併為一步操作。
- 靈活性:支持自定義鍵選擇器和聚合邏輯。
- 返回類型:返回
IEnumerable<KeyValuePair<TKey, TValue>>,便於進一步處理。
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>>,每個鍵值對包含鍵和該鍵的計數。
基礎用法
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()));
fruit => fruit按水果名稱分組並計數。- 結果是一個鍵值對集合,鍵是水果名稱,值是出現次數。
自定義鍵
var numbers = new[] { 1, 2, 3, 4, 5, 6 };
var result = numbers.CountBy(n => n % 2 == 0 ? "Even" : "Odd");
輸出:
Odd: 3
Even: 3
使用比較器:忽略大小寫
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忽略鍵的大小寫。
AggregateBy
作用:按鍵分組並在分組中執行自定義聚合邏輯(不僅僅是計數)。
類似 GroupBy(...).Aggregate(...) 的簡化版。
方法簽名
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)
參數説明:
source:輸入的集合(實現IEnumerable<TSource>)。keySelector:從每個元素提取分組的鍵。seed:聚合的初始值(每個分組從此值開始)。func:聚合函數,定義如何將元素累加到當前累積值。resultSelector:結果選擇器,將鍵和最終累積值轉換為結果。comparer:可選的鍵比較器。- 返回:
IEnumerable<KeyValuePair<TKey, TResult>>,每個鍵值對包含鍵和聚合結果。
求和
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)));
拼接字符串
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
使用自定義結果:統計最大值
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
CountBy vs AggregateBy
| 特性 | CountBy | AggregateBy |
|---|---|---|
| 功能 | 僅計數 | 自定義任何聚合操作 |
| 返回類型 | IEnumerable<KeyValuePair<TKey,int>> |
IEnumerable<KeyValuePair<TKey,TAccumulate>> |
| 複雜度 | 更簡潔 | 更靈活,但需提供 seed 和 accumulator |
| 適用場景 | 頻率統計 | 求和、平均值、拼接字符串、自定義聚合等 |
性能優勢
與 GroupBy 相比:
CountBy / AggregateBy只執行一次遍歷- 內部使用 哈希表累積,減少對象創建
- 對大數據集統計效率更高
實戰示例:日誌統計
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
統計單詞頻率並排序
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
複雜聚合(構建對象)
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
適用場景
CountBy
- 統計頻率:統計集合中元素的出現次數(如單詞計數、類別統計)。
- 分組分析:快速生成鍵值對形式的計數結果,適合數據分析。
- 替代
GroupBy + Count:在需要簡單計數時,CountBy更高效。
AggregateBy
- 分組聚合:對分組數據執行求和、最大值、最小值、平均值等操作。
- 複雜聚合:如連接字符串、構建複雜對象等。
- 高性能場景:需要高效處理大集合,避免中間分組對象的開銷。
總結
| 方法 | 用途 | 替代舊寫法 | 場景示例 |
|---|---|---|---|
CountBy |
按鍵分組計數 | GroupBy().Select(g => g.Count()) |
商品銷量、用户角色人數 |
AggregateBy |
按鍵分組並執行自定義聚合 | GroupBy().Aggregate() 或 GroupBy().Sum() |
日誌大小總和、字符串拼接 |
注意事項:
版本要求:
CountBy和AggregateBy是C# 13(.NET 9)的新特性,需目標框架為.NET 9.0或更高。- 在較低版本中,可使用
GroupBy + Count或Aggregate替代,但性能稍差。
性能優勢:
- 兩者直接生成鍵值對,避免
GroupBy的中間IGrouping對象,減少內存分配。 - 對於大集合或高頻操作,性能提升顯著。
鍵比較器:
- 默認使用
EqualityComparer<TKey>.Default,適合大多數場景。 - 對於自定義類型或特殊相等性邏輯,需提供
IEqualityComparer<TKey>。
不可變性:
- 返回的
IEnumerable<KeyValuePair<TKey, TValue>>是延遲求值的。 - 如果需要持久化結果,調用
ToList()或ToDictionary()。
錯誤處理:
- 如果
source為null,會拋出ArgumentNullException。 - 如果
keySelector或func拋出異常,需在調用代碼中處理。
與 GroupBy 的選擇:
- 如果需要訪問分組中的所有元素,使用
GroupBy。 - 如果只需要鍵和聚合結果(如計數、總和),優先使用
CountBy或AggregateBy。