为什么System.Attribute的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());
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: }