最近在读《Effective Java》,认为书中的每一条都十分经典。这回就简单记录一下Item 9。Item 9讲述的是“覆盖equals时总要覆盖hashCode”。我猜测,这条建议同时也会对C#适用。
请看下面这段代码:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Collections;
6: namespace EqualsGetHashCode
7: {
8: class Program
9: {
10: static void Main(string[] args)
11: {
12: Dictionary<PhoneNumber, string> m = new Dictionary<PhoneNumber, string>();
13: m.Add(new PhoneNumber(707,867,5309), "Jenny");
14: Console.WriteLine(m[new PhoneNumber(707, 867, 5309)]==null);
15: Console.ReadKey();
16: }
17: }
18: public class PhoneNumber
19: {
20: public short AreaCode
21: {
22: get;
23: set;
24: }
25: public short Prefix
26: {
27: get;
28: set;
29: }
30: public short LineNumber
31: {
32: get;
33: set;
34: }
35: public PhoneNumber(int areaCode,int prefix,int lineNumber)
36: {
37: RangeCheck(areaCode, 999, "area code");
38: RangeCheck(prefix, 999, "prefix");
39: RangeCheck(lineNumber, 9999, "line number");
40: this.AreaCode = (short)areaCode;
41: this.Prefix = (short)prefix;
42: this.LineNumber = (short)lineNumber;
43: }
44: public static void RangeCheck(int arg, int max, string name)
45: {
46: if (arg < 0 || arg > max)
47: {
48: throw new ArgumentOutOfRangeException("arg", "arg must be less or equal than max");
49: }
50: }
51: public override bool Equals(object obj)
52: {
53: if (obj == this)
54: {
55: return true;
56: }
57: if (typeof(PhoneNumber) != obj.GetType())
58: {
59: return false;
60: }
61: PhoneNumber phoneNumber = obj as PhoneNumber;
62: return phoneNumber.LineNumber == LineNumber
63: && phoneNumber.AreaCode == AreaCode
64: && phoneNumber.Prefix == Prefix;
65: }
66: }
67: }
运行以上代码会抛出异常KeyNotFoundException。从字面来看,添加的PhoneNumber实例和欲检索的PhoneNumber实例是“等同”的,因为各个成员值都是相等的。但是运行时,却抛出异常。当我在用Java实验时,返回的会是null。也就是说未能在容器中找到与检索的实例,根据《Effective Java》便知是hashCode的问题了。既然实例相等,那么它们的哈希值也应该相等(来自Java的Object规范)。在Visual Studio中,发现当你进覆盖Equals方法而未实现GetHashCode方法时,会提示“EqualsGetHashCode.PhoneNumber' overrides Object.Equals(object o) but does not override Object.GetHashCode() ”。从中可以推出,在字典中检索时,是根据哈希值进行比较的。所以GetHashCode没有被覆盖,即使欲检索的实例在字典中存在,但是由于哈希函数没有被重写,最后结果还是不能如您预期。所以我们还要实现GetHashCode方法,而且还要求在语义上要和Equals方法一致。
添加一个GetHashCode方法:
1: public override int GetHashCode()
2: {
3: int result = 17;
4: result = 31 * result + AreaCode;
5: result = 31 * result + Prefix;
6: result = 31 * result + LineNumber;
7: return result;
8: }
现在再运行,发现控制台输出:False。
总结
当你覆盖Equals方法时,一定要覆盖GetHashCode方法,否则当你在使用一些容器类型时,会出现与您预期相违的结果。因为容器类是根据GetHashCode方法来和容易里的键做哈希值比较,所以要覆盖GetHashCode方法。其实这也符合常规,既然两个实例相等了,那它们的哈希值不相等也无道理的吧。所以请记住:当你覆盖Equals方法时,一定要覆盖GetHashCode方法。