CodeGym /Các khóa học /C# SELF /Giao diện IComparer<T&...

Giao diện IComparer<T>

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Hãy tưởng tượng bạn là trưởng khoa ở một trường đại học, nơi có cực nhiều sinh viên. Bạn thường xuyên cần các danh sách được sắp xếp theo nhiều tiêu chí khác nhau:

  1. Theo tên (để tìm sinh viên theo alphabet cho lẹ).
  2. Theo điểm trung bình (để trao học bổng cho mấy bạn giỏi).
  3. Theo tuổi (cho thống kê, cuộc thi, v.v.).
  4. Theo khoá học, và trong mỗi khoá thì theo họ.

Nếu chỉ dùng IComparable<T>, thì class SinhVien của bạn chỉ có thể hiện thực một cách so sánh duy nhất. Ví dụ, bạn quyết định thứ tự "tự nhiên" của sinh viên là theo điểm trung bình. Ok, List.Sort() chạy ngon rồi! Nhưng nếu bạn cần danh sách sắp xếp theo tên thì sao? Class SinhVien đã "bận" so sánh theo điểm rồi. Nó không thể có hai thứ tự "tự nhiên" cùng lúc. Giống như bạn chỉ có một hướng dẫn "làm sao để giỏi boxing", mà giờ lại cần giỏi cờ vua, bạn thử áp dụng hướng dẫn boxing cho cờ vua thì chắc chắn fail rồi, đúng không?

Chính cho mấy trường hợp như vậy, khi cần sắp xếp cùng một kiểu object theo nhiều cách khác nhau và không muốn nhét logic so sánh vào trong class, thì giao diện IComparer<T> ra đời. Nó cực kỳ hữu ích nếu bạn không có quyền sửa code gốc của class hoặc class đó không nên biết hết mọi kiểu sắp xếp.

2. Giao diện IComparer<T>

Nếu IComparable<T> giống như cảm giác bên trong của object, biết tự so sánh chính mình với object khác, thì IComparer<T>một trọng tài hoàn toàn bên ngoài hoặc một giám khảo độc lập, người sẽ lấy hai người chơi (object) bất kỳ và so sánh họ theo luật riêng đã định sẵn.

Tưởng tượng bạn là huấn luyện viên bóng đá. Bạn cần chọn ai làm đội trưởng.

  • IComparable<T>: Mỗi cầu thủ tự nói: "Tôi mạnh hơn thằng kia vì tôi chạy nhanh hơn!" (luật riêng của bản thân).
  • IComparer<T>: Bạn, với tư cách huấn luyện viên, nói: "Ok, hôm nay chọn đội trưởng theo độ chính xác chuyền bóng. Nam, chuyền đi! Hùng, chuyền đi! Ok, Nam chuyền chuẩn hơn! Hôm nay Nam làm đội trưởng." (đây là luật bên ngoài mà bạn áp cho hai người bất kỳ).

Định nghĩa: IComparer<T> là một interface trong .NET cho phép bạn định nghĩa logic so sánh bên ngoài cho hai object kiểu T. Nghĩa là class hiện thực IComparer<T> không phải là một trong hai object được so sánh; nó chỉ cung cấp method Compare, nhận vào hai object và xác định thứ tự của chúng.

Cú pháp

Cú pháp của interface IComparer<T> khá đơn giản:


public interface IComparer<T>
{
    // Phương thức so sánh hai object kiểu T
    // x - object đầu tiên để so sánh
    // y - object thứ hai để so sánh
    int Compare(T x, T y);
}

Method Compare(T x, T y) hoạt động y chang như CompareTo(T other) trong IComparable<T>:

  • Trả về số âm nếu x "nhỏ hơn" y.
  • Trả về không (0) nếu x "bằng" y.
  • Trả về số dương nếu x "lớn hơn" y.

Trong trường hợp của mình, "nhỏ hơn", "bằng", "lớn hơn" hoàn toàn phụ thuộc vào logic so sánh mà bạn tự định nghĩa trong Compare.

Khác biệt giữa IComparer<T>IComparable<T>

Class / Interface Nơi hiện thực Dùng để làm gì Ví dụ sử dụng
IComparable<T>
Ngay trong kiểu (class/struct) Một cách so sánh chuẩn duy nhất Sắp xếp tăng dần theo ID
IComparer<T>
Trong class riêng biệt Bao nhiêu kiểu so sánh cũng được Sắp xếp theo tên, ngày tháng

3. Hiện thực IComparer<T> thực tế

Giờ mình sẽ phát triển tiếp "một app lớn" — mô hình user đơn giản. Giả sử bạn có class như này:


// Class user của mình
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Sắp xếp theo tên: tạo comparator riêng

Viết một class riêng hiện thực IComparer<User>, chuyên so sánh user theo tên:


// Class comparator để sắp xếp theo tên
public class UserNameComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // Kiểm tra null (cho chắc ăn!)
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;   // null "nhỏ hơn" mọi object
        if (y is null) return 1;

        // So sánh theo tên (chuẩn so sánh string)
        return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
}

Dùng comparator để sort list:


List<User> users = new List<User>
{
    new User { Name = "Ivan", Age = 20, Email = "ivan@mail.com" },
    new User { Name = "Anna", Age = 32, Email = "anna@gmail.com" },
    new User { Name = "Boris", Age = 28, Email = "boris@work.org" },
    new User { Name = "Ruslan", Age = 19, Email = "ruslan@yandex.ru" }
};

// Sắp xếp theo tên bằng IComparer
users.Sort(new UserNameComparer());

