1. Giới thiệu
Tại sao comparator lại thực sự quan trọng…
Trong dự án thực tế, mọi thứ không đơn giản như mấy bài tập trên lớp đâu: object phức tạp, dữ liệu đến từ user hoặc service khác, "thứ tự sắp xếp" (hoặc quy tắc so sánh bằng nhau) thường thay đổi tuỳ bài toán. Comparator giúp code linh hoạt, hành vi dễ đoán và kiểm soát được.
Trong .NET, comparator là thứ bắt buộc mỗi khi bạn cần sắp xếp, tìm kiếm, nhóm hoặc loại bỏ trùng lặp object của class tự định nghĩa, cũng như khi xây dựng cấu trúc dữ liệu có "thứ tự". Đặc biệt hay gặp trong collection, thuật toán và khi tích hợp với API ngoài.
Chỗ nào gặp comparator trong .NET:
- Sắp xếp collection (List<T>.Sort, Array.Sort, OrderBy trong LINQ)
- Cấu trúc dữ liệu có thứ tự (SortedSet<T>, SortedDictionary<TKey, TValue>)
- Tìm kiếm và so sánh object trong collection (Contains, IndexOf — khi cần xác định "bằng nhau", không chỉ thứ tự)
- Nhóm, lọc và loại trùng (ví dụ, .Distinct())
Thực tế thì cần để làm gì?
- Báo cáo cần xuất kết quả đã sắp xếp (ví dụ, danh sách sinh viên theo alphabet hoặc điểm trung bình)
- So khớp dữ liệu đầu vào với giá trị chuẩn (ví dụ, tìm bản ghi theo tiêu chí nào đó)
- Tiết kiệm bộ nhớ và tăng tốc (chọn đúng cấu trúc dữ liệu giúp tìm kiếm nhanh, app mượt hơn)
- Kiểm tra tính duy nhất (ví dụ, khi đăng ký user bằng e-mail)
2. Ví dụ thực tế: sắp xếp theo nhiều tiêu chí
Giả sử mình có class user như sau:
public class User
{
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Email { get; set; } = "";
public int Age { get; set; }
// Có thể override Equals và GetHashCode — để tự đọc thêm nhé
}
Mình có một list user, và muốn:
- Sắp xếp theo họ, nếu trùng họ thì theo tên.
- Cho phép tìm user theo e-mail (không phân biệt hoa thường).
- Loại trùng user khi thêm mới.
Sắp xếp bằng IComparer<T>
Cách 1: kiểu truyền thống — tạo class comparator riêng.
// Comparator để sắp xếp theo họ và tên
public class UserFullNameComparer : IComparer<User>
{
public int Compare(User? x, User? y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
if (lastNameComparison != 0)
return lastNameComparison;
return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
}
}
Cách dùng:
var users = new List<User>
{
new User { FirstName = "Ivan", LastName = "Petrov", Age = 20, Email = "ivan.petrov@email.com" },
new User { FirstName = "Anna", LastName = "Smirnova", Age = 22, Email = "anna.smirnova@email.com" },
new User { FirstName = "Petr", LastName = "Petrov", Age = 18, Email = "petr.petrov@email.com" }
};
users.Sort(new UserFullNameComparer());
foreach (var user in users)
{
Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");
}
// Kết quả — user được sắp xếp theo họ, nếu trùng họ thì theo tên.
Lưu ý: Làm kiểu này khi thứ tự so sánh cần ở nhiều chỗ khác nhau trong app và lambda không cứu được khỏi copy-paste.
Sắp xếp "tại chỗ" bằng lambda
Không thích tạo class chỉ để sắp xếp 1 lần? Dùng lambda cho nhanh!
users.Sort((x, y) =>
{
int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
if (lastNameComparison != 0)
return lastNameComparison;
return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
});
Hoạt động y chang, nhưng comparator tạo ngay trong lệnh gọi. Tiết kiệm dòng code, nhưng không tiện nếu cần dùng lại nhiều lần.
3. Tìm kiếm thông minh: so sánh e-mail không phân biệt hoa thường
Thực tế user nhập e-mail kiểu gì cũng được, còn bạn là coder chứ không phải trọng tài, nên hợp lý là so sánh e-mail không phân biệt hoa thường.
Làm cái này bằng comparator và tìm kiếm như sau:
public class EmailComparer : IEqualityComparer<User>
{
public bool Equals(User? x, User? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return string.Equals(x.Email, y.Email, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(User obj)
{
return obj.Email?.ToLowerInvariant().GetHashCode() ?? 0;
}
}
Dùng với HashSet:
var usersSet = new HashSet<User>(new EmailComparer());
usersSet.Add(new User { Email = "Petrov@example.com" });
bool contains = usersSet.Contains(new User { Email = "petrov@example.com" }); // true!
Lưu ý quan trọng: Khi tự tạo EqualityComparer, nhớ implement cả hai method: Equals và GetHashCode. Quên cái thứ hai là behavior sẽ kỳ lạ: so sánh có thể "fail" khi tìm trong collection.
4. Dùng SortedSet và SortedDictionary
Đây là lúc comparator phát huy hết sức mạnh.
SortedSet<T> và SortedDictionary<TKey, TValue> không chạy được với object của bạn nếu bạn không chỉ cho .NET biết cách so sánh chúng. Thứ tự so sánh và bằng nhau ảnh hưởng trực tiếp đến việc phần tử nào được coi là khác biệt!
Ví dụ với SortedSet<User>
var sortedUsersByFullName = new SortedSet<User>(new UserFullNameComparer())
{
new User { FirstName = "Ivan", LastName = "Petrov", Age = 20 },
new User { FirstName = "Anna", LastName = "Smirnova", Age = 22 },
new User { FirstName = "Petr", LastName = "Petrov", Age = 18 },
new User { FirstName = "Ivan", LastName = "Petrov", Age = 25 } // Trùng tên và họ
};
// "Ivan Petrov" khác tuổi — chỉ một người vào set thôi
Console.WriteLine("User trong SortedSet:");
foreach (var user in sortedUsersByFullName)
Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");
SortedSet sẽ không thêm hai "Ivan Petrov"!
Quan trọng: Comparator ở đây quyết định logic "duy nhất". Nếu bạn chỉ so sánh theo họ và tên, thì user trùng họ tên nhưng khác tuổi vẫn bị coi là cùng một người.
5. Bảng: khi nào dùng comparator nào trong .NET
| Tình huống | Cần implement | Ví dụ dùng |
|---|---|---|
| Thứ tự sắp xếp tự nhiên (một, dùng chung cho type) | |
|
| Nhiều kiểu sắp xếp (theo tên, tuổi, e-mail ...) | (class, lambda) |
|
| Tìm phần tử duy nhất trong collection | |
, |
| Nhóm, loại trùng | |
LINQ |
| Sắp xếp theo thời gian, ngày, hoặc tổ hợp field phức tạp | , lambda tại chỗ |
|
| Dùng trong LINQ (truy vấn một lần) | lambda trong , |
|
6. Thực hành: tìm kiếm và so sánh trong collection
Cùng code chức năng đăng ký user với e-mail duy nhất (không phân biệt hoa thường). Nếu user có e-mail đó đã tồn tại, phải báo cho biết.
public static bool RegisterUser(List<User> users, User newUser)
{
// Dùng Any với lambda để kiểm tra e-mail duy nhất
bool exists = users.Any(u =>
string.Equals(u.Email, newUser.Email, StringComparison.OrdinalIgnoreCase));
if (exists)
{
Console.WriteLine($"User với e-mail: {newUser.Email} đã đăng ký rồi!");
return false;
}
users.Add(newUser);
Console.WriteLine($"User {newUser.FirstName} đã được thêm.");
return true;
}
Cách dùng:
var userList = new List<User>
{
new User { Email = "first@example.com" }
};
RegisterUser(userList, new User { FirstName = "Vasya", Email = "FIRST@example.com" });
// Sẽ in: "User với e-mail: FIRST@example.com đã đăng ký rồi!"
7. Lỗi thường gặp khi dùng comparator
Ai cũng thích danh sách lỗi, nhưng mình sẽ kể sinh động hơn.
Đôi khi coder implement comparator cho type tự định nghĩa mà quên check null hoặc — tệ hơn — viết so sánh làm "lệch thứ tự". Ví dụ, nếu comparator trả về giá trị mâu thuẫn, sort sẽ chạy loạn, collection sẽ mất hoặc "dính" object.
Một lỗi nữa là không đồng bộ giữa Equals và logic comparator. Ví dụ, Equals cho là object khác nhau, còn comparator lại cho là giống nhau, thì trong SortedSet hoặc SortedDictionary sẽ loạn: phần tử không tìm thấy dù rõ ràng nó ở đó.
Cũng có trường hợp coder chỉ so sánh một thuộc tính (ví dụ, họ), quên mất có thể nhiều user trùng họ. Kết quả là object bị "đè", mất, dữ liệu không còn nhất quán. Tức là, nó không phản ánh đúng trạng thái thực tế của hệ thống — xuất hiện trùng lặp, mất thông tin hoặc sai logic chương trình.
GO TO FULL VERSION