Thứ Hai, 25 tháng 6, 2012

Sử dụng IComparable và IComparer


Trong C#, khi bạn làm việc với một tập hợp các đối tượng trong một mảng, một danh sách… thì yêu cầu thường gặp là sắp xếp danh sách đó. Nếu như sử dụng Generics trong C# thì bạn chỉ có thể sắp xếp được các kiểu dữ liệu cơ bản như int, float, string… nhờ sử dụng phương thức Sort. Còn những kiểu dữ liệu đặc biệt do người dùng tự định nghĩa (ví dụ là các class như Student, Person, Car…) thì bạn cần phải chỉ định rõ cách sắp xếp cho kiểu dữ liệu đó.
Để cho các bạn dễ hiểu thì chúng ta sẽ xét một ví dụ sau: Tạo một danh sách các số nguyên và sắp xếp danh sách đó theo thứ tự tăng dần. Yêu cầu sử dụng List<> để chứa các số nguyên đó và sử dụng phương thức Sort có sẵn trong List<>. Đoạn code theo yêu cầu bên trên sẽ là như sau:
1List<int> intList = new List<int>();
2intList.Add(42); //Thêm ngẫu nhiên một vài số vào danh sách
3intList.Add(10);
4intList. Add(25);
5//Sắp xếp danh sách này bằng phương thức Sort
6intList.Sort();
7foreach(int number in intList)
8    Console.WriteLine(number);

Như các bạn thấy, chúng ta đã tận dụng phương thức Sort có sẵn trong List<> để sắp xếp các số nguyên này. Bởi vì int là một kiểu dữ liệu cơ bản nên trình biên dịch hoàn toàn biết cách sắp xếp thứ tự các phần tử từ bé đến lớn.
Ví dụ tiếp theo có phần phức tạp hơn, đó là thay vì danh sách các số nguyên thì chúng ta sẽ làm việc với danh sách các đối tượng của lớp Person như sau:
1class Person
2{
3    public int Age{get;set;}
4    public string Name{get;set;}
5}
Việc tạo và thêm phần tử cho danh sách các đối tượng kiểu Person này cũng không có gì là khó khăn.
01List personList = new List();
02personList.Add(new Person{Age=10, Name="John"});
03personList.Add(new Person{Age=15, Name="Ann"});
04personList.Add(new Person{Age=8, Name="Kevin"});
05personList.Sort(); //Lỗi ở đây
06 foreach (var item in personList)
07{
08    Console.WriteLine(item.Name + ":" + item.Age);
09}
10Console.ReadKey(true);

