Equals

いつもの id:atsushieno:20050218 より、

>> id:atsushieno:20050218


何でa.Equals(b)をb.Equals(a)にしただけで問題が消えて無くなったんでしょうねぇ。

元ネタは分からないが、Equals の交換法則はよくある問題な気がする。
Object.Equals() の説明では

>> Object.Equals Method


次に示すステートメントは、 Equals メソッドのすべての実装に対し true である必要があります。リストの x、y、および z は、 null 参照ではないオブジェクト参照を表します。

  • x.Equals(x) は、浮動小数点型が関連する場合を除き、 true を返します。
  • x.Equals(y) は y.Equals(x) と同じ値を戻します。
  • x.Equals(y) は、x と y が両方とも NaN である場合に true を返します。
  • (x.Equals(y) && y.Equals(z)) は、x.Equals(z) が true を返す場合に限り、 true を返します。
  • x.Equals(y) を連続して呼び出す場合、x と y が参照するオブジェクトが変更されていない限り、同じ値が返されます。
  • x.Equals(null) は false を返します。

Equals メソッドに関するその他の必須動作については、 GetHashCode メソッドのトピックを参照してください

となっていて、a, b が null でない限り a.Equals(b) と b.Equals(a) は同じ値を戻さなければならない。

これは、一見当然のことにみえるが、実際は次のような実装が多くみうけられる。

public class A
{
  private int number;

  public virtual int Number
  {
    get { return this.number;  }
    set { this.number = value; }
  }

  public override bool Equals(object obj)
  {
    A that = obj as A;
    return (that != null)
        && (that.Number == this.Number);
  }

  public override int GetHashCode()
  {
    return this.Number;
  }
}

このクラスの実装は、この時点では前述の条件すべてを満たしている。
ここで、次のような実装を追加する。

public class B : A
{
  private int subNumber;

  public virtual int SubNumber
  {
    get { return this.subNumber;  }
    set { this.subNumber = value; }
  }

  public override bool Equals(object obj)
  {
    if (base.Equals(obj))
    {
      B that = obj as B;

      return (that != null)
          && (that.SubNumber == this.SubNumber);
    }

    return false;
  }

  public override int GetHashCode()
  {
    return base.GetHashCode();
  }
}

この時点で、A のインスタンス a と、B のインスタンス b において、a.Equals(b) が true であっても、b.Equals(a) が false であるという状況が生まれてしまう。
さて、A が既存の実装の場合、B の実装者はどのような Equals を実装すればよいだろうか?

Java では、

google:JavaHouse "How to override equals(Object)"
さわりしか読んでません*1が、非常に長い話になってますが、同じ話題だと思います。

*1:スレッド切れが追いにくい