Item 26: Implement Ordering Relations with IComparable and IComparer
用IComparable和IComparer实现排序关系
Your types need ordering relationships to describe how collections should be sorted and searched. The .NET Framework defines two interfaces that describe ordering relationships in your types: IComparable and IComparer. IComparable defines the natural order for your types. A type implements IComparer to describe alternative orderings. You can define your own implementations of the relational operators (<, >, <=, >=) to provide type-specific comparisons, to avoid some runtime inefficiencies in the interface implementations. This item discusses how to implement ordering relations so that the core .NET Framework orders your types through the defined interfaces and so that other users get the best performance from these operations.
你的类型需要排序关系来描述集合如何被存储和搜索。.NET框架定义了2个接口来描述类型的排序关系:IComparable和IComparer。IComparable为你的类型定义了自然的顺序。类型实现IComparer来描述其它的排序方式。你可以定义自己的关系操作符(<, >, <=, >=)实现来提供指定类型的比较,以避免在接口实现上出现的运行时低效的状况。本条款讨论了如何实现排序,那样的话核心.NET框架会通过定义的接口来为你的类型排序,其它用户从这些操作获得最好的性能。
The IComparable interface contains one method: CompareTo(). This method follows the long-standing tradition started with the C library function strcmp: Its return value is less than 0 if the current object is less than the comparison object, 0 if they are equal, and greater than 0 if the current object is greater than the comparison object. IComparable takes parameters of type System.Object. You need to perform runtime type checking on the argument to this function. Every time comparisons are performed, you must reinterpret the type of the argument:
IComparable接口包含一个方法:CompareTo()。该方法遵循了长标准传统,以C库函数的strcmp开始:如果当前对象比被比较的对象小,返回值小于0;如果它们相等,返回值等于0;如果当前对象比被比较的对象大,返回值就大于0。IComparable采用System.Object作为参数。你需要对传给方法的参数执行运行时类型检查。每次执行比较的时候,都应该重新解释参数的类型。
- public struct Customer : IComparable
- {
- private readonly String name;
- public Customer(String name)
- {
- this.name = name;
- }
- #region IComparable Members
- public Int32 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
- }
There's a lot to dislike about implementing comparisons consistent with the IComparable interface. You've got to check the runtime type of the argument. Incorrect code could legally call this method with anything as the argument to the CompareTo method. More so, proper arguments must be boxed and unboxed to provide the actual comparison. That's an extra runtime expense for each compare. Sorting a collection will make, on average N x log(n) comparisons of your object using the IComparable.Compare method. Each of those will cause three boxing and unboxing operations. For an array with 1,000 points, that will be more than 20,000 boxing and unboxing operations, on average: N x log(n) is almost 7,000, and there are 3 box and unbox operations per comparison. You must look for better alternatives. You can't change the definition of IComparable.CompareTo(). But that doesn't mean you're forced to live with the performance costs of a weakly typed implementation for all your users. You can create your own override of the CompareTo method that expects a Customer object:
在使用IComparable接口实现比较的一致性方面有很多值得痛恨的东西。你不得不检验参数的运行时类型。不正确的代码可以合法的使用任何东西作为参数来调用该方法,并将参数传递给CompareTo方法。还有,恰当的参数必须被装箱以及拆箱来提供实际的比较。对每个比较来说这都是额外的运行时开销。使用IComparable.Compare方法来对一个集合进行排序,平均需要比较N x log(n)次。每次都会引起三次装箱和拆箱操作。对于有1000个点的数组来说,将会花费多于20000次装箱和拆箱操作,平均来说,N x log(n)几乎是7000,每次比较会有3次装箱和拆箱操作。你必须寻找更好的替代方式。你不能修改IComparable.CompareTo()的定义。但是那并不意味着,你被强迫和为所有用户实现的弱类型的性能代价缠在一起。你可以创建自己重写的传入Customer对象的CompareTo方法。
- public struct Customer : IComparable
- {
- private readonly String name;
- public Customer(String name)
- {
- this.name = name;
- }
- #region IComparable Members
- // IComparable.CompareTo()
- // This is not type safe. The runtime type of the right parameter must be checked.
- public Int32 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 Int32 CompareTo(Customer right)
- {
- return name.CompareTo(right.name);
- }
- #endregion
- }
IComparable.CompareTo() is now an explicit interface implementation; it can be called only through an IComparable reference. Users of your customer struct will get the type-safe comparison, and the unsafe comparison is inaccessible. The following innocent mistake no longer compiles:
IComparable.CompareTo()方法现在是一个显式的接口实现,它只能通过IComparable引用调用。你的Customer结构体的用户将得到类型安全的比较,不安全的比较是不可访问的。下面无意的错误将不能编译通过。
- Customer c1;
- Employee e1;
- if (c1.CompareTo(e1) > 0)
- Console.WriteLine("Customer one is greater");
It does not compile because the arguments are wrong for the public Customer.CompareTo(Customer right) method. The IComparable. CompareTo(object right) method is not accessible. You can access the IComparable method only by explicitly casting the reference:
对于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");
When you implement IComparable, use explicit interface implementation and provide a strongly typed public overload. The strongly typed overload improves performance and decreases the likelihood that someone will misuse the CompareTo method. You won't see all the benefits in the Sort function that the .NET Framework uses because it will still access CompareTo() through the interface pointer (see Item 19), but code that knows the type of both objects being compared will get better performance.
但你实现IComparable时,使用显式接口实现,同时提供一个强类型的公共重载。强类型重载提高了性能,减少对CompareTo方法误用的可能性。你不会看到.Net框架使用的排序方法的优点,因为它将仍然会通过接口指针来访问CompareTo(见Item 19),但是知道被比较对象类型的代码会得到更好的性能。
We'll make one last small change to the Customer struct. The C# language lets you overload the standard relational operators. Those should make use of the type-safe CompareTo() method:
我们将对Customer结构体进行最后一次小小的改动。C#语言让你重载标准的关系操作符,会利用到类型安全的CompareTo方法:
- public struct Customer : IComparable
- {
- private String name;
- public Customer(String name)
- {
- this.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
- }
That's all for the standard order of customers: by name. Later, you must create a report sorting all customers by revenue. You still need the normal comparison functionality defined by the Customer struct, sorting them by name. You can implement this additional ordering requirement by creating a class that implements the IComparer interface. IComparer provides the standard way to provide alternative orders for a type. Any of the methods inside the .NET FCL that work on IComparable types provide overloads that order objects through IComparer. Because you authored the Customer struct, you can create this new class (RevenueComparer) as a private nested class inside the Customer struct. It gets exposed through a static property in the Customer struct:
这些全是customer的标准排序:通过名字。过后,你应该创建一个对所有customer通过revenue进行排序的报告。你仍然需要由Customer结构体定义的正常比较方法:按照name排序。可以通过创建实现IComparer接口的类来实现这个附加的排序要求。IComparer为一个类型的其它排序方式提供了标准的方式。在.Net框架类库里面的任何可以用在IComparable类型的方法,通过IComparer向排序对象提供重载。因为你编写了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
- }
- }
The last version of the Customer struct, with the embedded RevenueComparer, lets you order a collection of customers by name, the natural order for customers, and provides an alternative order by exposing a class that implements the IComparer interface to order customers by revenue. If you don't have access to the source for the Customer class, you can still provide an IComparer that orders customers using any of its public properties. You should use that idiom only when you do not have access to the source for the class, as when you need a different ordering for one of the classes in the .NET Framework.
Customer结构体的最后一个版本,在内部有一个嵌套的RevenueComparer类,让你通过名字(customer的自然顺序)来对一个customer的集合进行排序,同时,通过暴露实现了IComparer接口的类来实现不同的排序(对customer按照revenue排序)。如果你没有对Customer类型的源码的访问权限,你仍然能提供IComparer来使用Customer的任何公共属性对其进行排序。仅仅当你没有对类的源的访问权限的时候,并且在.Net框架下对这些类里的一个需要不同的排序的时候,你才应该使用那个习惯。
Nowhere in this item did I mention Equals() or the == operator (see Item 9). Ordering relations and equality are distinct operations. You do not need to implement an equality comparison to have an ordering relation. In fact, reference types commonly implement ordering based on the object contents, yet implement equality based on object identity. CompareTo() returns 0, even though Equals() returns false. That's perfectly legal. Equality and ordering relations are not necessarily the same.
在这个条款里面,我没有在任何地方提到Equals()或者 == 操作符(见Item 9)。排序关系和相等是不同的操作符。在需要排序关系的时候,不需要实现相等比较。事实上,引用类型通常根据对象内容实现排序,而实现相等性是基于对象定义的。甚至Equals()返回false时,CompareTo()也可以返回0。那是相当合法的。相等性和排序关系不是必须一样的。
IComparable and IComparer are the standard mechanisms for providing ordering relations for your types. IComparable should be used for the most natural ordering. When you implement IComparable, you should overload the comparison operators (<, >, <=, >=) consistently with our IComparable ordering. IComparable.CompareTo() uses System.Object parameters, so you should also provide a type-specific overload of the CompareTo() method. IComparer can be used to provide alternative orderings or can be used when you need to provide ordering for a type that does not provide it for you.
要为你的类型提供排序关系,IComparable和IComparer是标准的机制。IComparable应该被用来进行大多数的排序。当你实现IComparable时,应该重载比较操作符(<, >, <=, >=),使得和自己的IComparable排序一致。IComparable.CompareTo()使用System.Object作为参数,因此你应该提供对CompareTo()方法的类型安全的重载。或者当你需要为一个类型提供排序时,而那个类型没有提供支持时,IComparer可以被用来提供不同的排序。