# 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
你的類型需要排序關系用于定義集合的排序和搜索。 .NET 框架定義兩個接口描述類型的排序關系: IComparable<T> 和 IComparer<T> 。IComparable 定義了類型的自然排序。類型實現的 IComparer 描述的另一個排序。你可以在接口中提供具體類型的比較并避免運行時低效率的自定義關系操作符( < ,>,<=,>= )的實現。本原則會討論怎么實現排序關系,.NET 核心框架可以根據你定義的接口對你的類型進行排序,其他使用者可以這些操作得到更好的性能。
IComparable 接口包含一個方法: CompareTo() 。這個方法遵循了從 C 庫函數 strmp 開始的傳統:它返回小于0的數如果當前對象小于比較的對象,如果它們相等返回0,如果當前對象比比較的對象大則返回大于0的數。 IComparable<T> 會被 .NET 框架的大多數新 APIs 使用。然而,一些舊的 APIs 仍使用舊的的 IComparable 接口。因此,當你實現 IComparable<T> ,你也應該實現 IComparable 。IComparable 的參數類型為 System.Object 。你需要在運行時對參數進行檢查。每次比較都是耗時的,你必須重新解讀參數的類型:
```
和 IComparer <t>實現排序關系" style="display: none;">public struct Customer : IComparable<Customer>, IComparable
{
private readonly string name;
public Customer(string name)
{
this.name = name;
}
#region IComparable<Customer> Members
public int CompareTo(Customer other)
{
return name.CompareTo(other.name);
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (!(obj is Customer))
throw new ArgumentException("Argument is not a Customer", "obj");
Customer otherCustomer = (Customer)obj;
return this.CompareTo(otherCustomer);
}
#endregion
}</t>
```
注意到這個結構體顯示實現了 IComparable 。這就保證調用之前的接口實現參數為 Object 類型版本的 CompareTo() 的代碼。太不喜歡舊版本的 IComparable 。你不得不要對參數的運行時類型進行檢查。不正確的代碼可以合法調用使用任何參數調用 CompareTo 方法。更糟糕的是,合適的參數也必須經過封箱和拆箱來進行比較。這樣每次比較多了額外的運行花費。對集合進行排序,使用 IComparable 平均進行了 nlg(n) 次比較。每次都會引起三次的封箱和拆箱操作。對于長度為1000的數組,會有20000次封箱和拆箱操作,平均:nlg(n) 大約為7000,并且每次比較有3次封箱和拆箱的操作。
你可能會奇怪為什么你需要實現非泛型的 IComarable 接口。有兩個理由。首先,這只是簡單的向后兼容。你的類型交互的代碼在 .NET 2.0之前創建的。這意味著支持泛型之前的接口。第二,即使現代的代碼都會避免使用泛型,當它是基于反射。反射可能使用泛型,但是這比沒有泛型定義的反射困難得多。支持非泛型版本的 IComparable 接口使得你的類型很容易使用利用反射的算法。
因為舊版本的 IComparable.CompareTo() 已經被接口顯式實現,它只能通過 IComparable 引用調用。你 Customer struct 的使用者可以獲得類型安排的比較,但類型不安全的比較是不可訪問的。下面無辜的錯誤是不會通過編譯:
```
和 IComparer <t>實現排序關系" style="display: none;">Customer c1;
Employee e1;
if (c1.CompareTo(e1) > 0)
Console.WriteLine("Customer one is greater");</t>
```
這不能通過編譯因為參數對于 public Customer.CompareTo(Customer right) 方法是錯誤的。 IComparable.CompareTo(object right) 方法是不可訪問的。你只能通過顯式類型轉換才能訪問 IComparable 方法:
```
和 IComparer <t>實現排序關系" style="display: none;">Customer c1 = new Customer();
Employee e1 = new Employee();
if ((c1 as IComparable).CompareTo(e1) > 0)
Console.WriteLine("Customer one is greater");</t>
```
當你實現 IComparable ,使用顯式的接口實現并提供一個強類型的 public 重寫。強類型的重寫提供性能而且減少一些人錯誤使用 CompareTo 方法的概率。你沒有看到 .NET 框架使用 Sort 函數的所有優勢,因為它通過接口指針訪問 CompareTo() 方法(查看原則22),但是代碼知道兩個比較對象的類型會有更好的性能。
我們對 Customer struct 進行最后的小改變。C# 語言運行你重寫標準關系操作符。這些可以充分利用類型安全的 CompareTo() 方法:
```
和 IComparer <t>實現排序關系" style="display: none;">public struct Customer : IComparable<Customer>, IComparable
{
private readonly string name;
public Customer(string name)
{
this.name = name;
}
#region IComparable<Customer> Members
public int CompareTo(Customer other)
{
return name.CompareTo(other.name);
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (!(obj is Customer))
throw new ArgumentException("Argument is not a Customer", "obj");
Customer otherCustomer = (Customer)obj;
return this.CompareTo(otherCustomer);
}
#endregion
// Relational Operators.
public static bool operator <(Customer left, Customer right)
{
return left.CompareTo(right) < 0;
}
public static bool operator <=(Customer left, Customer right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >(Customer left, Customer right)
{
return left.CompareTo(right) > 0;
}
public static bool operator >=(Customer left, Customer right)
{
return left.CompareTo(right) >= 0;
}
}</t>
```
這就是標準的 Customer 的排序:通過名字。后面你必須創建對所有消費者根據收入的排序。你還是需要定義在 Customer struct 內的正軌的比較函數,即通過名字進行排序。在泛型成為 .NET 框架一部分后,很多 APIs 開發都會要求執行其他排序的 Comparison<T> 的委托。在 Customer 類中很簡單創建一個執行其他排序的靜態屬性。例如,下面這個委托比較兩個消費者的收入:
```
和 IComparer <t>實現排序關系" style="display: none;">public static Comparison<Customer> CompareByReview
{
get
{
return (left,right) =>
left.revenue.CompareTo(right.revenue);
}
}</t>
```
舊的類庫會要求使用 IComparer 接口的類似功能。 IComparer 提供可選的標準沒有泛型的排序。任何發布在 1.x .NET FCL 的 IComparable 實現方法都提供通過 IComparer 排序的重寫。因為你是 Customer struct 的作者,你可以為次在 Customer struct 內部創建一個 pirvate 嵌套類( RevenueComparer )。它會暴露 Customer strcut 的一個靜態屬性:
```
和 IComparer <t>實現排序關系" style="display: none;">public struct Customer : IComparable<Customer>, IComparable
{
private readonly string name;
private double revenue;
public Customer(string name, double revenue)
{
this.name = name;
this.revenue = revenue;
}
#region IComparable<Customer> Members
public int CompareTo(Customer other)
{
return name.CompareTo(other.name);
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (!(obj is Customer))
throw new ArgumentException("Argument is not a Customer", "obj");
Customer otherCustomer = (Customer)obj;
return this.CompareTo(otherCustomer);
}
#endregion
// Relational Operators.
public static bool operator <(Customer left, Customer right)
{
return left.CompareTo(right) < 0;
}
public static bool operator <=(Customer left, Customer right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >(Customer left, Customer right)
{
return left.CompareTo(right) > 0;
}
public static bool operator >=(Customer left, Customer right)
{
return left.CompareTo(right) >= 0;
}
private static RevenueComparer revComp = null;
// return an object that implements IComparer
// use lazy evaluation to create just one.
public static IComparer<Customer> RevenueCompare
{
get
{
if (revComp == null)
revComp = new RevenueComparer();
return revComp;
}
}
public static Comparison<Customer> CompareByReview
{
get
{
return (left,right) =>
left.revenue.CompareTo(right.revenue);
}
}
// Class to compare customers by revenue.
// This is always used via the interface pointer,
// so only provide the interface override.
private class RevenueComparer : IComparer<Customer>
{
#region IComparer<Customer> Members
int IComparer<Customer>.Compare(Customer left, Customer right)
{
return left.revenue.CompareTo(right.revenue);
}
#endregion
}
}</t>
```
最后的 Customer struct 版本,內嵌了 RevenueComparer ,你可以根據名字順序,自然順序進行排序,而且提供根據消費者收入排序實現的 IComparer 接口的可選排序。如果你不能訪問 Customer 類的代碼,你可以使用 public IComparer 的屬性對消費者進行排序。當你不能訪問到類的代碼,就好像你對 .NET 框架的類使用不同的排序,你應該使用這種用法。
在本原則中我還沒有提到 Equals() 或 == 操作符(查看原則6)。排序關系和相等是不同的操作。你不需要在排序關系中實現相等比較。事實上,引用類型通常根據對象內容實現排序,而相等是基于對象是否是同一個對象來判斷。 CompareTo() 返回0,即使 Equals() 返回false。這是完全合法的。相等和排序關系沒有必要相同。
IComparable 和 IComparer 是為你類型提供排序關系的標準機制。IComparable 主要用在自然排序中。當你實現 IComparable ,你也應該重寫和 IComparable 排序一直的比較操作符(< ,> ,<= ,>= )。 IComparable.CompareTo() 使用的是 System.Object 參數,所以你還應該提供具體類型的 CompareTo() 方法的重寫。 IComparer 可以提供另外的排序或者當類型沒有為你提供的排序。
小結:
每天只能發原創博客兩篇,先占個位置,未完待續!
終于整完這篇了,今天剛好是下半年的第一天(雖然現在已經是7月2日的凌晨1:20了),上半年因為身體狀態不行,就一直耽擱了,雖然我要更加努力!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2087383](/blog/2087383)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數