LoginSignup
23
18

More than 5 years have passed since last update.

「既定の等値比較子」の罠にハマる

Posted at

前書き

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を使用してデータをグループ化するサンプルです。

Sample.cs
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();
    }
}
output
初音
 - ミク
鏡音
 - リン
鏡音
 - レン
巡音
 - ルカ

LastNameでグループ化できていません。
グループ化部分をキャストするとどうかな?

Sample.cs
// LastNameでグループ化する(ここで「既定の等値比較子」を使用している)
var groups = people.GroupBy(v => (AVocaloid)v);
output
初音
 - ミク
鏡音
 - リン
 - レン
巡音
 - ルカ

今度はグループ化できました。

つまり、型の解釈によって結果が変わる怪しげなコードになっているということです。

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クラスに以下のメソッドを追加します。

Sample.cs
// これを追加
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);
}
output
初音
 - ミク
鏡音
 - リン
 - レン
巡音
 - ルカ

正しくグループ化されました。

まとめ

Equalsの実装は基本的なことですが、忘れがちでハマりがちです(私は)。
実装する際はお互い気をつけて記述していきましょう。

23
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
18