users.ForEach(u => Console.WriteLine(u.Name)); // Anna, Boris, Ivan, Ruslan

Thấy không, mọi thứ rất gọn gàng và đẹp! Class comparator trở thành một "công dân độc lập" trong chương trình — bạn có thể dùng cho bất kỳ list user nào khác.

4. Nhiều kiểu so sánh: tạo nhiều comparator khác nhau

Điều thú vị là bạn có thể tạo bao nhiêu comparator tuỳ thích. Ví dụ, giờ mình sẽ hiện thực sắp xếp theo tuổi:


// Comparator để sắp xếp theo tuổi
public class UserAgeComparer : 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;

        // Sắp xếp tăng dần theo tuổi
        return x.Age.CompareTo(y.Age);
    }
}

Và bây giờ:


users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Kết quả: Ruslan (19) Ivan (20) Boris (28) Anna (32)

Nếu muốn sắp xếp theo tuổi giảm dần, chỉ cần đảo ngược thứ tự tham số:


// Comparator để sắp xếp giảm dần theo tuổi
public class UserAgeDescendingComparer : 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;

        // Đảo thứ tự: y.CompareTo(x)
        return y.Age.CompareTo(x.Age);
    }
}

5. Một số lưu ý hay ho

Bên trong nó hoạt động thế nào?

Khi bạn gọi Sort() với comparator, list sẽ đưa từng phần tử cho comparator và hỏi: "Đặt ai trước?". Compare của bạn trả lời: "thằng này, thằng kia hoặc giữ nguyên". Sort sẽ lặp lại câu hỏi này cho mọi cặp, cho đến khi ra list cuối cùng đã sort.

Nếu hai giá trị bằng nhau thì sao? Chỉ cần trả về 0 — nghĩa là thứ tự không đổi (hoặc do cơ chế sort bên trong quyết định).

IComparer<T> còn dùng ở đâu nữa?

Interface IComparer<T> không chỉ dùng cho list đâu. Đây là vài ví dụ nó xuất hiện trong .NET:

Ví dụ:


var sortedSet = new SortedSet<User>(new UserAgeComparer());

Giờ SortedSet sẽ luôn tự động giữ thứ tự theo tuổi!

Về null-safe ngắn gọn

Một trong những lỗi newbie hay gặp nhất là NullReferenceException. Đừng quên kiểm tra object có null không trong Compare, nhất là khi list có thể chứa giá trị null.

Pattern phổ biến (nhắc lại cho chắc):


if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;

Thói quen này rất tốt: giúp tránh crash app vào lúc không ngờ nhất!

Ưu điểm và hạn chế của cách dùng IComparer<T>

  • Rõ ràng tách biệt logic so sánh khỏi dữ liệu. Class user không cần quan tâm sẽ bị sort kiểu gì.
  • Dễ tái sử dụng logic so sánh ở nhiều nơi.
  • Rất mở rộng: muốn bao nhiêu kiểu sort cũng được mà không phải sửa kiểu gốc.

Nhưng nhớ cẩn thận, đừng quên kiểm tra null hoặc để logic so sánh không nhất quán. Ví dụ, nếu Compare(x, y) trả về 0 thì Compare(y, x) cũng phải trả về 0; nếu Compare(x, y) trả về >0 thì Compare(y, x) phải trả về <0, v.v.

Bảng nhớ nhanh: khi nào dùng cái gì?

Bài toán Dùng cái gì Đặt logic ở đâu
Một kiểu sort "tự nhiên" duy nhất
IComparable<T>
Ngay trong kiểu (class/struct)
Nhiều kiểu sort khác nhau
IComparer<T>
Trong class riêng biệt
Nhanh, một lần, "on the fly"
Comparison<T>
/ lambda
Trong tham số method Sort, qua delegate
Logic phức tạp, dùng nhiều lần
IComparer<T>
Như một class comparator riêng

Bài sau bạn sẽ học cách dùng và kết hợp delegate với lambda để so sánh object. Còn giờ hãy thử tự code vài comparator cho app của mình và tận hưởng kiến trúc gọn gàng, nơi sort nằm ngoài class chính, còn logic chọn tiêu chí thì cực kỳ linh hoạt và mở rộng.

6. Lỗi thường gặp khi code comparator

Lỗi #1: không kiểm tra null.
Nếu một trong hai object đem so sánh là null mà bạn không kiểm tra, app sẽ crash với NullReferenceException.

Lỗi #2: trả về giá trị -1, 0, +1 không đúng.
Method Compare phải trả về số âm nếu object đầu nhỏ hơn object sau, 0 nếu bằng nhau, số dương nếu lớn hơn. Làm sai sẽ khiến sort chạy "kỳ lạ".

Lỗi #3: logic so sánh không đối xứng.
Nếu khi so x với y bạn trả về một giá trị, mà khi so y với x lại trả về cùng giá trị đó (thay vì giá trị ngược lại), kết quả sẽ rất khó lường.

Lỗi #4: dùng Sort() mà không có comparator cho kiểu custom.
Nếu kiểu không hiện thực IComparable hoặc IComparable<T>, gọi Sort() mà không truyền comparator sẽ bị lỗi InvalidOperationException.

Cách tránh:
Kiểm tra kỹ các trường hợp biên, viết unit test cho đoạn code quan trọng (mình sẽ nói kỹ hơn sau!), đừng ngại đọc docs — và hãy để comparator chạy mượt như đồng hồ Thuỵ Sĩ.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Triển khai comparator cho chuỗi
Triển khai comparator cho chuỗi
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION