Effective C# 原则26:用IComparable和IComparer实现对象的顺序关系

Effective C# 原则26:用IComparable和IComparer实现对象的顺序关系

Item 26: Implement Ordering Relations with IComparable and 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 可以为排序提供一个可选的排序依据,这可以用于一些没有给你提供排序依据的类型上,提供你自己的排序依据。
==========
   

Item 26: Implement Ordering Relations with IComparable and 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.

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:

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
}

 

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:

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() is now an explicit interfaceimplementation; 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:

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 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.

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:

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
}

 

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:

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.

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.

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.

你可能感兴趣的:(comparable)