博客 / 詳情

返回

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

簡介

.NET 8 之前,LINQ 沒有內置 CountByAggregateBy 方法,但在 .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 後接 CountAggregate,但它們更高效、更簡潔,減少了中間分組對象的創建,優化了性能。

關鍵特點:

  • 高效性:直接生成鍵值對結果,避免 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() 日誌大小總和、字符串拼接

注意事項:

版本要求:

  • CountByAggregateByC# 13(.NET 9)的新特性,需目標框架為 .NET 9.0 或更高。
  • 在較低版本中,可使用 GroupBy + CountAggregate 替代,但性能稍差。

性能優勢:

  • 兩者直接生成鍵值對,避免 GroupBy 的中間 IGrouping 對象,減少內存分配。
  • 對於大集合或高頻操作,性能提升顯著。

鍵比較器:

  • 默認使用 EqualityComparer<TKey>.Default,適合大多數場景。
  • 對於自定義類型或特殊相等性邏輯,需提供 IEqualityComparer<TKey>

不可變性:

  • 返回的 IEnumerable<KeyValuePair<TKey, TValue>> 是延遲求值的。
  • 如果需要持久化結果,調用 ToList()ToDictionary()

錯誤處理:

  • 如果 sourcenull,會拋出 ArgumentNullException
  • 如果 keySelectorfunc 拋出異常,需在調用代碼中處理。

與 GroupBy 的選擇:

  • 如果需要訪問分組中的所有元素,使用 GroupBy
  • 如果只需要鍵和聚合結果(如計數、總和),優先使用 CountByAggregateBy
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.