Nhưng lúc này chúng ta không thể gọi phương thức Sort() trên personList bởi vì trình biên dịch sẽ không hiểu là cách sắp xếp đối tượng kiểu Person này là như thế nào (cho 2 đối tượng kiểu Person là A và B, sẽ sắp xếp A trước B hay B trước A?). Nếu bạn không tin thì có thể thử, bạn sẽ nhận đượcInvalidOperationException với thông điệp “Failed to compare two elements in the array”.
Phương thức Sort() của List<> sẽ chỉ có thể thực hiện được trên những class có cài đặt interfaceIComparable. Nói cách khác, các class này phải có một phương thức chỉ rõ cách so sánh hai đối tượng của class đó là như thế nào. IComparable có một phương thức duy nhất là CompareTo. Tham số nhận vào là một kiểu object hoặc trong trường hợp bạn sử dụng interface IComparable<> thì tham số truyền vào là kiểu dữ liệu của lớp cài đặt interface đó. Như vậy, lớp Person của chúng ta lúc này sẽ là:
1class Person : IComparable
2{
3    public int Age {get;set;}
4    public string Name{get;set;}
5    public int CompareTo(Person other)
6    {
7        return this.Age.CompareTo(other.Age);
8    }
9}
Phương thức CompareTo sẽ dựa vào giá trị trả về để biết được thứ tự của hai đối tượng khi đem ra so sánh. Giá trị trả về nhỏ hơn 0 nghĩa là đối tượng hiện tại sẽ nhỏ hơn (hay đứng trước) đối tượng other. Giá trị trả về là 0 thì hai đối tượng bằng nhau, còn nếu giá trị trả về lớn hơn 0 thì đối tượng hiện tại lớn hơn (hay đứng sau) đối tượng other. Ở trên, thay vì phải dùng các câu lệnh if..else để trả về giá trị theo quy định thì chúng ta đã tận dụng lại phương thức CompareTo có sẵn trong các kiểu dữ liệu cơ bản (thuộc tính Age thuộc kiểu Integer). Hiện tại, chúng ta đang cài đặt cho lớp Person này sẽ sắp xếp theo thuộc tính Age. Nếu như cách so sánh phức tạp hơn, chúng ta chỉ cần cài đặt nó trong phương thức CompareTo, miễn sao giá trị trả về phản ánh đúng thứ tự mà chúng ta yêu cầu.
Sau khi đã chỉnh sửa lớp Person như trên thì ta đã có thể sử dụng được phương thức Sort của List<> để sắp xếp các đối tượng kiểu Person. Kết quả xuất ra của chúng ta là như sau:
image
Tuy nhiên, cài đặt IComparable có một hạn chế đó là chúng ta chỉ có thể quy định cho lớp Person này 1 cách sắp xếp duy nhất. Giả sử chúng ta muốn cùng lúc vừa có thể sắp xếp danh sách personList theo tên, vừa có thể sắp xếp theo tuổi thì phải làm sao? Khi đó chúng ta cần đến sự trợ giúp của một interface khác: IComparer.
Khác với IComparable được cài đặt ngay trên đối tượng mà bạn muốn sắp xếp thì IComparer như là một bộ so sánh riêng. Khi cần sắp xếp với bộ sắp xếp nào thì ta chỉ cần gắn nó với bộ so sánh đó là được (Hãy tưởng tượng bạn đang có 10 thùng hàng và có 2 loại máy sắp xếp, nếu cho 10 thùng hàng này qua máy 1 thì các thùng hàng sẽ được sắp xếp theo kích thước, còn nếu cho 10 thùng hàng này qua máy 2 thì các thùng hàng sẽ được sắp xếp theo khối lượng. Máy 1 và 2 ở đây đóng vai tròng giống như các Comparer, mỗi máy sẽ có cách sắp xếp riêng mà khi cần sử dụng thì gọi tới nó).
Chúng ta sẽ bắt đầu với việc cài đặt một Bộ so sánh cho phép so sánh hai đối tượng kiểu Person theo thuộc tính Age và một Bộ so sánh theo thuộc tính Name
1class MyAgeComparer : IComparer
2{
3    public int Compare(Person x, Person y)
4    {
5        return x.Age.CompareTo(y.Age);
6    }
7}
1class MyNameComparer : IComparer
2{
3    public int Compare(Person x, Person y)
4    {
5        return x.Name.CompareTo(y.Name);
6    }
7}
Tương tự như phương thức CompareTo của interface IComparable, phương thức Compare trong interface IComparer cũng dựa vào kết quả trả về để biết thứ tự của hai đối tượng. Lưu ý rằng interface IComparable được cài đặt trực tiếp ngay trên lớp các đối tượng trong danh sách, còn IComparer thì được cài đặt trên một lớp riêng (Vì thế nó sẽ có hai tham số trong phương thức Compare).
Đã có hai Comparer như trên, khi chúng ta muốn sử dụng bộ so sánh nào khi sắp xếp với phương thức Sort thì chỉ việc chỉ định đối tượng của lớp Comparer đó. Ví dụ như với danh sách personList đã tạo ra, chúng ta sẽ sắp xếp danh sách này theo tuổi như sau:
1personList.Sort(new MyAgeComparer());
Còn nếu bạn muốn sắp xếp theo tên thì sao? Rất dễ dàng, chỉ cần thay đối tượng tương ứng trong tham số của phương thức Sort
1personList.Sort(new MyNameComparer());
Làm theo cách này, bạn có thể cài đặt nhiều cách sắp xếp khác nhau trên một kiểu dữ liệu. Khi nào cần cách sắp xếp nào, ta chỉ việc gọi cách sắp xếp đó.
Tổng kết: Chúng ta đã tìm hiểu cách sử dụng hai interface IComparable và IComparer khi cài đặt nó trên một ví dụ thực tế, trong bài kế tiếp, chúng ta sẽ cùng nhau tìm hiểu về hai interface khác làIEnumerable và IEnumerator.

Không có nhận xét nào: