建议12: 重写Equals时也要重写GetHashCode

转载 2016年08月30日 17:10:21

建议12: 重写Equals时也要重写GetHashCode

除非考虑到自定义类型会被用作基于散列的集合的键值;否则,不建议重写Equals方法,因为这会带来一系列的问题。

如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息:

“重写 Object.Equals(object o)但不重写 Object.GetHashCode()”

如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug。还是针对上一个建议中的Person进行编码,代码如下所示:

  1. static Dictionary<Person, PersonMoreInfo> PersonValues = new Dictionary<Person,  
  2.     PersonMoreInfo>();  
  3. static void Main(string[] args)  
  4. {  
  5.     AddAPerson();  
  6.     Person mike = new Person("NB123");  
  7.     //Console.WriteLine(mike.GetHashCode());  
  8.     Console.WriteLine(PersonValues.ContainsKey(mike));  
  9. }  
  10.  
  11. static void AddAPerson()  
  12. {  
  13.     Person mike = new Person("NB123");  
  14.     PersonMoreInfo mikeValue = new PersonMoreInfo() { SomeInfo = "Mike's info" };  
  15.     PersonValues.Add(mike, mikeValue);  
  16.     //Console.WriteLine(mike.GetHashCode());  
  17.     Console.WriteLine(PersonValues.ContainsKey(mike));  

本段代码的输出将会是:

  1. True  
  2. False 

理论上来说,在上一个建议中我们已经重写了Person的Equals方法;也就是说,在AddAPerson方法中的mike和Main方法中的mike属于“值相等”。于是,将该“值”作为key放入Dictionary中,再在某处根据mike将mikeValue取出来,这会是理所当然的事情。可是,从上面的代码段中我们发现,针对同一个示例,这种结论是正确的,若是针对不同的mike示例,这种结果就有问题了。

基于键值的集合(如上面的Dictionary)会根据Key值来查找Value值。CLR内部会优化这种查找,实际上,最终是根据Key值的HashCode来查找Value值。代码运行的时候,CLR首先会调用Person类型的GetHashCode,由于发现Person没有实现GetHashCode,所以CLR最终会调用Object的GetHashCode方法。将上面代码中的两行注释代码去掉,运行程序得到输出,我们会发现,Main方法和AddAPerson方法中的两个mike的HashCode是不同的。这里需要解释为什么两者实际对应调用的Object.GetHashCode会不相同。

Object为所有的CLR类型都提供了GetHashCode的默认实现。每new一个对象,CLR都会为该对象生成一个固定的整型值,该整型值在对象的生存周期内不会改变,而该对象默认的GetHashCode实现就是对该整型值求HashCode。所以,在上面代码中,两个mike对象虽然属性值都一致,但是它们默认实现的HashCode不一致,这就导致Dictionary中出现异常的行为。若要修正该问题,就必须重写GetHashCode方法。Person类的一个简单的重写可以是如下的形式:

  1. public override int GetHashCode()  
  2. {  
  3.     return this.IDCode.GetHashCode();  

此时再运行本条建议开始时代码的输出,就会发现两者的HashCode是一致的,而Dictionary也会找到相应的键值,输出:True。

细心的读者可能已经发现一个问题,Person类的IDCode属性是一个只读属性。从语法特性本身来讲,可以将IDCode设置为可写;然而从现实的角度考虑,一个“人”一旦踏入社会,其IDCode就不应该改变,如果要改变,就相当于是另外一个人了。所以,我们应该只实现该IDCode的只读属性。同理,GetHashCode方法也应该基于那些只读的属性或特性生成HashCode。

GetHashCode方法还存在另外一个问题,它永远只返回一个整型类型,而整型类型的容量显然无法满足字符串的容量,以下的例子就能产生两个同样的HashCode。

  1. string str1 = "NB0903100006";  
  2. string str2 = "NB0904140001";  
  3. Console.WriteLine(str1.GetHashCode());  
  4. Console.WriteLine(str2.GetHashCode()); 

为了减少两个不同类型之间根据字符串产生相同的HashCode的几率,一个稍作改进版本的GetHashCode方法如下:
  1. public override int GetHashCode()  
  2. {  
  3.     return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.  
  4.         FullName + "#" + this.IDCode).GetHashCode();  

注意 重写Equals方法的同时,也应该实现一个类型安全的接口IEquatable,所以Person类型的最终版本应该如下所示:

  1. class Person : IEquatable<Person> 
  2. {  
  3.     public string IDCode { get; private set; }  
  4.  
  5.     public Person(string idCode)  
  6.     {  
  7.         this.IDCode = idCode;  
  8.     }  
  9.  
  10.     public override bool Equals(object obj)  
  11.     {  
  12.         return IDCode == (obj as Person).IDCode;  
  13.     }  
  14.  
  15.     public override int GetHashCode()  
  16.     {  
  17.         return (System.Reflection.MethodBase.GetCurrentMethod().
  18. DeclaringType.FullName + "#" + this.IDCode).GetHashCode();  
  19.     }  
  20.  
  21.     public bool Equals(Person other)  
  22.     {  
  23.         return IDCode == other.IDCode;  
  24.     }  


转自:《编写高质量代码改善C#程序的157个建议》陆敏技

相关文章推荐

建议14: 正确实现浅拷贝和深拷贝

建议14: 正确实现浅拷贝和深拷贝 为对象创建副本的技术称为拷贝(也叫克隆)。我们将拷贝分为浅拷贝和深拷贝。 浅拷贝 将对象中的所有字段复制到新的对象(副本)中。其中,值类型字段的值被复制到副本中...
  • houwc
  • houwc
  • 2016-08-31 10:38
  • 146

改善C#程序的50种方法

为什么程序已经可以正常工作了,我们还要改变它们呢?答案就是我们可以让它们变得更好。我们常常会改变所使用的工具或者语言,因为新的工具或者语言更富生产力。如果固守旧有的习惯,我们将得不到期望的结果。对于C...

建议13: 为类型输出格式化字符串(1)

建议13: 为类型输出格式化字符串(1) 有两种方法可以为类型提供格式化的字符串输出。一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable。这对类型来说,是一种主动实现...
  • houwc
  • houwc
  • 2016-08-30 17:25
  • 163

建议11: 区别对待==和Equals

建议11: 区别对待==和Equals 在开始本建议之前,首先要明确概念“相等性”。CLR中将“相等性”分为两类:“值相等性”和“引用相等性”。如果用来比较的两个变量所包含的数值相等,那么将其定义为...
  • houwc
  • houwc
  • 2016-08-30 15:36
  • 225

建议13: 为类型输出格式化字符串(2)

建议13: 为类型输出格式化字符串(2) 一个典型的格式化器应该继承接口IFormatProvider和ICustomFomatter,所以应该像下面这样调用格式化器: Person person...
  • houwc
  • houwc
  • 2016-08-31 10:31
  • 161

Tip12 重写Equals时也要重写GetHashCode

Tip12 重写Equals时也要重写GetHashCode 如果重写Equals方法但不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug。例如:...

为什么重写equals方法的同时也要重写hashcode方法?

参考链接: http://www.iteye.com/problems/23334 http://www.iteye.com/topic/257191 第一个链接 首先说建议的...

斗地主原型:equals要重写.同时也要重写出:hashCode

学习常量:public static final 后要大写字母. public class Card { /** * 打牌案例 :斗地主 toString():返回对象文本描述 ...

C#学习笔记第四篇之Equals,GetHashCode ,ToString函数深度剖析(一)

 学JAVA的时候也是经常碰到这三个函数,因为不是经常用,所以也不熟悉具体有什么用,有次看视频,看到一个老师讲到前两个函数,有大致的印象,最近学C#的时候又碰到了这三个函数,正好碰上要学习C#中...

C#学习笔记第四篇之Equals,GetHashCode ,ToString函数深度剖析(二)

 C#学习笔记第四篇之Equals,GetHashCode ,ToString函数深度剖析(二)C#学习笔记第四篇之Equals,GetHashCode ,ToString函数深度剖析(二)
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)