你的類型應該有一個順序關系,以便在集合中描述它們如何存儲以及排 序。.Net框架為你提供了兩個接口來描述對象的順序關系:IComparable 和 IComparer。IComparable 為你的類定義了自然順序,而實現IComparer接口的類 可以描述其它可選的順序。你可以在實現接口時,定義並實現你自己關系操作符 (<,>,<=,>=),用於避免在運行時默認比較關系的低效問題。這 一原則將討論如何實現順序關系,以便.Net框架的核心可以通過你定義的接口對 你的類型進行排序。這樣用戶可以在些操作上得更好的效率。
IComparable接口只有一個方法:CompareTo(),這個方法沿用了傳統的C 函數庫裡的strcmp函數的實現原則:如果當前對象比目標對象小,它的返回值小 於0;如果相等就返回0;如果當前對象比目標對象大,返回值就大於0。 IComparable以System.Object做為參數,因此在使用這個函數時,你須要對運行 時的對象進行檢測。每次進行比較時,你必須重新解釋參數的類型:
public struct Customer : IComparable
{
private readonly string _name;
public Customer( string name )
{
_name = name;
}
#region IComparable Members
public int CompareTo( object right )
{
if ( ! ( right is Customer ) )
throw new ArgumentException( "Argument not a customer",
"right" );
Customer rightCustomer = ( Customer )right;
return _name.CompareTo( rightCustomer._name );
}
#endregion
}
關於實現比較與IComparable接口的一致性有 很多不太喜歡的地方,首先就是你要檢測參數的運行時類型。不正確的代碼可以 用任何類型做為參數來調用CompareTo方法。還有,正確的參數還必須進行裝箱 與拆箱後才能提供實際的比較。每次比較都要進行這樣額外的開銷。在對集合進 行排序時,在對象上進行的平均比較次數為N x log(N),而每次都會產生三次裝 箱與拆箱。對於一個有1000個點的數組來說,這將會產生大概20000次的裝箱與 拆箱操作,平均計算:N x log(n) 有7000次,每次比較有3次裝箱與拆箱。因此 ,你必須自己找個可選的比較方法。你無法改變IComparable.CompareTo()的定 義,但這並不意味著你要被迫讓你的用戶在一個弱類型的實現上也要忍受性能的 損失。你可以重載CompareTo()方法,讓它只對Customer 對象操作:
public struct Customer : IComparable
{
private string _name;
public Customer( string name )
{
_name = name;
}
#region IComparable Members
// IComparable.CompareTo()
// This is not type safe. The runtime type
// of the right parameter must be checked.
int IComparable.CompareTo( object right )
{
if ( ! ( right is Customer ) )
throw new ArgumentException( "Argument not a customer",
"right" );
Customer rightCustomer = ( Customer ) right;
return CompareTo( rightCustomer );
}
// type-safe CompareTo.
// Right is a customer, or derived from Customer.
public int CompareTo( Customer right )
{
return _name.CompareTo( right._name );
}
#endregion
}
現在,IComparable.CompareTo()就是一個隱 式的接口實現,它只能通過IComparable 接口的引用才能調用。你的用戶則只能 使用一個類型安全的調用,而且不安全的比較是不可能訪問的。下面這樣無意的 錯誤就不能通過編譯了:
Customer c1;
Employee e1;
if ( c1.CompareTo( e1 ) > 0 )
Console.WriteLine( "Customer one is greater" );
這不能通過編譯,因 為對於公共的Customer.CompareTo(Customer right)方法在參數上不匹配,而 IComparable. CompareTo(object right)方法又不可訪問,因此,你只能通過強 制轉化為IComparable 接口後才能訪問:
Customer c1;
Employee e1;
if ( ( c1 as IComparable ).CompareTo( e1 ) > 0 )
Console.WriteLine( "Customer one is greater" );
當你通過隱式實現IComparable接口而又提供了一個類型安全 的比較時,重載版本的強類型比較增加了性能,而且減少了其他人誤用 CompareTo方法的可能。你還不能看到.Net框架裡Sort函數的所有好處,這是因 為它還是用接口指針(參見原則19)來訪問CompareTo()方法,但在已道兩個對象 的類型時,代碼的性能會好一些。
我們再對Customer 結構做一個小的修 改,C#語言可以重載標准的關系運算符,這些應該利用類型安全的CompareTo() 方法:
public struct Customer : IComparable
{
private string _name;
public Customer( string name )
{
_name = name;
}
#region IComparable Members
// IComparable.CompareTo()
// This is not type safe. The runtime type
// of the right parameter must be checked.
int IComparable.CompareTo( object right )
{
if ( ! ( right is Customer ) )
throw new ArgumentException( "Argument not a customer",
"right");
Customer rightCustomer = ( Customer ) right;
return CompareTo( rightCustomer );
}
// type-safe CompareTo.
// Right is a customer, or derived from Customer.
public int CompareTo( Customer right )
{
return _name.CompareTo( right._name );
}
// 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;
}
#endregion
}
所有客戶的順序關系就是這樣 :以名字排序。不久,你很可能要創建一個報表,要以客戶的收入進行排序。你 還是需要Custom結構裡定義的普通的比較機制:以名字排序。你可以通過添加一 個實現了IComparer 接口的類來完成這個新增的需求。IComparer給類型比較提 供另一個標准的選擇,在.Net FCL中任何在IComparable接口上工作的函數,都 提供一個重載,以便通過接口對對象進行排序。因為你是Customer結構的作者, 你可以創建一個新的類(RevenueComparer)做為Customer結構的一個私有的嵌套 類。它通過Customer結構的靜態屬性暴露給用戶:
public struct Customer : IComparable
{
private string _name;
private double _revenue;
// code from earlier example elided.
private static RevenueComparer _revComp = null;
// return an object that implements IComparer
// use lazy evaluation to create just one.
public static IComparer RevenueCompare
{
get
{
if ( _revComp == null )
_revComp = new RevenueComparer();
return _revComp;
}
}
// Class to compare customers by revenue.
// This is always used via the interface pointer,
// so only provide the interface override.
private class RevenueComparer : IComparer
{
#region IComparer Members
int IComparer.Compare( object left, object right )
{
if ( ! ( left is Customer ) )
throw new ArgumentException(
"Argument is not a Customer",
"left");
if (! ( right is Customer) )
throw new ArgumentException(
"Argument is not a Customer",
"right");
Customer leftCustomer = ( Customer ) left;
Customer rightCustomer = ( Customer ) right;
return leftCustomer._revenue.CompareTo (
rightCustomer._revenue);
}
#endregion
}
}
最後這個版本的Customer結構,包 含了RevenueComparer類,這樣你就可以以自然順序-名字,對對象進行排序;還 可有一個選擇就是用這個暴露出來的,實現了IComparer 接口的類,以收入對客 戶進行排序。如果你沒有辦法訪問Customer類的源代碼,你還可以提供一個 IComparer接口,用於對它的任何公共屬性進行排序。只有在你無法取得源代碼 時才使用這樣的習慣,同時也是在.Net框架裡的一個類須要不同的排序依據時才 這樣用。
這一原則裡沒有涉及Equals()方法和==操作符(參見原則9)。排 序和相等是很清楚的操作,你不用實現一個相等比較來表達排序關系。 實際上 ,引用類型通常是基於對象的內容進行排序的,而相等則是基於對象的ID的。在 Equals()返回false時,CompareTo()可以返回0。這完全是合法的,相等與排序 完全沒必要一樣。
(譯注:注意作者這裡討論的對象,是排序與相等這兩 種操作,而不是具體的對象,對於一些特殊的對象,相等與排序可能相關。)
IComparable 和IComparer接口為類型的排序提供了標准的機制, IComparable 應該在大多數自然排序下使用。當你實現IComparable接口時,你 應該為類型排序重載一致的比較操作符(<, >, <=, >=)。 IComparable.CompareTo()使用的是System.Object做為參數,同樣你也要重載一 個類型安全的CompareTo()方法。IComparer 可以為排序提供一個可選的排序依 據,這可以用於一些沒有給你提供排序依據的類型上,提供你自己的排序依據。
返回教程目錄