Effective C# 原則26:用IComparable和IComparer實現對象的循序關聯性(譯)

來源:互聯網
上載者:User

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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.