しばらくの間、曖昧な理解のまま Equals と GetHashCode を実装していたので、反省。
まず、Equals と GetHashCode の実装については、以下の記事が参考になります。
概ね理解しているつもりだったんですが、今日ハッシュのコレクションを扱うときに、コレクション内のある要素を削除(Remove)しようとしても、削除できない(Contains(item) が false になってしまう)、というバグが出たので調べていたら、こちらの記事に行き当たりました。
- 2 つのオブジェクトが等しい (== 演算子の定義によって等しい) 場合、それらのオブジェクトからは同じハッシュ値が生成されなければならない。同じハッシュ値が生成されない場合、コンテナからオブジェクトを探しだすためのキーとしてハッシュコードを使用することができない。
- 任意のオブジェクト A に対して、A.GetHashCode() は各インスタンス毎に異なる値でなければならない。A のどんなメソッドが呼ばれたとしても、A.GetHashCode() は常に同じ値を返さなければならない。これによって、オブジェクトが格納されている正しいバケツを常に見つけだすことができる。
- ハッシュ関数は任意に入力された複数の整数値に対し、ランダム分布となる返り値を返す必要がある。この性質により、ハッシュベースのコンテナへ効率的にオブジェクトを格納できる。
この実装ポリシーのうち、2番を思いっきり無視していました。つまり、ハッシュのコレクションを生成したあとに、コレクション内のある要素の GetHashCode の演算に利用してるプロパティが変化して、ハッシュ値が変わってしまったようです。
また、以下の様な記事もありました。
エンティティにおける、Equals と GetHashCode の実装
今回このハッシュのコレクションというのは、いわいるエンティティのコレクションだったわけですが、その実装が以下のようなもの。
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public override bool Equals(object obj)
{
var other = obj as User;
return
other != null &&
FirstName == other.FirstName &&
LastName == other.LastName;
}
public override int GetHashCode()
{
return
(FirstName ?? string.Empty).GetHashCode() ^
(LastName ?? string.Empty).GetHashCode();
}
}
テーブルのほうはこんな感じです。
CREATE TABLE Users (
Id INT IDENTITY PRIMARY KEY,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NOT NULL,
UNIQUE (FirstName, LastName)
)
Id が人工キーで、スキーマ上のメインの識別子になります。自然キーとして FirstName と LastName が利用できる(実際には名前は被るので使えないと思いますが…例です)ので、FirstName と LastName に一意キー制約を付けてあります。
この Equals 及び GetHashCode の実装では、以下のガイドラインの推奨に従って、人工キーではなく自然キーによる等価比較の実装を行なっています。(ちなみに ORM は NHibernate です
ビジネスキーの等価性 を使って、 equals() と hashCode() を実装することをお勧めします。ビジネスキーの等価性とは、 equals() メソッドが、ビジネスキー、つまり現実の世界においてインスタンスを特定するキー(自然 候補キー) を形成するプロパティだけを比較することを意味します。
このとき、FirstName と LastName が変化する場合、上記のような実装だとハッシュ値が変わってしまってポリシー違反を起こします。
なので、FirstName と LastName が変化する属性としての側面をもつ場合は、Equals と GetHashCode の実装に使うとマズイということになります。(FirstName と LastName が不変ならオッケーなんですが
このような場合は、素直に変化しない(はずの) Id を用いたほうがよさそうです。
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public override bool Equals(object obj)
{
var other = obj as User;
return
other != null &&
Id == other.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
本来なら自然キーでの実装のほうが、なにかと扱いやすいのですが仕方ないですね。
ちなみにこの例でも、Id のセッタが公開されてしまっているので、キチンと実装する場合は、Id が不変になるように工夫したほうがよさそうです。