1. Giới thiệu
Nhóm dữ liệu nghĩa là chia các phần tử trong collection thành các "giỏ" dựa trên một đặc điểm chung nào đó. Hãy tưởng tượng bạn đang phân loại táo theo màu: tất cả táo đỏ cho vào một giỏ, táo xanh vào giỏ khác, táo vàng thì cho vào chỗ khác nữa. Trong lập trình, cái này gọi là grouping.
Tại sao cần grouping?
Trong thực tế, rất hay cần group người theo thành phố, sản phẩm theo loại, giao dịch theo ngày, v.v. Như vậy sẽ dễ làm phân tích: ví dụ, biết được mỗi lớp có bao nhiêu học sinh, hoặc thành phố nào nhiều sinh viên nhất.
LINQ có method riêng cho việc này: GroupBy. Ngoài ra còn có operator tương tự trong query syntax — là group by. Cùng xem ví dụ trên app quản lý sinh viên và lớp học của tụi mình nhé.
2. GroupBy trong method syntax
Signature cơ bản và kết quả trả về
Method GroupBy nhìn hơi "ngầu" tí:
IEnumerable<IGrouping<TKey, TElement>> GroupBy<TElement, TKey>(
this IEnumerable<TElement> source,
Func<TElement, TKey> keySelector
)
Giải thích:
- source — collection gốc (ví dụ, list sinh viên).
- keySelector — function xác định tiêu chí group, ví dụ property ClassName hoặc City.
Lưu ý!
Kết quả trả về không phải chỉ là mảng object của bạn, mà là collection các group đặc biệt (IGrouping<TKey, TElement>). Mỗi group gồm:
- Key của group (Key — cái đặc điểm chung để gom lại),
- Collection các phần tử thuộc group đó.
Sơ đồ:
| Key (Khóa) | Phần tử trong group (nhóm object) |
|---|---|
| "9A" | Ivan, Oleg, Maria |
| "10Б" | Svetlana, Pyotr |
| ... | ... |
Ví dụ 1: Group sinh viên theo tên lớp
Giả sử có class Student:
public class Student
{
public string Name { get; set; }
public string ClassName { get; set; }
public int Grade { get; set; }
}
Và collection sinh viên:
var students = new List<Student>
{
new Student { Name = "Ivan", ClassName = "9A", Grade = 5 },
new Student { Name = "Oleg", ClassName = "9A", Grade = 4 },
new Student { Name = "Maria", ClassName = "10Б", Grade = 5 },
new Student { Name = "Svetlana", ClassName = "10Б", Grade = 3 }
};
Group sinh viên theo lớp:
var studentsByClass = students.GroupBy(s => s.ClassName);
foreach (var group in studentsByClass)
{
Console.WriteLine($"Lớp: {group.Key}"); // Key của group
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}, điểm: {student.Grade}");
}
}
Chuyện gì xảy ra?
Method GroupBy tạo ra hai group: một cho "9A", một cho "10Б". Lặp qua các group, bạn có thể in ra bất kỳ thông tin aggregate nào cho từng "bó".
Minh họa: collection sau khi GroupBy
Cứ tưởng tượng như này:
students.GroupBy(s => s.ClassName)
├─ group "9A" { Ivan, Oleg }
└─ group "10Б" { Maria, Svetlana }
Mỗi "nhánh" là một mini-list sinh viên, gom theo key.
Sinh viên theo điểm: group theo tiêu chí bất kỳ
Giờ thử group không theo lớp mà theo điểm nhé!
var studentsByGrade = students.GroupBy(s => s.Grade);
foreach (var group in studentsByGrade)
{
Console.WriteLine($"Điểm: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name} từ lớp {student.ClassName}");
}
}
Kết quả:
Điểm: 5
- Ivan từ lớp 9A
- Maria từ lớp 10Б
Điểm: 4
- Oleg từ lớp 9A
Điểm: 3
- Svetlana từ lớp 10Б
3. Xử lý group trong LINQ: có thể làm gì sau GroupBy
Rất thường sau khi group, bạn sẽ muốn lấy thông tin aggregate nào đó — ví dụ đếm số phần tử trong mỗi group, tính điểm trung bình, gom list tên.
Ví dụ 2: Đếm số sinh viên trong mỗi lớp
var classCounts = students
.GroupBy(s => s.ClassName)
.Select(g => new { Class = g.Key, Count = g.Count() });
foreach (var cc in classCounts)
{
Console.WriteLine($"Lớp {cc.Class} có {cc.Count} sinh viên");
}
Kết quả:
Lớp 9A có 2 sinh viên
Lớp 10Б có 2 sinh viên
- Đầu tiên group sinh viên theo lớp.
- Sau đó cho mỗi group tạo object ẩn danh với key (tên lớp) và count (method Count()).
Ví dụ 3: Gom list tên học sinh giỏi theo lớp
var excellentByClass = students
.Where(s => s.Grade == 5)
.GroupBy(s => s.ClassName)
.Select(g => new
{
Class = g.Key,
ExcellentStudents = g.Select(s => s.Name).ToList()
});
foreach (var group in excellentByClass)
{
Console.WriteLine($"Lớp {group.Class} học sinh giỏi: {string.Join(", ", group.ExcellentStudents)}");
}
4. group by trong query syntax
Nếu bạn thích kiểu giống SQL, hoặc quen làm database, thì LINQ cũng hỗ trợ query syntax kiểu group by ... into ....
Ví dụ group cơ bản
var studentsByClass =
from s in students
group s by s.ClassName into classGroup
select classGroup;
foreach (var group in studentsByClass)
{
Console.WriteLine($"Lớp: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
Đầu tiên "group" theo tiêu chí, sau đó đặt tên cho group (into classGroup) — rồi dùng nó làm gì cũng được.
Projection với aggregate
Ví dụ muốn biết các lớp và số sinh viên mỗi lớp:
var countsByCity =
from s in students
group s by s.ClassName into classGroup
select new { Class = classGroup.Key, Count = classGroup.Count() };
foreach (var item in countsByCity)
{
Console.WriteLine($"Lớp {item.Class} có {item.Count} sinh viên");
}
— Syntax vẫn vậy, nhưng sau select bạn có thể làm projection gì cũng được (aggregate, list, average, v.v.).
5. Một số kịch bản nâng cao
Group theo nhiều tiêu chí (key phức hợp)
Đôi khi cần group theo nhiều property cùng lúc. Ví dụ, theo lớp và điểm:
var byClassAndGrade = students
.GroupBy(s => new { s.ClassName, s.Grade });
foreach (var group in byClassAndGrade)
{
Console.WriteLine($"Lớp: {group.Key.ClassName}, Điểm: {group.Key.Grade}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
Key của group giờ là kiểu ẩn danh (kết hợp nhiều field).
Group thực tế hoạt động thế nào?
Một chút "bếp núc" LINQ
- Khi bạn gọi GroupBy, LINQ sẽ tạo một "bảng" đặc biệt bên trong: mỗi giá trị key duy nhất là một group riêng.
- Các group implement interface IGrouping<TKey, TElement>. Có thể lấy property Key để biết tiêu chí group.
Lazy evaluation và hiệu năng
- Group chỉ được tính khi bạn duyệt qua (foreach). Nên nếu collection rất lớn — đừng lo, group chỉ "sống" khi bạn dùng tới.
- Nếu bạn định dùng lại group nhiều lần — có thể gọi .ToList() hoặc .ToArray() để "chốt" kết quả.
Ứng dụng thực tế
Group giúp xây dựng report, thống kê, phân tích phức tạp. Một số ví dụ thực tế:
- Trong shop online: group đơn hàng theo user để show lịch sử mua của từng người.
- Trong nền tảng giáo dục: group sinh viên theo giáo viên hoặc môn học.
- Trong app HR: group nhân viên theo phòng ban và đếm headcount.
- Phỏng vấn: có thể bị yêu cầu viết group dữ liệu thực với aggregate.
LINQ giúp prototype mấy bài này cực nhanh, và biết đọc/viết group là điểm cộng lớn cho dev .NET.
Sơ đồ minh họa
Đây là sơ đồ đơn giản xử lý collection bằng GroupBy:
[Collection sinh viên]
|
[GroupBy theo ClassName]
|
[Group "9A"] ---> [Ivan, Oleg]
[Group "10Б"] --> [Maria, Svetlana]
Mỗi group là một mini-collection với key riêng.
6. Lỗi thường gặp và bẫy khi group
Một số bạn mới học dễ bị rối vì kết quả của GroupBy:
- Sau khi gọi GroupBy bạn không nhận được "group" kiểu List<List<Student>>, mà là collection IGrouping<TKey, TElement>. Để làm việc với group, dùng property Key và lặp qua phần tử trong group.
- Group theo key phức hợp (nhiều field): nhớ dùng kiểu ẩn danh hoặc tạo class riêng nếu cần key có cấu trúc.
- Nếu chỉ cần biết số phần tử trong group — nhớ dùng Select(g => g.Count()), không thì phải đếm thủ công từng group.
- Dễ bị rối với vòng lặp lồng nhau: vòng ngoài lặp group, vòng trong lặp phần tử trong group.
- Nếu sai lambda hoặc key, bạn sẽ ra group không đúng (hoặc chỉ có một group to nếu key giống nhau hết).
GO TO FULL VERSION