コレクションの要素を特定するためのインターフェイス
今回はコレクション内の要素が等しいかどうかを判断する際に用いられるインターフェイスIEquatable<T>
及びIEquaulityComparer<T>
について再確認してみたいと思います。
.NET/C#のコレクションに関するまとめ情報はこちら
.NET のコレクションついて再確認する
コレクション内での比較と並べ替え | Microsoft Docs
等しいかどうかの確認
Contains
、IndexOf
、LastIndexOf
、Remove
などのメソッドは、コレクション要素に対して等値比較子を使用します。 コレクションがジェネリックの場合、次のガイドラインに従ってアイテムの等価性が比較されます。
T 型でIEquatable<T>
ジェネリック インターフェイスが実装されている場合、等値比較子はそのインターフェイスのEquals
メソッドです。
T 型でIEquatable<T>
が実装されていない場合、Object.Equals
が使用されます。
また、ディクショナリ コレクションのコンストラクターの一部のオーバーロードでは、IEqualityComparer<T>
の実装が受け付けられて、キーが等しいかどうかを比較するために使用されます。
上記ドキュメントによると、
- コレクションの要素が
IEquatable<T>
を実装している場合はIEquatable<T>
のEquals
メソッドが利用される。 -
IEquatable<T>
が実装されていない場合は、__Object.Equals
__が使用される。 -
Dictionary<TKey,TValue>()
のキー値が等しいかの比較には__IEqualityComparer<T>
__が使用される。
とあります。
1. Object.Equals
メソッドとIEquatable<T>
インターフェイス
Object.Equals
まずは、コレクションの要素がIEquatable<T>
を実装しない場合に利用されるObject.Equals
メソッドの挙動について確認しましょう。
- 参照型(クラス)の場合
Object.Equals
は参照の等価性を比較するため、比較対象が同じインスタンスを参照している場合のみTrueを返します。
したがって、すべてのフィールド、プロパティの値が一致していてもインスタンスが異なるとFalseを返します。 - 値型(構造体)の場合
Object.Equals
は、型のフィールド、プロパティを全て調べるような実装にオーバーライドされています。そのため、インスタンスが異なっていても、プロパティ、フィールドの値が一致していれば同値とみなされます。
//Classの場合
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
var p1 = new Point(11,22);
var p2 = new Point(11,22);
Console.WriteLine(p1.Equals(p2)); //> false (値は同じだが、参照が異なるのでfalse)
var p3= p1;
Console.WriteLine(p1.Equals(p3)); //> true (同じインスタンスを参照しているのでtrue)
//構造体の場合
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
var ps1= new Point(11,22);
var ps2= new Point(11,22);
Console.WriteLine(ps1.Equals(ps2)); //> true (値型の場合は、参照が異なっていても値が一致していればtrue)
つまり、参照型でオブジェクトの参照ではなく「値が一致しているか」を確認する必要がある場合は、IEquatable<T>
を実装して等価性を定義してあげる必要があるということになります。
また、値型の場合でもObject.Equals
のフィールドやプロパティの同値性を調べる処理がリフレクションを使った非常に重い処理となっているため、IEquatable<T>
を実装してその型専用の処理を記述してあげた方が実行速度の面で有利になるとの事です。
- コレクションの要素として独自の型を利用する場合は、
IEquatable<T>
を実装することを検討しましょう。
IEquatable<T>
IEquatable<T>
にはジェネリック型引数Tを引数に取るEquals
メソッドのみが定義されています。
public interface IEquatable<T>
{
bool Equals(T? other);
}
IEquatable<T>
の実装について下記にガイドラインがあります。
このガイドラインにも記載されていますが、C#9.0から利用可能な record
型 を使うことで、下記のような IEquatable<T>
の実装は記述する必要がなくなります。
record
が利用可能なケースではできるだけrecord
型で定義するようにしましょう。
レコード型の例
レコード型の場合、下記のように定義するだけで 下記のPointクラスと同等のコードが自動生成されます。
record Point(int X, int Y);
//または
record Point
{
public int X {get; init;}
public int Y {get; init;}
}
クラスの例
ガイドラインに従い、下記の通りに実装します。
- IEquatableを実装し、メソッド
bool Equals(T other)
を定義します - Object.Equalをオーバーライドします
- Object.GetHashCodeをオーバーライドします
- 演算子(==)と(!=)のオーバーロードを実装します(推奨・必須ではない)
class Point : IEquatable<Point>
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
//IEquatable<Point> の実装
public bool Equals(Point other)
{
//比較相手が NULL だったら false 確定
if (other is null) { return false; }
//参照が一致しているので true
if (ReferenceEquals(this, other)) { return true; }
//実行時の型が異なる場合はFalseとみなす(基底クラスと、それを継承したクラスで比較したケースなど)
if (GetType() != other.GetType()) { return false; }
//プロパティX、Yどうしを比較し、一致していれば true
return (X, Y) == (other.X, other.Y);
}
//Object.Equalsのオーバーライド
public override bool Equals(object obj) => Equals(obj as Point);
//Object.GetHashCodeのオーバーライド
public override int GetHashCode() => (X, Y).GetHashCode();
//演算子(==,!=)のオーバーロード
public static bool operator ==(Point l, Point r) => l?.Equals(r) ?? (r is null);
public static bool operator !=(Point l, Point r) => !(l == r);
}
構造体の例
構造体の場合もクラスと同じ手順で実装を行いますが、
構造体は継承できない、Nullを考慮する必要がないということでより簡略化した記述となっています。
struct Point : IEquatable<Point>
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public override bool Equals(object obj) => (obj is Point other) && this.Equals(other);
public bool Equals(Point p) => (X,Y) == (p.X,p.y);
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(Point l, Point r) => l.Equals(r);
public static bool operator !=(Point l, Point r) => !(l == r);
}
(オマケ)タプルを利用したプロパティの初期化と比較、そしてGetHashCodeの実装
ちょっと横道にそれますが、上記サンプルコード(構造体の例など)にちょっと見慣れない形式のコードがいくつか見受けられます。
タプル(ValueTuple)を使った記法なのですが、スッキリと書くことができて便利ですね
特に、GetHashCodeの実装ですが、タプルのGetHashCode()を呼ぶだけで複数の値を組み合わせた場合の適切なHashCodeが取得できます。
//まとめて代入
(X, Y) = (x, y);
//これと同じ
X=x;
Y=y;
//まとめて比較
(X, Y) == (x, y);
//これと同じ
X==x && Y=y;
//GetHashCodeの生成
public override int GetHashCode() => (X, Y).GetHashCode();
//この書き方でもOK
public override int GetHashCode() => HashCode.Combine(X, Y);
タプルについては下記を参照ください
https://ufcpp.net/study/csharp/datatype/tuples/
2. IEqualityComparer<T>
インタフェイス
また、ディクショナリ コレクションのコンストラクターの一部のオーバーロードでは、
IEqualityComparer<T>
の実装が受け付けられて、キーが等しいかどうかを比較するために使用されます。
とあるように、IEqualityComparer<T>
は、Dictionary<TKey,TValue>
や HashSet<T>
のキーを比較する際に用いられます。また、Linqの拡張メソッドの中にも IEqualityComparer<T>
の等価比較を利用するものが多数存在します。
IEqualityComparer<T>
インターフェースは Equals(T,T)
メソッドとGetHashCode(T)
メソッドを持ちます.
public interface IEqualityComparer<in T>
{
bool Equals(T? x, T? y);
int GetHashCode(T obj);
}
Dictionary<TKey,TValue>
で IEqualityComparer<TKey>
を使う
では、Dictionary<TKey,TValue>
を例に見てみましょう
下記のようにKey1,Key2を組み合わせて一意になるようなオブジェクトをキーとして使うとします。
public class Keys
{
public int Key1 { get; }
public int Key2 { get; }
public Keys(int key1, int key2) => (Key1, Key2) = (key1, key2);
}
//IEqualityComparer<Keys>を実装したクラス。これをDictionaryのコンストラクタに渡す
public class KeysEqualityComparer : IEqualityComparer<Keys>
{
public bool Equals(Keys a, Keys b) => (a.Key1, a.Key2) == (b.Key1, b.Key2);
public int GetHashCode([DisallowNull] Keys obj) => (obj.Key1, obj.Key2).GetHashCode();
}
KeysEqualityComparer
のインスタンスを Dictionaryの引数に渡して利用します。
キーの組み合わせが一致する要素Cが取れることが分かります。
var dict = new Dictionary<Keys, string>(new KeysEqualityComparer())
{
[new Keys(1, 2)] = "A",
[new Keys(2, 1)] = "B",
[new Keys(2, 2)] = "C",
[new Keys(3, 3)] = "D"
};
var k = new Keys(2, 2);
if(dic.TryGetValue(k, out string x))
{
Console.WriteLine($"Containts: {x}"); //> Containts: C
}
else
{
Console.WriteLine($"not exists: Keys=({k.Key1},{k.Key2})");
}
IEqualityComparer<T>
を渡さない場合
DictionaryのコンストラクタでIEqualityComparer<T>
を指定しない場合、
IEqualityComparer<T>
を実装した基本クラス EqualityComparer<T>
の Default
プロパティが使われます。
public abstract class EqualityComparer<T> : System.Collections.Generic.IEqualityComparer<T>, System.Collections.IEqualityComparer
{
protected EqualityComparer();
public static EqualityComparer<T> Default { get; }
public abstract int GetHashCode([DisallowNull] T obj);
public abstract bool Equals(T? x, T? y);
}
EqualityComparer<T>.Default
Defaultプロパティは、ジェネリック型引数T
に対する既定の比較子(EqualityComparer<T>
)を返します。
その挙動は以下の通りです。
-
T
がIEquatable<T>
を実装している場合、IEquatable<T>.Equals()
、IEquatable<T>.GetHashCode()
を利用 - それ以外では
Object.Equals()
、Object.GetHashCode()
を利用
先ほどのコードでDictionaryにEqualityComparer
を渡さないようにします。
Keys
がIEquatableを実装しないため、TryGetValue
メソッドでは Keys
クラスのObject.Equals()
が呼ばれます。
その結果、同じKe1,Key2の組み合わせが存在しても(オブジェクトのインスタンスが異なるので)TryGetVale()
メソッドはFalseを返します。
//IEqualityComparer<T> を渡さない
var dict = new Dictionary<Keys, string>()
{
[new Keys(1, 2)] = "A",
[new Keys(2, 1)] = "B",
[new Keys(2, 2)] = "C",
[new Keys(3, 3)] = "D"
};
var k = new Keys(2, 2);
if(dic.TryGetValue(k, out string x))
{
Console.WriteLine($"Containts: {x}");
}
else
{
Console.WriteLine($"not exists: Keys=({k.Key1},{k.Key2})"); //> not exists: keys=(2,2)
}
次に、Keysクラスを拡張し、IEquatable<T>
を実装させてみます
class KeysEq :Keys, IEquatable<KeysEq>
{
public KeysEq(int key1, int key2) : base(key1, key2) { }
public bool Equals(KeysEq other)
{
if (other is null) { return false; }
if (ReferenceEquals(this, other)) { return true; }
if (GetType() != other.GetType()) { return false; }
return (Key1, Key2) == (other.Key1, other.Key2);
}
public override bool Equals(object obj) => Equals(obj as KeysEq);
public override int GetHashCode() => (Key1, Key2).GetHashCode();
}
今度は、IEquatable<KeysEq>.Equals()
が呼ばれます。
このメソッドは、Key1,Key2の組み合わせが一致するかで同値判定を行うよう実装しているため、Keys=(2,2)の値"C"が取得できます。
//IEqualityComparer<T> を渡さない
//KeysEqクラスをKeyに利用
var dict = new Dictionary<KeysEq, string>()
{
[new Keys(1, 2)] = "A",
[new Keys(2, 1)] = "B",
[new Keys(2, 2)] = "C",
[new Keys(3, 3)] = "D"
};
var k = new KeysEq(2, 2);
if(dic.TryGetValue(k, out string x))
{
Console.WriteLine($"Containts: {x}"); //> Containts: C
}
else
{
Console.WriteLine($"not exists: Keys=({k.Key1},{k.Key2})");
}
まとめ
独自の型をコレクションの要素として利用する際には、次のことを考慮して設計しましょう
- 同値性の判断が必要か、必要であればその基準が「参照の一致」で良いのか、「値の一致」が必要なのかを考えましょう。
- そして「値の一致」が必要な場合は、
-
IEquatable<T>
が自動実装されるレコード型を利用することを考えよう - レコード型を利用できない場合は、
IEquatable<T>
を実装した型を用意しよう - Dictionary(やHashSet)のキーに使う型についても、レコード型か
IEquatable<T>
を実装した型を利用するか、 - もしくは、
IEqualityComparer<T>
を実装したクラスをコンストラクタに渡すようにしよう
-