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

Giao diện IComparable<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 nhờ thằng bạn xếp sách lên kệ. Nếu là sách có số trên gáy (1, 2, 3…), nó sẽ làm ngon lành: "À, 1 trước 2, 2 trước 3". Đó là thứ tự tự nhiên của số (theo thứ tự tăng dần). Tương tự, nếu là sách có tên bắt đầu bằng "A", "B", "C", nó sẽ xếp theo alphabet. Đó cũng là thứ tự "tự nhiên" cho string (theo alphabet).

Nhưng nếu trên gáy chỉ có họ tác giả thì sao? Bạn bảo: "Xếp theo thứ tự đi". Thằng bạn sẽ hỏi: "Theo thứ tự nào? Theo họ? Theo năm xuất bản? Theo số trang?" Đấy, vấn đề bắt đầu từ đây. Với các entity tự chế của bạn thì không có rõ ràng thứ tự tự nhiên.

Lập trình cũng vậy thôi. Khi mình sort list số nguyên List<int>, C# biết phải làm gì. Với List<string> cũng ổn, nó dùng thứ tự từ điển (alphabet). Nhưng nếu bạn có List<Student>, mỗi Student là object gồm tên, họ, tuổi, id, vân vân, thì C# chịu. Nó không biết so sánh hai student theo cái gì. Theo tên? Theo ID? Theo điểm trung bình? Đó chính là vấn đề mà bạn gặp ở bài "Sắp xếp collection", khi List<T>.Sort() báo lỗi khi thử sort collection kiểu tự định nghĩa.

Để giải quyết, mình phải chỉ rõ cho C# biết tiêu chí so sánh object của mình là gì. Đó là lý do có giao diện IComparable<T>.

2. Giao diện IComparable<T>

Vậy, để object của mình "biết" vị trí của nó so với object khác cùng loại, mình dùng giao diện IComparable<T>. Đây là một contract. Khi class hoặc struct của bạn implement giao diện này, bạn như nói với compiler: "Ê! Object của tao so sánh được với nhau và đây là hướng dẫn cụ thể."

Nó hoạt động thế nào?

Giao diện IComparable<T> chỉ định đúng một method:

public interface IComparable<in T>
{
    int CompareTo(T other);
}

Method này nhận một object kiểu T (cùng kiểu với object hiện tại) và phải trả về:

  • số âm (< 0), nếu object hiện tại "nhỏ hơn" object so sánh;
  • không (0), nếu chúng "bằng nhau" (về mặt sort);
  • số dương (> 0), nếu object hiện tại "lớn hơn".

Kiểu như trọng tài chấm điểm boxing: bạn đánh hay hơn thì thắng round, điểm cao hơn; đánh dở thì điểm thấp; hòa thì điểm bằng nhau. Ở đây còn đơn giản hơn: chỉ là số có dấu.

Tại sao lại như vậy?

Các method sort (ví dụ List<T>.Sort()) sẽ gọi CompareTo cho các phần tử để biết ai đứng trước, ai đứng sau. Nếu class của bạn implement giao diện này — nó sort được luôn!

3. Thực hành

Giả sử mình có class user như này (User):

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Thử sort list user xem sao:

