trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

EFFECTIVE C# 6.0/7.0 読書メモ 項目20

項目20 IComparableとIComparerにより順序関係を実装する

IComparable<T>IComparer<T>は順序関係を定義するインターフェイス。コレクションのソート、検索するために順序関係を定義する必要がある。

例えば'List.Sort()'では'T.CompareTo(T)'を使用する。

IComparable<T>

自然な順序を定義する。メソッドはCompareTo(T)

IComparable<T>を実装するときはIComparableも実装すること。IComparable<T>よりも古いインターフェイスであるIComparableを実装する理由は
後方互換
・BCLでは1.0当時の実装との互換性が要求される

IComparableのメソッドはCompareTo(object)。object型の引数を取るため、あらゆるものを指定でき間違ったコードを記述できる。一部の引数(値型?)においてはボックス化とボックス化解除が発生し、コストがかかる。

次のように明示的に実装すると、IComparableインターフェイスの参照を経由しなければ呼び出すことができなくなり、間違って使用するのを防ぐことができる。

public struct Customer : IComparable<Customer>, IComparable
{
    private readonly string name;
    private double revenue;
    //略
    public int CompareTo(Customer other) => name.CompareTo(other.name);
    int IComparable.CompareTo(object obj)
    {
        if (!(obj is Customer))
            throw new ArgumentException("引数はCustomer型ではありません", "obj");
        Customer otherCustomer = (Customer)obj;
        return this.CompareTo(otherCustomer);
    }
    //略
}
Customer c1;
Employee e1;
if( c1.CompareTo(e1)>0 )//シグネチャが一致しないのでコンパイルエラー
{}
if( ((IComparable)c1).CompareTo(e1)>0 )//明示的な呼び出し。コンパイルエラーにならない
{}
関係演算子(<.<=,>,>=)をオーバーロード

CompareTo(T)メソッドを使用するとよい。インターフェイスを定義することによる実行時の性能低下を回避しつつ、型に固有の比較処理を実行できるようになる。

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

IComparer

例ではCustomerに対して、通常の順序(CompareTo)としてnameを使用した。 別の基準(revenue)で順序を付けたい場合はIComparer<T>を使用する。 IComparable<T>型を処理するメソッドには、IComparer<T>インターフェイス経由でオブジェクトを並び替えるようなオーバーロードが用意されている。 例えば List<T>.Sort(IComparer<T>);。他、SortedListなど。

Customer構造体の内部クラスとして新しいクラス(RevenueComarer)を定義するとよい。

    private static Lazy<RevenueComparer> revComp = new Lazy<RevenueComparer>(() => new RevenueComparer());
    public static IComparer<Customer> RevenueCompare => revComp.Value;
    private class RevenueComparer : IComparer<Customer>
    {
        int IComparer<Customer>.Compare(Customer left, Customer right) => left.revenue.CompareTo(right.revenue);
    }

(なぜ遅延で作ってるのだろう…)

Comparison

同じ型の 2 つのオブジェクトを比較するメソッド。
public delegate int Comparison<in T>(T x, T y);

ジェネリックが導入された後から追加されたほとんどのAPIでは、別のソートを実行できるようにComparison<T>を受け取るようになっている。例えばList<T>.Sort(Comparison<T>);

statcプロパティをCustomer型に用意すればよい。 所得額(revenue)で比較する場合、

    public static Comparison<Customer> CompareByRevenue => (left, right) => left.revenue.CompareTo(right.revenue);
注意点

順序関係と同値性はそれぞれ異なる処理。CompareTo()が0を返してもEquals()がfalseを返すことがあり、これは問題のない挙動。

使用例
> var cstl = new List<Customer>() { 
    new Customer("d", 100), 
    new Customer("f", 500),
    new Customer("a", 300), 
    new Customer("g", 400) };
> foreach(var cst in cstl) 
  { Console.WriteLine(cst.Name + "," + cst.Revenue); }
d,100
f,500
a,300
g,400
> cstl.Sort()//デフォではCompareToが使用される
> foreach (var cst in cstl) 
  { Console.WriteLine(cst.Name + "," + cst.Revenue); }
a,300
d,100
f,500
g,400
> cstl.Sort(Customer.CompareByRevenue);//Comparison<T>を使用してSort
> foreach (var cst in cstl)
  { Console.WriteLine(cst.Name + "," + cst.Revenue); }
d,100
a,300
g,400
f,500
> cstl.Sort()
> foreach (var cst in cstl) { Console.WriteLine(cst.Name + "," + cst.Revenue); }
a,300
d,100
f,500
g,400
> cstl.Sort(Customer.RevenueCompare);//IComparer<T>を使用してSort
> foreach (var cst in cstl) { Console.WriteLine(cst.Name + "," + cst.Revenue); }
d,100
a,300
g,400
f,500