CodeGym /課程 /C# SELF /進階聚合用 Aggregate

進階聚合用 Aggregate

C# SELF
等級 32 , 課堂 4
開放

1. 前言

老派的 LINQ 方法就像學校餐廳的飯菜:你選 "總和""最小值""平均值",然後就走人。但有時候你想來點特別的,這時 Aggregate 就像主廚,照你的菜單下廚。

如果拿 functional programming 來比,Aggregate 就是 Reduce(或 fold):你走過整個集合,一步步把它折成一個值。怎麼折?你自己決定。完全掌控,超有創意空間。

Aggregate 很方便解決的問題:

  • 算複雜的總和:像是所有數字的乘積、只加偶數/奇數、或照特殊規則加總。
  • 自訂邏輯合併字串(例如偶數和奇數 index 用不同分隔符)。
  • 產生字串報表("Markdown 列表"、HTML、任何自訂格式)。
  • 建立有狀態變化的集合(像是根據特殊 key 規則從物件清單做出字典)。
  • 任何標準聚合函式沒辦法搞定的複雜計算。

2. Aggregate 方法的簽名和運作原理

來看看 Microsoft 官方文件裡的 Enumerable.Aggregate

public static TAccumulate Aggregate<TSource, TAccumulate>(
    this IEnumerable<TSource> source, 
    TAccumulate seed, 
    Func<TAccumulate, TSource, TAccumulate> func
)

這裡每個參數是什麼意思:

  • source — 你的原始集合。
  • seed — "初始值"(可以想成起始累加器)。
  • func — 接收兩個參數的函式:累積值(acc)、目前集合元素,回傳新的累積值。

還有個簡單的 overload:

public static TSource Aggregate<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, TSource, TSource> func
)

這種寫法裡,"初始值" 就是集合的第一個元素,然後 func 會從第二個跑到最後一個。

3. Aggregate 最簡單的用法範例

先來個小小的懸念:你知道 Aggregate 其實可以取代 SumProduct 嗎?

int[] numbers = { 2, 3, 4 };

// 算總和
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = 累積, val = 下一個元素

// 算乘積
int product = numbers.Aggregate((acc, val) => acc * val);

Console.WriteLine(sum);     // 9
Console.WriteLine(product); // 24

看起來就像 SumMultiply,但是自己寫的!可以開玩笑說:用 Sum 只能算總和,但 Aggregate 可以算總和、"反總和",甚至 "平方根總和"。

4. 用 Aggregate 合併字串

把所有字串串成一條,用逗號隔開(最後沒有多餘逗號):

string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// 結果: "C#, LINQ, rocks"

如果集合可能是空的,初始值(seed)就超好用:

// 從空字串開始
string report = words.Aggregate(
    "技術: ",
    (acc, word) => acc + word + "; ",
    acc => acc.TrimEnd(' ', ';') // 最後把多餘的 ";" 去掉
);

Console.WriteLine(report); // "技術: C#; LINQ; rocks"

注意第三個參數 — 結果轉換函式(result selector),這是有 seed overload 才有的。就像 "甜點收尾" 一樣。

5. Aggregate 在實際應用程式裡的用法

還記得我們玩了好幾天的那個學習用小程式嗎?假設有個 Student 類別:

public class Student
{
    public string Name { get; set; }
    public int Grade { get; set; }
}

學生清單:

var students = new List<Student>
{
    new Student { Name = "愛麗絲", Grade = 5 },
    new Student { Name = "鮑勃", Grade = 4 },
    new Student { Name = "瓦夏", Grade = 3 },
    new Student { Name = "瑪麗亞", Grade = 5 }
};

任務:得到這樣的字串
"最佳: 愛麗絲, 瑪麗亞"
— 也就是所有 Grade == 5 的人。

新手通常這樣寫:

