為什麼GetHashCode方法需要如此設計?

wjaning發表於2021-09-09

昨天我在實現《透過擴充套件改善ASP.NET MVC的驗證機制[使用篇]》的時候為了Attribute 的一個小問題後耗費了大半天的精力,雖然最終找到了問題的癥結並解決了問題,但是我依然不知道微軟如此設計的目的何在。閒話少說,我們先來演示一下我具體遇到的問題如何發生的。

目錄:
一、問題重現
二、透過Attribute的Equals方法和GetHashCode方法進行對等判斷
三、Attribute物件和Attribute型別的HashCode
四、倘若為FooAttribute新增一個屬性/欄位
五、Attribute的GetHashCode方式是如何實現的?

一、問題重現

如下面的程式碼片斷所示,我們定義了兩個Attribute。其中抽象的BaseAttribute中定義了一個Name屬性,而FooAttribute直接繼承自BaseAttribute,並不曾定義任何屬性和欄位。在型別Bar上,我們應用了三個FooAttribute特性,其Name屬性分別為A、B和C。

   1: [Foo(Name = "A")]
   2: [Foo(Name = "B")]
   3: [Foo(Name = "C")]
   4: public class Bar
   5: {
   6:
   7: }
   8:
   9: [AttributeUsage( AttributeTargets.Class, Inherited=true, AllowMultiple=true)]
  10: public abstract class BaseAttribute : Attribute
  11: {
  12:     public string Name { get; set; }
  13: }
  14: public class FooAttribute : BaseAttribute
  15: {

  17: }

在我的程式中具有類似於如下一段程式碼:我們呼叫Bar型別物件的GetCustomAttributes方法得到所有的Attribute特性並篩選出型別為FooAttribute特性列表,毫無疑問,這個列表包含Name屬性分別為A、B和C的三個FooAttribute物件。然後我們從該列表中將Name屬性為C的FooAttribute物件移掉,最終列印列表出餘下的FooAttribute的Name屬性。

   1: var attributes = typeof(Bar).GetCustomAttributes(true).OfType().ToList();
   2: var attribute = attributes.First(item => item.Name == "C");
   3: attributes.Remove(attribute);
   4: Array.ForEach(attributes.ToArray(), a => Console.WriteLine(a.Name));

按照絕大部分人思維,最終列印出來的肯定是A和B,但是真正執行的結果卻是B和C。下面所示的確實就是最終的執行結果:

   1: B
   2: C


二、透過Attribute的Equals方法和GetHashCode方法進行對等判斷

然後我們透過如下的方式判定兩個FooAttribute物件的對等性。如下面的程式碼片斷所示,我們直接呼叫建構函式建立了兩個FooAttribute物件,它們的Name屬性分別設定為“ABC”和“123”。最後兩句程式碼分別透過呼叫Equals和HashCode判斷兩個FooAttribute是否相等。

   1: FooAttribute attribute1 = new FooAttribute{ Name = "ABC" };
   2: FooAttribute attribute2 = new FooAttribute{ Name = "123"};
   3: Console.WriteLine("attribute1.Equals(attribute2) = {0}",attribute1.Equals(attribute2));
   4: Console.WriteLine("attribute1.GetHashCode() == attribute2.GetHashCode() = {0}", attribute1.GetHashCode() == attribute2.GetHashCode());

透過如下的輸出結果我們可以看出這兩個分明具有不同Name屬性值FooAttribute居然被認定為是“相等的”:

   1: attribute1.Equals(attribute2) = True
   2: attribute1.GetHashCode() == attribute2.GetHashCode() = True


三、Attribute物件和Attribute型別的HashCode

實際上兩個FooAttribute物件的HashCode和FooAttribute型別是相等的。為此我們新增了額外兩行程式碼判斷typeof(FooAttribute)和FooAttribute物件的HashCode之間的對等性。

   1: FooAttribute attribute1 = new FooAttribute{ Name = "ABC" };
   2: FooAttribute attribute2 = new FooAttribute{ Name = "123"};
   3: Console.WriteLine("attribute1.Equals(attribute2) = {0}",attribute1.Equals(attribute2));
   4: Console.WriteLine("attribute1.GetHashCode() == attribute2.GetHashCode() = {0}", attribute1.GetHashCode() == attribute2.GetHashCode());
   5: Console.WriteLine("attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode() = {0}",
   6:     attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode());

typeof(FooAttribute)和FooAttribute物件之間對等性可以透過如下的輸出結果看出來:

   1: attribute1.Equals(attribute2) = True
   2: attribute1.GetHashCode() == attribute2.GetHashCode() = True
   3: attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode() = True


四、倘若為FooAttribute新增一個屬性

但是不要以為Attribute的GetHashCode方法總是返回型別本身的HashCode,如果我們在FooAttribute定義一個屬性/欄位,最終的對等性判斷又會不同。為此我們在FooAttribute定義了一個Type屬性。

   1: public class FooAttribute : BaseAttribute
   2: {
   3:     public Type Type {get;set;}
   4: }

然後我們在建立FooAttribute時指定其Type屬性:

   1: FooAttribute attribute1 = new FooAttribute{ Name = "ABC", Type=typeof(string)};
   2: FooAttribute attribute2 = new FooAttribute{ Name = "ABC" , Type=typeof(int)};
   3: Console.WriteLine("attribute1.Equals(attribute2) = {0}",attribute1.Equals(attribute2));
   4: Console.WriteLine("attribute1.GetHashCode() == attribute2.GetHashCode() = {0}", attribute1.GetHashCode() == attribute2.GetHashCode());
   5: Console.WriteLine("attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode() = {0}",
   6:     attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode());

現在具有不同Type屬性值得兩個FooAttribute就不相等了,這可以透過如下所示的輸出結果看出來: 

   1: attribute1.Equals(attribute2) = False
   2: attribute1.GetHashCode() == attribute2.GetHashCode() = False
   3: attribute1.GetHashCode() == typeof(FooAttribute).GetHashCode() = False


五、Attribute的GetHashCode方式是如何實現的?

Attribute的HashCode是由定義在自身型別的欄位值派生,不包括從基類繼承下來的屬性值。如果自身型別不曾定義任何欄位,則直接使用型別的HashCode,這可以透過Attribute的GetHashCode方法的實現看出來,而Equals的邏輯與此類似。

   1: [SecuritySafeCritical]
   2: public override int GetHashCode()
   3: {
   4:     Type type = base.GetType();
   5:     FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
   6:     object obj2 = null;
   7:     for (int i = 0; i 
   8:     {
   9:         object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(this, false, false);
  10:         if ((obj3 != null) && !obj3.GetType().IsArray)
  11:         {
  12:             obj2 = obj3;
  13:         }
  14:         if (obj2 != null)
  15:         {
  16:             break;
  17:         }
  18:     }
  19:     if (obj2 != null)
  20:     {
  21:         return obj2.GetHashCode();
  22:     }
  23:     return type.GetHashCode();
  24: }

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2983/viewspace-2806153/,如需轉載,請註明出處,否則將追究法律責任。

相關文章