1. Giới thiệu
Các method LINQ cũ xưa — giống như đồ ăn ở căn tin: chọn "tổng", "nhỏ nhất" hay "trung bình" rồi đi tiếp. Nhưng đôi khi muốn gì đó đặc biệt hơn. Đó là lúc Aggregate xuất hiện — như đầu bếp nấu theo công thức của bạn.
Nếu so với functional programming, Aggregate chính là Reduce (hoặc fold): bạn đi qua collection và từng bước gộp nó thành một giá trị duy nhất. Làm thế nào — bạn tự quyết định. Toàn quyền kiểm soát, tha hồ sáng tạo.
Bài toán dùng Aggregate rất tiện:
- Lấy tổng phức tạp: ví dụ, tích tất cả số, hoặc tổng chỉ số chẵn/lẻ, hoặc tổng theo quy tắc đặc biệt.
- Nối chuỗi với logic tự custom (ví dụ, dùng dấu phân cách khác nhau cho index chẵn/lẻ).
- Tạo báo cáo dạng text ("Markdown-list", HTML, bất kỳ format custom nào).
- Xây collection với trạng thái thay đổi (ví dụ, tạo dictionary từ list object theo quy tắc key riêng).
- Bất kỳ phép tính phức tạp nào không nằm trong các hàm tổng hợp chuẩn.
2. Signature và nguyên lý hoạt động của method Aggregate
Cùng xem tài liệu chính thức Microsoft về Enumerable.Aggregate:
public static TAccumulate Aggregate<TSource, TAccumulate>(
this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func
)
Giải thích từng phần:
- source — collection gốc của bạn.
- seed — "giá trị khởi đầu" (có thể coi là accumulator bắt đầu).
- func — function nhận hai tham số: giá trị tích lũy (acc), phần tử hiện tại của collection và trả về giá trị tích lũy mới.
Có cả overload đơn giản hơn:
public static TSource Aggregate<TSource>(
this IEnumerable<TSource> source,
Func<TSource, TSource, TSource> func
)
Ở version này, "giá trị khởi đầu" là phần tử đầu tiên của collection, sau đó func chạy từ phần tử thứ hai đến cuối.
3. Ví dụ đơn giản dùng Aggregate
Bắt đầu bằng một câu đố nhỏ cho sinh viên: có ai biết Aggregate có thể thay cho Sum hoặc Product không?
int[] numbers = { 2, 3, 4 };
// Tính tổng
int sum = numbers.Aggregate((acc, val) => acc + val); // acc = tích lũy, val = phần tử tiếp theo
// Tính tích
int product = numbers.Aggregate((acc, val) => acc * val);
Console.WriteLine(sum); // 9
Console.WriteLine(product); // 24
Nhìn giống Sum và Multiply, nhưng tự code! Có thể đùa: dùng method Sum thì luôn ra tổng, còn Aggregate — vừa tổng, vừa "anti-tổng", thậm chí "tổng căn bậc hai".
4. Dùng Aggregate để nối chuỗi
Nối tất cả chuỗi thành một, cách nhau bởi dấu phẩy (không có phẩy thừa ở cuối):
string[] words = { "C#", "LINQ", "rocks" };
string result = words.Aggregate((acc, word) => acc + ", " + word);
// kết quả: "C#, LINQ, rocks"
Nếu collection có thể rỗng, seed rất tiện:
// Bắt đầu bằng chuỗi rỗng
string report = words.Aggregate(
"Công nghệ: ",
(acc, word) => acc + word + "; ",
acc => acc.TrimEnd(' ', ';') // cuối cùng bỏ ";" thừa
);
Console.WriteLine(report); // "Công nghệ: C#; LINQ; rocks"
Chú ý tham số thứ ba — function chuyển đổi kết quả (result selector), chỉ có ở overload với seed. Kiểu như "tráng miệng" cho món cuối cùng.
5. Aggregate trong ứng dụng thực tế
Nhớ lại app học tập mà tụi mình nghịch mấy hôm nay. Giả sử có class Student:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
List sinh viên:
var students = new List<Student>
{
new Student { Name = "Alisa", Grade = 5 },
new Student { Name = "Bob", Grade = 4 },
new Student { Name = "Vasya", Grade = 3 },
new Student { Name = "Maria", Grade = 5 }
};
Bài toán: lấy chuỗi kiểu
"Xuất sắc: Alisa, Maria"
— tức là tất cả ai có Grade == 5.
Cách newbie hay làm:
var best = "";
foreach (var s in students)
{
if (s.Grade == 5)
best += s.Name + ", ";
}
best = best.TrimEnd(',', ' ');
Console.WriteLine("Xuất sắc: " + best);
Còn đây — kiểu LINQ với Aggregate:
var bestStr = students
.Where(s => s.Grade == 5)
.Select(s => s.Name)
.Aggregate("Xuất sắc: ", (acc, name) => acc + name + ", ")
.TrimEnd(',', ' ');
Console.WriteLine(bestStr);
Đỉnh chưa: code tối giản, đọc phát hiểu luôn. Sếp bạn mà không biết LINQ cũng hiểu ý tưởng (hoặc ít nhất là thấy bạn cố gắng, không hiểu cũng kệ).
6. Tình huống phức tạp hơn
Thực ra, accumulator trong Aggregate không chỉ là số hay chuỗi, mà là bất cứ gì: dictionary, class tự tạo, struct cũng được.
Ví dụ: từ list sinh viên, đếm số sinh viên cho từng điểm:
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;
}
);
// In ra:
foreach (var pair in gradeCounts)
{
Console.WriteLine($"Điểm {pair.Key}: {pair.Value} sinh viên");
}
Cách này thực chất là GroupBy thủ công. Sao không dùng GroupBy? Đôi khi cần tổng hợp đặc biệt, không có sẵn trong LINQ, ví dụ chỉ cộng nếu sinh viên không phải Vasya, hoặc build báo cáo cho báo cáo.
7. Minh họa: Aggregate hoạt động thế nào (sơ đồ khối)
Giả sử có mảng { 2, 4, 3 } và muốn cộng dồn:
acc: 2 (phần tử đầu)
|
v
val: 4
acc = acc + val = 2 + 4 = 6
|
v
val: 3
acc = acc + val = 6 + 3 = 9
|
v
[Tất cả phần tử đã xử lý]
|
v
Kết quả: 9
Tương tự cho overload với seed:
seed: 0
|
v
val: 2
acc = 0 + 2 = 2
|
v
val: 4
acc = 2 + 4 = 6
|
v
val: 3
acc = 6 + 3 = 9
|
v
Kết quả: 9
So sánh Aggregate với các method tổng hợp khác
| Method | Hành vi chuẩn | Linh hoạt | Với collection rỗng | Ví dụ |
|---|---|---|---|---|
|
Cộng số | Thấp | Trả về 0 | |
|
Đếm phần tử | Thấp | Trả về 0 | |
|
Bất kỳ phép tính nào | Cao | Cần seed | |
|
Nối chuỗi | Trung bình | Rỗng = "" | |
8. Tips thực tế, lỗi thường gặp và lưu ý
Vì quá linh hoạt, Aggregate dễ gây bất ngờ nếu dùng sai. Lỗi phổ biến nhất:
Đôi khi coder quên seed (giá trị khởi đầu) và không để ý nếu collection rỗng thì overload không có seed sẽ ném exception (InvalidOperationException). Vì vậy với collection rỗng hãy dùng overload có seed:
var sum = new int[0].Aggregate(0, (acc, n) => acc + n); // OK! Trả về 0
Nếu bạn nối chuỗi bằng Aggregate, rất dễ bị thừa dấu phân cách (ví dụ, "," ở cuối). Nên xóa nó bằng .TrimEnd(',', ' ') — hoặc dùng string.Join nếu chỉ đơn giản nối chuỗi.
Accumulator kiểu mutable (ví dụ, List hoặc Dictionary) thường dùng trong Aggregate, nhưng cẩn thận: nếu bạn thay đổi nó trực tiếp (in-place), thì mọi bước đều trỏ cùng một object. Điều này có thể gây hiệu ứng lạ khi chạy song song hoặc nếu bạn mong đợi copy. Vì vậy, theo functional style, tốt nhất mỗi bước trả về object mới, không sửa cái cũ.
Trong code cho người khác dùng, đừng cố phức tạp hóa Aggregate chỉ để "ngầu": newbie thấy nó khó hơn foreach hay các hàm tổng hợp chuẩn. Nếu bài toán đơn giản — dùng Sum, Count, Join. Nhưng nếu "tạo text theo mẫu đặc biệt" — Aggregate là bạn thân!
9. Liên hệ thực tế, phỏng vấn và framework
Trong ngành, method Aggregate hay gặp ở chỗ cần tổng hợp hoặc gộp dữ liệu kiểu lạ, ví dụ tạo báo cáo phức tạp, thống kê, graph, tính hash, sinh id unique hoặc build UI component từ collection theo logic riêng.
Khi phỏng vấn, câu hỏi về LINQ gần như luôn có kiểu: "Làm sao lấy tổng các phần tử của mảng?", "Làm sao biến list chuỗi thành một chuỗi?", hoặc "Làm sao đếm số phần tử unique?" — và thường mong chờ giải pháp dùng LINQ, trong đó có Aggregate. Có thể hỏi sáng tạo: "Bạn có thể dùng LINQ tính tổng bình phương số chẵn, rồi trả về chuỗi mô tả quá trình đó không?"
Trong nhiều thư viện .NET nổi tiếng và framework như Entity Framework, Dapper, RavenDB, method Aggregate hiếm khi dùng trực tiếp ở phía DB, nhưng ở code thì dùng nhiều để tổng hợp trong memory.
GO TO FULL VERSION