Effective C# 原则26:用IComparable和IComparer实现对象的顺序关系
你的类型应该有一个顺序关系,以便在集合中描述它们如何存储以及排序。.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
可以为排序提供一个可选的排序依据,这可以用于一些没有给你提供排序依据的类型上,提供你自己的排序依据。