IComparableとIComparer
《 初回公開:2020/06/28 , 最終更新:未 》
visual studio2019 C#8.0および一部C#7.3で動作確認。
【 目次 】
IEnumerableインターフェイスとIEnumeratorインターフェイスのペアのと同様に、よく使われる定義済みインターフェースとしてIComparableインターフェースとIComparerインターフェースがある。
IComparableインターフェースは名前の通りオブジェクトが比較可能である事を示し、オブジェクト同士を比較するために使われる。
一方、IComparerインターフェースもオブジェクト同士を比較する時に使われるのであるが、IComparableインターフェイスが自分自身と他のオブジェクトを比較するのに対して、IComparerインターフェイスを実装したクラスは、引数で与えられた2つのオブジェクトを比較するためのヘルパーオブジェクトの役割を持つ。
IComparable
IComparable インターフェイス
IComparableインターフェイスは以下のように定義されている。
[NullableContextAttribute(2)] public interface IComparable { int CompareTo(object? obj); }
IComparableインターフェイスを実装するには、CompareToメソッドを記述しなければならない。
そしてCompareToメソッドの内部処理として、以下のようなコードを記述しなければならない。
自分自身の値とCompareToメソッドの引数で指定される他のオブジェクトの値とを比較してその結果により以下の値を返す。
- 自分自身と他のオブジェクトの値が等しい場合は 0 を
- 自分自身が他のオブジェクトの値より大きい場合は 正の整数 を
- 自分自身が他のオブジェクトの値より小さい場合は 負の整数 を
IComparableインターフェイスの実装例として、複素数のベクトルの大きさを比較するMyComplexクラスの例を示す。
public class MyComplex : IComparable { private double _re; public double Re { get => _re; } private double _im; public double Im { get => _im; } private double _squaredAbs; public double SquaredAbs { get => _squaredAbs; } public int CompareTo(object obj) { // double型のCompareToメソッドを利用すれば簡単だがそれではサンプルコードにならないので //return abs.CompareTo(obj); MyComplex other = obj as MyComplex; if (other != null) { // 自分が小さい場合は負の整数を返す。 if (SquaredAbs < other.SquaredAbs) { return -1; } // 自分が大きい場合は正の整数を返す。 else if (SquaredAbs > other.SquaredAbs) { return 1; } // 同じ場合は0を else { return 0; } } else throw new ArgumentException("Object is not a MyComplex"); } public MyComplex(double re, double im) { this._re = re; this._im = im; _squaredAbs = re * re + im * im; } public override string ToString() { return $"{Re} + j{Im}"; } }
このコードでは大きさの比較値として、ベクトルの二乗を内部変数_squaredAbsに保持していて、その値同士の比較を行う。
パフォーマンスのため平方根の計算は省略している。
IComparableインターフェイスの用途として、Array.Sort や ArrayList.Sortなどのメソッドによって、複数の要素を持つコレクションのソートに使われる。
ここでは、List<T>クラスのSortメソッドの例を示す。
var list = new List<MyComplex> { new MyComplex(1, 1), new MyComplex(1, 2), new MyComplex(2, 1), // nullとの比較 null, null }; list.Sort(); int i = 0; foreach (var item in list) if (item == null) { Console.WriteLine($"list[{i++}]: null"); } else { Console.WriteLine($"list[{i++}]: {item}"); }
実行結果
list[0]: null list[1]: null list[2]: 1 + j1 list[3]: 1 + j2 list[4]: 2 + j1
ジェネリック版のIComparable<T>インターフェイス
IComparableインターフェイスにもジェネリック版のIComparable<T>が存在する。
冗長ではあるがIComparable<T>を使って実装し直してみると
public class MyGenericComplex : IComparable<MyComplex> { private double _re; public double Re { get => _re; } private double _im; public double Im { get => _im; } private double _squaredAbs; public double SquaredAbs { get => _squaredAbs; } public int CompareTo([AllowNull] MyComplex other) { if (SquaredAbs < other.SquaredAbs) { return -1; } else if (SquaredAbs > other.SquaredAbs) { return 1; } else { return 0; } } public MyGenericComplex(double re, double im) { this._re = re; this._im = im; _squaredAbs = re * re + im * im; } public override string ToString() { return $"{Re} + j{Im}"; } } private static void IComparableTest2() { var list = new List<MyGenericComplex> { new MyGenericComplex(1, 1), new MyGenericComplex(1, 2), new MyGenericComplex(2, 1), // nullとの比較 null, null }; list.Sort(); int i = 0; foreach (var item in list) if (item == null) { Console.WriteLine($"list[{i++}]: null"); } else { Console.WriteLine($"list[{i++}]: {item}"); } }
IComparerインターフェイス
IComparableインターフェイスが自分自身と他のオブジェクトを比較するのに対して、IComparerインターフェイスは引数で与えられた2つのオブジェクトを比較するメソッドを提供する。
IComparerインターフェイスにもジェネリック版が存在する。
IComparer<T>インターフェイスは次のように定義されている。
[NullableContextAttribute(1)] public interface IComparer<[NullableAttribute(2)] in T> { int Compare([AllowNull] T x, [AllowNull] T y); }
IComparerインターフェイスを実装するには、Compareメソッドを記述しなければならない。
Compareメソッドでは、第一引数の値と第二引数の値とを比較してその結果により以下の値を返す。
- 第一引数と第二引数の値が等しい場合は 0 を
- 第一引数が第二引数の値より大きい場合は 正の整数 を
- 第一引数が第二引数の値より小さい場合は 負の整数 を
前述のMyComplexのオブジェクトを比較する、IComparerインターフェイスを実装するMyIComparerメソッドの例を以下に示す。
public class MyComparer : IComparer<MyComplex> { public int Compare([AllowNull] MyComplex x, [AllowNull] MyComplex y) { if (x == null) return -1; //if (y == null) return 1; // 第一引数が小さい場合は負の整数を返す。 if (x.SquaredAbs < y.SquaredAbs) { return -1; } // 第一引数が大きい場合は正の整数を返す。 else if (x.SquaredAbs > y.SquaredAbs) { return 1; } // 同じ場合は0を else { return 0; } } }
ListクラスのSortメソッドにはIComparerオブジェクトを引数として渡す事ができる。
var list = new List<MyComplex>(); list.Add(new MyComplex(1, 1)); list.Add(new MyComplex(1, 2)); list.Add(new MyComplex(2, 1)); // nullとの比較 list.Add(null); list.Add(null); list.Sort(new MyComparer()); int i = 0; foreach (var item in list) if (item == null) { Console.WriteLine($"list[{i++}]: null"); } else { Console.WriteLine($"list[{i++}]: {item}"); }
実行結果
list[0]: null list[1]: null list[2]: 1 + j1 list[3]: 1 + j2 list[4]: 2 + j1
IComparerインターフェイスを実装した複数のクラスを用意する事で複数の異なる基準で大小関係を判定する事ができる。
次のクラスは複素数クラスの虚数部の値を比較する。
public class MyComparer2 : IComparer<MyComplex> { public int Compare([AllowNull] MyComplex x, [AllowNull] MyComplex y) { if (x == null) return -1; //if (y == null) return 1; // 第一引数が小さい場合は負の整数を返す。 if (x.Im < y.Im) { return -1; } // 第一引数が大きい場合は正の整数を返す。 else if (x.Im > y.Im) { return 1; } // 同じ場合は0を else { return 0; } } }
参考記事
- 大小関係の定義と比較 (Compare, IComparable, IComparer) - Programming/.NET Framework/比較 - 総武ソフトウェア推進所
IComparable.CompareToメソッドは引数がobject型であるため、どのような型との比較になるかは呼び出し元次第となります。
そのため、IComparableインターフェイスを実装する側で引数>の型をチェックする必要があります。
上記のコードでは異なる型との比較ではArgumentExceptionをスローするようにしていますが、場合によっては例外とはせず適切な比較結果を返すように実装する事も可能です。
また、null(Nothing)との比較についても、上記のコードではArgumentNullExceptionをスローするようにしていますが、string型のようにnullはすべてのオブジェクトよりも小さいと定義することもできます。 - 配列やコレクション内の要素を並び替える - .NET Tips (VB.NET,C#...)
- Comparer クラス (System.Collections) | Microsoft Docs