オブジェクトの同一性を見分ける EqualityComparer の変更点

更新


[[公開メモ]]

#contents

* object.Equals と object.GetHashCode で意味のある比較を実現する [#qe6373a9]

これらをオーバーライドすることで、同一の内容を持つ異なるオブジェクトを
同値と評価することができる。

 LANG:C#
 using System.Collections.Generic;
 
 // 特殊な同値評価を持つクラスを定義
 class TestClass
 {
     public string text;
     public override bool Equals(object obj)
     {
         return ( obj is TestClass ) && ( (TestClass)obj ).text == text;
     }
     public override int GetHashCode()
     {
         return text.GetHashCode();
     }
     public int ObjectGetHashCode()  // for debug
     {
         return base.GetHashCode();
     }
     public TestClass(string text)
     {
         this.text = text;
     }
 }
 
 ...
     // 同じ値を持つ2つのオブジェクトを作成
     var a = new TestClass("a");
     var b = new TestClass("a");
 
     // == はオブジェクトの同一性をチェックするので false となる
     Assert.IsFalse( a == b );
 
     // AreEqual はオブジェクトの同値性をチェックするので true となる
     Assert.AreEqual(a, b);
     
     // List<>.Contains は同値なインスタンスを探索する
     var list = new List<TestClass>();
     list.Add(a);
     Assert.IsTrue(list.Contains(b));
 
     // Dictionary<,> も同値性を正しく解釈する
     var dictionary = new Dictionary<TestClass, bool>();
     dictionary[a] = true;
     Assert.IsTrue(dictionary.ContainsKey(b));
     Assert.IsTrue(dictionary[b]);

* 今度は見分けられなくなる [#n6b374cc]

で、これはこれで非常に便利なのだけれど、今度は異なるインスタンスを見分けたい時に困る。

現実の問題として、上記のように独自の同値性を実装した TestClass 
のようなクラスのインスタンスを__同一性__で判断して Dictionary 
のようなクラスのインスタンスを、通常通り「同一性」で判断して Dictionary 
のキーにしたかったのだが、その方法が分からず苦労した。

Dictionary<,> では、オブジェクトの同値性をカスタマイズするために 
EqualityComparer を実装すれば良いと書いてある。

これには、Equals と GetHashCode を定義することになる。

.Equals( a, b ) は、return a == b; とか、return object.ReferenceEquals(a, b); 
などとすれば、同一性を判断できるのだが、

.GetHashCode( a ) をどうするか。object.GetHashCode を呼び出したくても
オーバーライドしてしまったものは正攻法で呼び出すことができない。

そこで、オペコードを直に書いて無理矢理 object.GetHashCode を呼び出すことにした。

参照:
http://d.hatena.ne.jp/siokoshou/20070517#p1 ~
http://msdn.microsoft.com/ja-jp/library/exczf7b9(VS.80).aspx ~

 LANG:C#
 using System.Collections.Generic;
 using System.Reflection.Emit;
 
 public class EqualityComparerByRef<T>: EqualityComparer<T>
     where T: class
 {
     public override bool Equals(T x, T y)
     {
         return Object.ReferenceEquals(x, y);
     }
 
     Func<T, int> getHashCode;
     public EqualityComparerByRef()
     {
         var dm = new DynamicMethod(
             "GetHashCodeByRef",                 // name of the dynamic method
             typeof(int),                        // type of return value
             new Type[] { 
                 typeof(T)                       // type of "this"
             },
             typeof(EqualityComparerByRef<T>));  // owner
 
         var ilg = dm.GetILGenerator();
 
         ilg.Emit(OpCodes.Ldarg_0);                          // push "this" on the stack
         ilg.Emit(OpCodes.Call, 
                 typeof(object).GetMethod("GetHashCode"));   // returned value is on the stack
         ilg.Emit(OpCodes.Ret);                              // return
         getHashCode = (Func<T, int>)dm.CreateDelegate(typeof(Func<T, int>));
     }
 
     public override int GetHashCode(T obj)
     {
         return getHashCode(obj);
     }
 
     public static EqualityComparerByRef<T> Default { get { return _default; } }
     static EqualityComparerByRef<T> _default = new EqualityComparerByRef<T>();
 }
 
 ...
 
     var a = new TestClass("a");
     var b = new TestClass("a");
 
     var DefaultComparer = EqualityComparer<TestClass>.Default;
     var ComparerByRef = EqualityComparerByRef<TestClass>.Default;
     
     Assert.IsTrue(DefaultComparer.Equals(a, a));    // same object
     Assert.IsTrue(ComparerByRef.Equals(a, a));      // same object
 
     Assert.IsTrue(DefaultComparer.Equals(a, b));    // different object with same value
     Assert.IsFalse(ComparerByRef.Equals(a, b));     // different object with same value
 
     var list = new List<TestClass>();
 
     list.Add(a);
     Assert.IsTrue(list.Contains(b));
     Assert.IsFalse(list.Contains(b, ComparerByRef));
 
     var dictionary = new Dictionary<TestClass, bool>(ComparerByRef);
     dictionary[a] = true;
     Assert.IsFalse(dictionary.ContainsKey(b));
 
     Assert.AreEqual(b.ObjectHashCode(), ComparerByRef.GetHashCode(b));

* コメント [#xe6efdeb]

#article_kcaptcha

Counter: 9485 (from 2010/06/03), today: 1, yesterday: 0