Chapter 3 - Item 20 : Implement Ordering Relations with IComparable and IComparer

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

本節提出如何利用 IComparable<T> 和 IComparer<T> 寫出高效的物件比較功能(e.g. <, >, <=, >=),其主要應用於物件排序時提供比較的演算法。

範例程式碼:

1. 實作 IComparable<T> 與 IComparable 比較物件大小。

public struct Customer : IComparable<Customer>, IComparable
{
    public string Name { get; }

    public Customer( string name )
    {
        Name = name;
    }

    public int CompareTo( Customer other ) =>
        Name.CompareTo( other.Name );

    int IComparable.CompareTo( object obj )
    {
        try
        {
            var customer = ( Customer ) obj;
            return CompareTo( customer );
        }
        catch ( InvalidCastException )
        {
            throw;
        }
    }
}

在實作 IComparable<T> 時也需實作 IComparable,原因在於較舊的版本(在 .NET 2.0 以前)並沒有泛型的版本可供使用;其可能產生裝箱、拆箱操作。然而藉由明確指定 CompareTo 的泛型介面實作方法為 public 可以避免錯誤呼叫到非泛型的版本。

Customer c1;
Employee e1;

// No longer compiles.
if( c1.CompareTo( e1 ) > 0 )
    Console.WriteLine( "Customer one is greater" ); 

反之需要明確轉型為 IComparable 才能通過編譯。

Customer c1;
Employee e1;
    
// Explicit casts to IComparable.
// Compiles successful.
if( ( ( IComparable )c1 ).CompareTo( e1 ) > 0 )
    Console.WriteLine( "Customer one is greater" ); 

2. 實作 <, >, <=, >= 增加介面實作完整性。

public struct Customer : IComparable<Customer>, IComparable
{
    public string Name { get; }

    public Customer( string name )
    {
        Name = name;
    }

    public int CompareTo( Customer other ) =>
        Name.CompareTo( other.Name );

    int IComparable.CompareTo( object obj )
    {
        try
        {
            var customer = ( Customer ) obj;
            return CompareTo( customer );
        }
        catch ( InvalidCastException )
        {
            throw;
        }
    }

    public static bool operator <( Customer left, Customer right ) =>
        left.CompareTo( right ) < 0;

    public static bool operator <=( Customer left, Customer right ) =>
        left.CompareTo( right ) <= 0;

    public static bool operator >( Customer left, Customer right ) =>
        left.CompareTo( right ) > 0;

    public static bool operator >=( Customer left, Customer right ) =>
        left.CompareTo( right ) >= 0;
}
Note:<, >, <=, >= 運算子和 Equals 與 == 並不衝突,排序時的比較和物件本身是否相等可以是分開的邏輯。(比如說同樣為 18 歲男性,使用年齡排序 CompareTo 會回傳 0;但不同人代表著不同個體,Equals 和 == 會回傳 false。)

3. 實作 Comparsion<T> 委派與 IComparer<T> 擴充不同的比較方式。

public struct Customer : IComparable<Customer>, IComparable
{
    public static Comparison<Customer> CompareByRevenue =>
        ( left, right ) => left.Revenue.CompareTo( right.Revenue );

    public static IComparer<Customer> RevComparer =>
        _revComp.Value;

    private static Lazy<RevenueComparer> _revComp =>
        new Lazy<RevenueComparer>( ( ) => new RevenueComparer( ) );

    public string Name { get; }
    private double Revenue { get; set; }

    public Customer( string name, double revenue )
    {
        Name = name;
        Revenue = revenue;
    }

    public int CompareTo( Customer other ) =>
        Name.CompareTo( other.Name );

    int IComparable.CompareTo( object obj )
    {
        try
        {
            var customer = ( Customer ) obj;
            return CompareTo( customer );
        }
        catch ( InvalidCastException )
        {
            throw;
        }
    }

    public static bool operator <( Customer left, Customer right ) =>
        left.CompareTo( right ) < 0;

    public static bool operator <=( Customer left, Customer right ) =>
        left.CompareTo( right ) <= 0;

    public static bool operator >( Customer left, Customer right ) =>
        left.CompareTo( right ) > 0;

    public static bool operator >=( Customer left, Customer right ) =>
        left.CompareTo( right ) >= 0;

    // Class to compare customers by revenue.
    // This is always used via the interface pointer,
    // so only provide the interface override.
    private class RevenueComparer : IComparer<Customer>
    {
        int IComparer<Customer>.Compare( Customer left, Customer right ) =>
            left.Revenue.CompareTo( right.Revenue );
    }
}

Customer 現在也可以依照 Revenue 來排序。

結論:
1. 當實作了 IComparable<T>,也應同時實作 <, >, <=, >= 以保持結果一致。 

2. IComparable<T> 比 IComparable 少了裝箱、拆箱操作。

3. 實作 Comparsion<T> 委派或 IComparer<T> 擴充不同的比較方式。