var best = "";
foreach (var s in students)
{
    if (s.Grade == 5)
        best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("最佳: " + best);

來看看 LINQ 風格的 Aggregate

var bestStr = students
    .Where(s => s.Grade == 5)
    .Select(s => s.Name)
    .Aggregate("最佳: ", (acc, name) => acc + name + ", ")
    .TrimEnd(',', ' ');

Console.WriteLine(bestStr);

優點:程式碼最少,可讀性最高。就算你老闆不懂 LINQ,也能看懂你的意思(不懂也會覺得你很努力)。

6. 更進階的情境

其實 Aggregate 的累加器不一定要是數字或字串,什麼都可以:字典、你自己的 class 或 struct 都行。

例如:從學生清單算出每個分數有幾個學生:

var gradeCounts = students.Aggregate(
    new Dictionary<int, int>(),
    (dict, student) => {
        if (dict.ContainsKey(student.Grade))
            dict[student.Grade]++;
        else
            dict[student.Grade] = 1;
        return dict;
    }
);

// 輸出用:
foreach (var pair in gradeCounts)
{
    Console.WriteLine($"分數 {pair.Key}: {pair.Value} 位學生");
}

這種寫法其實就是手動實作 GroupBy。為什麼不用 GroupBy?有時你需要特殊聚合,標準 LINQ 方法沒有,比如只算不是瓦夏的學生,或是要產生報表給報表用。

7. 視覺化:Aggregate 怎麼運作(流程圖)

假設有個陣列 { 2, 4, 3 },我們要累加總和:


acc: 2(第一個元素)
      |
      v
val: 4
acc = acc + val = 2 + 4 = 6
      |
      v
val: 3
acc = acc + val = 6 + 3 = 9
      |
      v
[全部元素處理完畢]
      |
      v
結果: 9

有 seed overload 的流程也差不多:


seed: 0
      |
      v
val: 2
acc = 0 + 2 = 2
      |
      v
val: 4
acc = 2 + 4 = 6
      |
      v
val: 3
acc = 6 + 3 = 9
      |
      v
結果: 9

Aggregate 跟其他聚合方法比較

方法 標準行為 彈性 空集合時 範例
Sum
加總數字 回傳 0
[1,2,3].Sum() = 6
Count
計算元素數量 回傳 0
[a,b].Count() = 2
Aggregate
任何計算 需要 seed
[1,2,3].Aggregate((a,b)=>a*b)=6
string.Join
合併字串 空集合 = ""
string.Join(", ", arr)

8. 實用建議、常見錯誤和重點提醒

因為 Aggregate 太靈活,用錯很容易出包。最常見的錯誤有:

有時候工程師會忘記 seed(初始值),沒注意到如果集合是空的,沒 seed 的 overload 會丟出例外(InvalidOperationException)。所以空集合時請用有 seed 的 overload:

var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // OK!回傳 0

如果你用 Aggregate 累加字串,很容易多出一個分隔符(像最後多一個 ",")。最好用 .TrimEnd(',', ' ') 把它去掉——或者如果只是單純合併字串,用 string.Join 就好。

累加器如果是可變型別(像 ListDictionary),在 Aggregate 裡常常會用到,但要小心:如果你 in-place 修改它,每一步其實都是同一個物件的 reference。這在做平行運算或你預期會複製時可能出現怪問題。所以純函式風格下,每一步最好回傳新物件,不要改舊的。

寫給別人用的 code,別為了 "炫技" 把 Aggregate 用得太複雜:新手會覺得它比 foreach 或標準聚合難懂。如果只是單純計算,用 SumCountJoin 就好。但如果你要 "照特殊模板組字串"——Aggregate 就是你的好朋友!

9. 跟實際工作、面試和框架的關聯

在業界,Aggregate 常出現在需要特殊計算或資料折疊的地方,比如產生複雜報表、統計、圖形、計算 hash、產生唯一 id,甚至根據集合自訂邏輯組 UI 元件。

面試時,LINQ 幾乎一定會被問到:"怎麼算陣列元素總和?"、"怎麼把字串清單變成一條字串?" 或 "怎麼算唯一元素數量?"——而且常常希望你用 LINQ,甚至 Aggregate。有時還會出創意題:"你能用 LINQ 算出偶數的平方和,然後回傳描述這個過程的字串嗎?"

在很多熱門 .NET 函式庫和框架裡,像 Entity Framework、Dapper、RavenDB,Aggregate 很少直接在資料庫端用,但在程式碼裡做記憶體聚合時超好用。

1
問卷/小測驗
資料分組,等級 32,課堂 4
未開放
資料分組
進階 LINQ:分組與聚合函式
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION