Dictionary使用自定義型別為KEY
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;
}
}
為什麼會有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());
這是因為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);
}
}
}
歸根結底,因為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();
}
}
在構造Dictionarty時如果不傳遞實現了這個介面的物件,那麼就會使用EqualityComparer.Default。具體使用的是哪個,還是要看我們用做key的那個物件實現了哪個介面。
比如
struct MyKey : IEquatable<MyKey>
{
}
//這個方法會被呼叫
public bool Equals(MyKey that)
{
}
和下面這個,生成的就是不同的例項
struct MyKey
{
}
//這個方法會被呼叫
public bool Equals(Object that)
{
}
兩個擁有相同Hashcode的物件,只能說是有可能是相等的。而可能性就取決你的Hash函式是怎麼實現的了。實現得越好,相等的可能性越大,相應的Hashtable效能就越好。這是因為放置在同一個Hash桶上的元素可能性就越小,越少可能發生碰撞。
可以想象,最爛的Hashcode的實現方法無疑就是返回一個寫死的整數,這樣Hashtable很容易就被迫轉換成連結串列結構。這是查詢的時間複雜度就變味O(n)
public override int GetHashCode()
{
return 31;
}
一個好的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
}
}
這時,我們就要重寫hashcode方法,常見的就是XOR方式(先“或”然後取反):
public struct Point {
public int x;
public int y;
//other methods
public override int GetHashCode() {
return x ^ y;
}
}
當然,我們在這裡可以直接使用.NET框架中幫string型別重寫的GetHashcode方法:
public override int GetHashCode()
{
return (ID + name).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 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.不要試圖從hash code中排除一個物件的某些關鍵欄位來提高效能。
這就相當於把限制條件放寬,使得物件間的區別不那麼明顯,最終導致hash函式計算出來的hashcode相等,使得放入hashtable時發生碰撞,導致效能低下。