前書き
C#には等値比較を行なう方法が複数用意されています。この事自体は他の言語を見ても珍しい事ではありませんから、等値比較で混乱したことがある方は多いのではないでしょうか。
この記事では、等値比較の種類について簡単にご紹介しつつ、愚かな私がハマった「既定の等値比較子」に関する罠について書きたいと思います。
等値比較の種類
operator==
, operator!=
演算子による等値比較です。
演算子を定義するには以下の条件があります。
- インターフェイス内に定義することはできない。
- 被演算値の少なくとも1つは、演算子を定義している型と同じ型でなければならない。
つまり、インターフェイス同士に対する演算子を定義することはできないのです。
残念すぎる(´・ω・`)
object.Equals(object)
参照型の場合、既定ではobject.ReferenceEquals(object, object)
が呼ばれますので、参照の等値を判定します。
値型の場合、既定では各フィールドそれぞれのobject.Equals(object)
を使用して比較されます。
このメソッドはオーバーライド可能です。
object.Equals(object, object)
staticなEquals
です。
object.GetHashCode()
等値比較の効率化のために使用されるint型のハッシュ値です。
List<T>.IndexOf(T)
やDictionary<TKey, TValue>.this[TKey]
などで使用されています。
Equals
系メソッドを再定義した場合、このメソッドを正しく定義し直してやる必要があります。
このとき、オブジェクトAとオブジェクトBが等値であるなら、ハッシュ値も等値であるという性質を満たす必要があります。
IEquatable<T>.Equals(T)
IEquatable<T>
を実装することで、型に特化したEquals
を定義することができます。
このメソッドを実装した場合、必ずobject.Equals(object)
を適切にオーバーライドしてください。(後述の罠参照)
IEqualityComparer<T>.Equals(T, T)
とIEqualityComparer<T>.GetHashCode(T)
IEqualityComparer<T>
は、任意の型同士に関して等値比較を定義することができる便利インターフェイスです。
IEquatable<T>
と異なり、クラスの外部から等値性を上塗りできるため、使い勝手がいい場合があります。
また、複雑な型同士の比較も定義することが可能です。
Dictionary
やLINQで使用できます。
EqualityComparer<T>.Default
Dictionary
やLINQにおいて、IEqualityComparer<T>
を指定しない場合に使用される**「既定の等値比較子」**です。
この等値比較子は、次の振る舞いをするEquals(T, T)
を持っています。
- 型
T
に対してIEquatable<T>
が実装されている場合はIEquatable<T>.Equals(T)
を使用して比較を行なう。 - そうでない場合、
object.Equals(object)
を使用して比較を行なう。
このEqualityComparer<T>.Default
が本記事の主役です。
EqualityComparer<T>.Default
の罠
何も考えずにIEquatable<T>
を実装すると(´・ω・`)
LINQのGroupBy
を使用してデータをグループ化するサンプルです。
abstract class AVocaloid : IEquatable<AVocaloid>
{
public string FirstName { get; set; }
public string LastName { get; set; }
// LastNameのみで等値性を評価
public bool Equals(AVocaloid other)
{
return LastName == other.LastName;
}
// LastNameのみでハッシュ値を生成
public override int GetHashCode()
{
return LastName.GetHashCode();
}
}
// 具象クラスを定義する
class Vocaloid : AVocaloid
{
}
class Program
{
public static void Main()
{
var people = new[]
{
new Vocaloid() { FirstName = "ミク", LastName = "初音" },
new Vocaloid() { FirstName = "リン", LastName = "鏡音" },
new Vocaloid() { FirstName = "レン", LastName = "鏡音" },
new Vocaloid() { FirstName = "ルカ", LastName = "巡音" },
};
// LastNameでグループ化する(ここで「既定の等値比較子」を使用している)
var groups = people.GroupBy(v => v);
// グループ群を走査
foreach (var g in groups)
{
Console.WriteLine(g.Key.LastName);
// 各インスタンスを走査
foreach (var v in g)
{
Console.WriteLine($" - {v.FirstName}");
}
}
Console.ReadLine();
}
}
初音
- ミク
鏡音
- リン
鏡音
- レン
巡音
- ルカ
LastName
でグループ化できていません。
グループ化部分をキャストするとどうかな?
// LastNameでグループ化する(ここで「既定の等値比較子」を使用している)
var groups = people.GroupBy(v => (AVocaloid)v);
初音
- ミク
鏡音
- リン
- レン
巡音
- ルカ
今度はグループ化できました。
つまり、型の解釈によって結果が変わる怪しげなコードになっているということです。
EqualityComparer<T>.Default
の動作
前述のEqualityComparer<T>.Default.Equals(T, T)
の動作を再掲します。
- 型
T
に対してIEquatable<T>
が実装されている場合はIEquatable<T>.Equals(T)
を使用して比較を行なう。 - そうでない場合、
object.Equals(object)
を使用して比較を行なう。
このIEquatable<T>
のT
は、親クラスをさかのぼって検査するわけではなく、T
型のみしか感知しないのです!
先程の例では、AVocaloid
型がIEquatable<AVocaloid>
を実装していましたが、Vocaloid
型は実装していません。
これが静的に解釈されてしまっているのです。
ドキュメントを読もう
IEquatable<T>
のMSDNドキュメントに、次のような記述があります。
Notes to Implementers:
If you implement IEquatable, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable.Equals method.
If you do override Object.Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class.
In addition, you should overload the op_Equality and op_Inequality operators.
This ensures that all tests for equality return consistent results.
先程の例では、object.Equals(object)
を実装していませんでしたね(´・ω・`)
そのためにおかしな挙動になっていました。
今度はちゃんと実装する
AVocaloid
クラスに以下のメソッドを追加します。
// これを追加
public override bool Equals(object obj)
{
return obj is AVocaloid ? Equals((AVocaloid)obj) : base.Equals(obj);
}
// ついでに演算子も追加しておく
public static bool operator==(AVocaloid obj1, AVocaloid obj2)
{
return obj1.Equals(obj2);
}
public static bool operator!=(AVocaloid obj1, AVocaloid obj2)
{
return !(obj1 == obj2);
}
初音
- ミク
鏡音
- リン
- レン
巡音
- ルカ
正しくグループ化されました。
まとめ
Equals
の実装は基本的なことですが、忘れがちでハマりがちです(私は)。
実装する際はお互い気をつけて記述していきましょう。