項目20 IComparableとIComparerにより順序関係を実装する
IComparable<T>
とIComparer<T>
は順序関係を定義するインターフェイス。コレクションのソート、検索するために順序関係を定義する必要がある。
例えば'List
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