Dictionary作为字典进行索引取值时的效率相比List的遍历查找要好很多,然而有时淡出的int,string等作为关键值索引并不够使用,需要自定义类型来作为KEY,对于自定义的类型作为KEY,要求重写两个方法,分别是Equals和GetHashCode。关于这两个,尤其是GetHashCode转载一篇文章分享。
原文地址还要谈谈Equals和GetHashcode,感谢原作者提供好文章
一.两个逻辑上相等的实例对象。
两个对象相等,除了指两个不同变量引用了同一个对象外,更多的是指逻辑上的相等。什么是逻辑上相等呢?就是在一定的前提上,这两个对象是相等的。比如说某男生叫刘益红,然后也有另外一个女生叫刘益红,虽然这两个人身高,爱好,甚至性别上都不相同,但是从名字上来说,两者是相同的。Equals方法通常指的就是逻辑上相等。有些东西不可比较,比如说人和树比智力,因为树没有智力,所以不可比较。但是可以知道人和树不相等。
二.Object的GetHashcode方法。
计算Hashcode的算法中,应该至少包含一个实例字段。Object中由于没有有意义的实例字段,也对其派生类型的字段一无所知,因此就没有逻辑相等这一概念。所以默认情况下Object的GetHashcode方法的返回值,应该都是独一无二的。利用Object的GetHashcode方法的返回值,可以在AppDomain中唯一性的标识对象。
下面是.Net中Object代码的实现:
[Serializable] public class Object { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] public Object() { } public virtual string ToString() { return this.GetType().ToString(); } [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual bool Equals(object obj) { return RuntimeHelpers.Equals(this, obj); } [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool Equals(object objA, object objB) { return objA == objB || (objA != null && objB != null && objA.Equals(objB)); } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public static bool ReferenceEquals(object objA, object objB) { return objA == objB; } [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] public virtual int GetHashCode() { return RuntimeHelpers.GetHashCode(this); } [SecuritySafeCritical] [MethodImpl(MethodImplOptions.InternalCall)] public extern Type GetType(); [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Finalize() { } [SecuritySafeCritical] [MethodImpl(MethodImplOptions.InternalCall)] protected extern object MemberwiseClone(); [SecurityCritical] private void FieldSetter(string typeName, string fieldName, object val) { FieldInfo fieldInfo = this.GetFieldInfo(typeName, fieldName); if (fieldInfo.IsInitOnly) { throw new FieldAccessException(Environment.GetResourceString("FieldAccess_InitOnly")); } Message.CoerceArg(val, fieldInfo.FieldType); fieldInfo.SetValue(this, val); } private void FieldGetter(string typeName, string fieldName, ref object val) { FieldInfo fieldInfo = this.GetFieldInfo(typeName, fieldName); val = fieldInfo.GetValue(this); } private FieldInfo GetFieldInfo(string typeName, string fieldName) { Type type = this.GetType(); while (null != type && !type.FullName.Equals(typeName)) { type = type.BaseType; } if (null == type) { throw new RemotingException(string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("Remoting_BadType"), new object[] { typeName })); } FieldInfo field = type.GetField(fieldName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); if (null == field) { throw new RemotingException(string.Format(CultureInfo.CurrentCulture, Environment.GetResourceString("Remoting_BadField"), new object[] { fieldName, typeName })); } return field; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
为什么会有Hashcode?
Hashcode是为了帮助计算出该对象在hashtable中所处的位置。而能够把一个对象放入hashtable中无疑是有好处的。
这是Hashcode的作用,但是我们为什么需要他?
因为一个类型在定义了Equals方法后,在System.Collections.Hashtable类型,System.Collections.Generic.Dictionary类型以及其他一些集合的实现中,要求如果两个对象相等,不能单单只看Equals方法返回true,还必须要有相同的Hashcode.
这相当于一种前提条件的假设,而上述这些类型就是基于这种假设的基础上实现的。如果不遵守这些条件,那么在使用这些集合的时候就会出问题。
下面是摘自MSDN的一段描述
“Hashcode是一个用于在相等测试过程中标识对象的数值。它还可以作为一个集合中的对象的索引。 GetHashCode方法适用于哈希算法和诸如哈希表之类的数据结构。 GetHashCode 方法的默认实现不保证针对不同的对象返回唯一值。而且,.NET Framework 不保证 GetHashCode 方法的默认实现以及它所返回的值在不同版本的 .NET Framework 中是相同的。因此,在进行哈希运算时,该方法的默认实现不得用作唯一对象标识符。”
上面这段话想说明的就是:两个对象相等,hashcode也应该相等。但是两个对象不等,hashcode也有可能相等。当对象不相等但是hashcode相等的时候,就叫做hash冲突。
下面这两个不同的string对象就产生了相同的hashcode:
string str1 = "NB0903100006"; string str2 = "NB0904140001"; Console.WriteLine(str1.GetHashCode()); Console.WriteLine(str2.GetHashCode());
- 1
- 2
- 3
- 4
这是因为string类型重写了Object的GetHashcode方法,如下:
public override int GetHashCode() { unsafe { fixed (char *src = this) { Contract.Assert(src[this.Length] == '\0', "src[this.Length] == '\\0'"); Contract.Assert( ((int)src)%4 == 0, "Managed string should start at 4 bytes boundary");#if WIN32 int hash1 = (5381<<16) + 5381; #else int hash1 = 5381;#endif int hash2 = hash1;#if WIN32 // 32bit machines. int* pint = (int *)src; int len = this.Length; while(len > 0) { hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; if( len <= 2) { break; } hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1]; pint += 2; len -= 4; } #else int c; char *s = src; while ((c = s[0]) != 0) { hash1 = ((hash1 << 5) + hash1) ^ c; c = s[1]; if (c == 0) break; hash2 = ((hash2 << 5) + hash2) ^ c; s += 2; }#endif #if DEBUG // We want to ensure we can change our hash function daily. // This is perfectly fine as long as you don't persist the // value from GetHashCode to disk or count on String A // hashing before string B. Those are bugs in your code. hash1 ^= ThisAssembly.DailyBuildNumber; #endif return hash1 + (hash2 * 1566083941); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
归根结底,因为hashcode本来就是为了方便我们计算位置用的,本意并不是用来判断两个对象是否相等,这工作还是要交给Equals方法来完成。总而言之,保持两者的一致性是最好的做法。
所以.NET中定义了一个比较相等性的接口IEqualityComparer,就包含了Equals、GetHashCode这两种方法。Dictionary
class BoxEqualityComparer : IEqualityComparer<Box>{ public bool Equals(Box b1, Box b2) { if (b1.Height == b2.Height & b1.Length == b2.Length & b1.Width == b2.Width) { return true; } else { return false; } } public int GetHashCode(Box bx) { int hCode = bx.Height ^ bx.Length ^ bx.Width; return hCode.GetHashCode(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在构造Dictionarty时如果不传递实现了这个接口的对象,那么就会使用EqualityComparer.Default。具体使用的是哪个,还是要看我们用做key的那个对象实现了哪个接口。
比如
struct MyKey : IEquatable<MyKey> {}//这个方法会被调用 public bool Equals(MyKey that) { }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
和下面这个,生成的就是不同的实例
struct MyKey {}//这个方法会被调用 public bool Equals(Object that) { }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
两个拥有相同Hashcode的对象,只能说是有可能是相等的。而可能性就取决你的Hash函数是怎么实现的了。实现得越好,相等的可能性越大,相应的Hashtable性能就越好。这是因为放置在同一个Hash桶上的元素可能性就越小,越少可能发生碰撞。
可以想象,最烂的Hashcode的实现方法无疑就是返回一个写死的整数,这样Hashtable很容易就被迫转换成链表结构。这是查找的时间复杂度就变味O(n)
public override int GetHashCode() { return 31; }
- 1
- 2
- 3
- 4
一个好的hash函数通常意味着尽量做到“为不相等的对象产生不相等的hashcode”,但是不要忘记”相同的对象必须有相同的hashcode”。一个是尽量做到,一个是必须的。
不相等的对象有相同的hashcode只是影响性能,而相同的对象(Equals返回true)没有相同的hashcode就会破坏整个前提条件。
因此,计算hashcode的时候要避免使用在实现Equals方法中没有使用的字段,否则也可能出现Equals为true,但是hashcode却不相等的情况。
三.逻辑上相等但是完全不同的实例
正如同1中所举的例子一样,两人同名,但是两人并不是同一个人。如上所述一般情况下我们判断两个对象是否相等使用的是Equals方法,但是在一些数据结构里面,判断两个对象是否相同,却采用的是hashcode。比如说Dictionnary,这时候如果没有重写GetHashcode方法,就会产生问题。
简单的描述一下整个过程:
1.在一个基于hashtable这种数据结构的集合中,添加一个key/value pair的时候,首先会获取key对象的hashcode,而这个hashcode指出这个key/value pair应该放在数组的那个位置上。
2.当我们在集合中查找一个对象是否存在时,会先获取指定对象的hashcode,而这个hashcode就是当初用来计算出存放对象的位置的。因此如果hashcode发生了改变,那么你也没办法找到先前存放的对象,因为你计算出来的数组下标是错误的。
在没有重写GetHashCode方法的情况下,这个方法继承自Object,而Object的实现就是每一个New出来的对象GetHashCode返回的值都应该不一样。
举例:
public class Staff { private readonly string ID; private readonly string name; public Staff(string ID, string name) { this.ID = ID; this.name = name; } public override bool Equals(object obj) { if (obj == this) return true; if (!(obj is Staff)) return false; var staff = (Staff)obj; return name == staff.name && ID == staff.ID; } } public class HashtableTest { public static void Main(){ Staff a = new Staff("123", "langxue"); Staff b = new Staff("123", "langxue"); Console.WriteLine(a.Equals(b)); //返回true var dic = new Dictionary<Staff, int>(); dic.Add(new Staff("123", "langxue"), 0213); Console.WriteLine(dic.ContainsKey(new Staff("123", "langxue"))); //返回false } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
这时,我们就要重写hashcode方法,常见的就是XOR方式(先“或”然后取反):
public struct Point { public int x; public int y; //other methods public override int GetHashCode() { return x ^ y; }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
当然,我们在这里可以直接使用.NET框架中帮string类型重写的GetHashcode方法:
public override int GetHashCode() { return (ID + name).GetHashCode(); }
- 1
- 2
- 3
- 4
重写后的代码如下:
public class Staff { private readonly string ID; private readonly string name; public Staff(string ID, string name) { this.ID = ID; this.name = name; } public override bool Equals(object obj) { if (obj == this) return true; if (!(obj is Staff)) return false; var staff = (Staff)obj; return name == staff.name && ID == staff.ID; } public override int GetHashCode() { return (ID + name).GetHashCode(); } } public class HashtableTest { public static void Main(){ Staff a = new Staff("123", "langxue"); Staff b = new Staff("123", "langxue"); Console.WriteLine(a.Equals(b)); var dic = new Dictionary<Staff, int>(); dic.Add(new Staff("123", "langxue"), 0213); Console.WriteLine(dic.ContainsKey(new Staff("123", "langxue"))); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
四.一些推荐做法:
1.不要试图从hash code中排除一个对象的某些关键字段来提高性能。
这就相当于把限制条件放宽,使得对象间的区别不那么明显,最终导致hash函数计算出来的hashcode相等,使得放入hashtable时发生碰撞,导致性能低下。