手记

为什么GetHashCode方法需要如此设计?

昨天我在实现《通过扩展改善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<FooAttribute>().ToList<FooAttribute>();
   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 < fields.Length; 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: }
0人推荐
随时随地看视频
慕课网APP