List<User> users = new List<User>
{
    new User { Name = "Sergey", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

users.Sort(); // BÙM! InvalidOperationException

Nó báo lỗi: "At least one object must implement IComparable" (Ít nhất một object phải implement IComparable).

Sửa lại: implement IComparable<User>

Thêm giao diện vào class. Tạm thời sort theo tuổi — từ nhỏ đến lớn:

public class User : IComparable<User>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public int CompareTo(User other)
    {
        // Chống ngáo: nếu other == null, user hiện tại "lớn hơn"
        if (other == null) return 1;
        return this.Age.CompareTo(other.Age); // sort theo tuổi
    }
}

Giờ tạo list, gọi users.Sort(); và in ra console:

List<User> users = new List<User>
{
    new User { Name = "Sergey", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

// Sort theo tuổi (dùng CompareTo)
users.Sort();

// In user đã sort
foreach (User user in users)
{
    Console.WriteLine($"{user.Name}, {user.Age}");
}

List sẽ sort theo tuổi:

Maria, 22
Anton, 27
Sergey, 31

Minh họa: trước và sau

Tên Tuổi
Sergey 31
Maria 22
Anton 27

Sau khi sort:

Tên Tuổi
Maria 22
Anton 27
Sergey 31

4. Lưu ý và lỗi thường gặp

Chống null

Trong CompareTo nhớ check other khác null. Nếu quên, object có thể dính NullReferenceException — kiểu bug cực kỳ khó chịu trước ngày release. Thường thì nếu object so sánh là null, coi như object hiện tại "lớn hơn":

public int CompareTo(User other)
{
    if (other == null) return 1;
    // ...
}

Nhớ tính chất bắc cầu

Nếu A < B và B < C, thì A phải < C. Nếu không giữ quy tắc này, sort sẽ loạn (vui đấy, nhưng sai nhé!).

Nếu sort theo nhiều trường

Ví dụ, cần sort theo tuổi, nếu bằng tuổi thì sort tiếp theo tên alphabet. Làm như này:

public int CompareTo(User other)
{
    if (other == null) return 1;

    int ageCompare = this.Age.CompareTo(other.Age);
    if (ageCompare != 0) return ageCompare;

    // Nếu tuổi bằng nhau — so sánh tên
    return this.Name.CompareTo(other.Name);
}

5. Sort: giờ object của bạn cũng làm được

Mọi thứ List<int> làm được — class của bạn cũng làm được

Giờ bạn dùng được mọi method cần so sánh: Sort, BinarySearch, thậm chí chèn vào collection đã sort (ví dụ SortedSet<T>).

users.Sort();
// users giờ đã sort theo tuổi (và tên nếu tuổi bằng nhau)

Ví dụ trong app quản lý user

Giả sử trước đó bạn đã làm app quản lý user. Giờ mình thêm luôn thứ tự "tự nhiên" vào code cũ. Nó sẽ như này:

// Class User đã implement IComparable<User>

List<User> users = new List<User>
{
    new User { Name = "Ivan", Age = 45 },
    new User { Name = "Galina", Age = 27 },
    new User { Name = "Yuriy", Age = 27 }
};

// Sort theo tuổi, rồi theo tên
users.Sort();
foreach (var u in users)
{
    Console.WriteLine($"{u.Name} - {u.Age}");
}

Kết quả:

Galina - 27
Yuriy - 27
Ivan - 45

Galina và Yuriy cùng tuổi, sort tiếp theo tên khi tuổi bằng nhau.

6. Cách CompareTo hoạt động: chuẩn toán học

Nhắc lại quy tắc trả về:

  • Số âm (ví dụ -1): object hiện tại đứng trước object so sánh.
  • Không: coi như bằng nhau khi sort.
  • Số dương (ví dụ 1): object hiện tại đứng sau object so sánh.

Kiểu dữ liệu built-in (ví dụ Age.CompareTo(other.Age)) đã implement chuẩn này, luôn trả về -1, 0 hoặc 1.

Bảng trả về của method CompareTo

Giá trị trả về Ý nghĩa? Ví dụ
< 0 Nhỏ hơn (đứng trước)
20.CompareTo(21): -1
0 Bằng nhau
23.CompareTo(23): 0
> 0 Lớn hơn (đứng sau)
42.CompareTo(15): 1

7. Sort nhiều trường: kết hợp field

Đôi khi cần sort phức tạp hơn: ví dụ theo họ, tên, tuổi. Dùng cách quen thuộc, so sánh lần lượt:

public class Student : IComparable<Student>
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public int CompareTo(Student other)
    {
        if (other == null) return 1;
        int lastNameCompare = this.LastName.CompareTo(other.LastName);
        if (lastNameCompare != 0) return lastNameCompare;
        int firstNameCompare = this.FirstName.CompareTo(other.FirstName);
        if (firstNameCompare != 0) return firstNameCompare;
        return this.Grade.CompareTo(other.Grade);
    }
}

8. Khi KHÔNG nên implement IComparable<T>

Tình huống thực tế (kiểu dev!): nếu object không có "thứ tự tự nhiên" thì đừng implement IComparable<T>. Ví dụ, class Point, bạn không biết sort theo X, Y hay khoảng cách từ gốc tọa độ — tốt nhất để sort bên ngoài bằng function so sánh (IComparer<T>). Phần này sẽ nói ở bài sau nhé.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION