C# Dictionary中做Key的類應該注意重寫getHashCode和Equals
下面的內容中有很多一部分是筆者自己的想法,所以有些說法可能會有失偏頗,還望指正。
Wanger說GetHashCode()是他在Effective C#所有的50個建議中唯一一項關於不推薦函式的建議。GetHashCode()這個方法只會用於一個地方:給基於Hash的Collection(比如HashTable和Dictionary)的Key定義Hash值,也就是物件做為Key,而物件的GetHashCode()為該Key獲取Hash值。
說到Hash,先說一下Hash查詢演算法。
我們知道大部分的查詢演算法,順序查詢,二分查詢或者是B-Tree查詢,由於查詢的關鍵字和待查詢的記錄地址之間沒有必然的聯絡,都是基於比較的,無非是在比較的次數上有多有少。最理想的演算法當然是不比較,能夠直接通過關鍵字得到待查詢的記錄的地址,這就是Hash查找了。通過Hash函式,將記錄的Key值直接對應到記錄的地址,一個Key對應一個地址,由於Key值是某種語言中所有允許標誌符的全集,比某個程式中可能用到的地址空間大很多,這樣就勢必會有不同的Key值對應相同地址的情況,良好的Hash演算法要求,經過Hash函式運算由Key得到的地址空間是均勻的,也就是地址空間是隨機的。但是不可能完全避免上述情況,於是又有各種偵探和解決地址衝突的演算法。
OK,上面的內容是資料結構的基礎知識,大家應該都知道,不過做為一個完整的讀後感,不提這些好像不完整,如果熟知的話,可以略過以上內容。
對應到HashTable 中,自定義的類就是Key,而GetHashCode()就是那個Hash演算法,GethHashCode()得到的值就是HashTable儲存的物件的記憶體地址。
於是就有Wanger提到的一個良好的GetHashCode()必須滿足的三個條件。
1.兩個物件Equals,則通過GetHashCode()得到的值必須相同。
物件做為Key值,必須對應一個Value物件,如果得到的值不同,則會出現一個Key對應多個Value物件的情況,這違背了Key的原始意義,不過如果你非要違背這個原則,在HashTable的語法中也是沒有任何問題的(這個在後面會舉例論述)只不過違背了Key的語意。
2.通過GetHashCode()得到的值必須是恆定不變的。
這個很明顯,如果在儲存以後這個值可以隨意變動,在通過Key取Value的時候就會有問題,在語法上會報經典的“未將物件引用設定到物件例項”。
3.通過GetHashCode()得到的值必須在整數的取值範圍內是均勻分佈的。
這樣做的目的是提高HashTable的查詢效率。
Wanger隨後給出了Object的GetHashCode()和ValueType的GetHashCode()的演算法,以及是否滿足上述三條原則。
下面簡要敘述一下,有興趣的可以看一下原著。
Object,預設的Equals()是通過Object建立時生成的Identity來比較的,而GetHashCode()是從1開始遞增的序列值,所以如果Equals相等,則GetHashCode()必然相等。當然第二條也能滿足,因為沒辦法修改這個GetHashCode的值。第三條就不能滿足了,除非是建立大量的Object。
ValueType,Equals()是通過比較各個欄位的值,而GetHashCode()是通過比較第一個欄位的值來實現的,這樣也能保證第一條。第二條就不一定能保證了,如果第一個欄位不是Readonly的,那由它得到的HashCode也是變化的。第三條規則取決於第一個欄位怎麼使用。
下面舉個簡單的例子。
/// <summary>
/// 做為鍵的類
/// </summary>
class keyClass
{
private string name;
public string _name
{
get
{
return name;
}
set
{
name = value;
}
}
private string code;
public string _code
{
get
{
return code;
}
set
{
code = value;
}
}
public override bool Equals(object obj)
{
if(null == obj)
return false;
if(obj.GetType()!=this.GetType())
return false;
return(((keyClass)obj)._name.Equals(this._name));
}
public override int GetHashCode()
{
return this._code.GetHashCode();
}
}
測試類
/// <summary>
/// 測試鍵值的Hashtable
/// </summary>
class HashTableTest
{
/// <summary>
/// 應用程式的主入口點。
/// </summary>
[STAThread]
static void Main(string[] args)
{
keyClass testKey = new keyClass();
testKey._code = "110";
testKey._name = "222";
keyClass testKey2 = new keyClass();
testKey2._code = "111";
testKey2._name = "222";
System.Collections.Hashtable aa = new Hashtable();
aa.Add(testKey,"test");
aa.Add(testKey2,"test2");
Console.WriteLine(aa[testKey].ToString());
Console.WriteLine(aa[testKey2].ToString());
Console.ReadLine();
}
}
單步跟蹤一下上述程式碼就會發現HashTable建立和查詢的過程。
建立:
首先根據鍵物件的GetHashTable()(當然在建立HashTable的時候可以制定其他的Hash函式做為定址函式,這裡不予討論)得到HashCode,如果在儲存桶中該地址沒有被佔用,則將其存入其中,如果佔用了則呼叫當前Key物件的Equals方法判斷佔用該地址的物件跟當前Key物件是否是同一物件,如果是則丟擲異常,說該項已經存在於HashTable中(我不知道是否有辦法在儲存桶中儲存兩個HasCode和Key值都相同的物件,不過理論上應該是不允許的)。如果不是同一物件則在儲存桶中另外找個地方把物件存起來。
查詢
首先根據鍵物件的GetHashTable()(當然在建立HashTable的時候可以制定其他的Hash函式做為定址函式,這裡不予討論)得到HashCode,然後將儲存桶中對應的key物件跟當前的Key值通過Equals方法比較,看是否為同一物件,如果不同則繼續查詢。
靠,這不就是Hash演算法的過程嘛!
對啊,我也沒說不是啊。
那你直接說跟Hash的查詢的演算法一樣不就行了
唉,不是要湊篇幅嘛!
自從.NET Framework 2.0引入泛型之後,對集合的使用就開創了新的局面。首先我們不用考慮型別是否安全,利用泛型以及對泛型引數的約束完全可以保障這一點;其次,集合元素不會因為頻繁的Boxing和Unboxing而影響集合遍歷與操作的效能。泛型帶來的這兩點好處毋庸置疑。在Dictionary<TKey, TValue>中,除了字串,我們普遍會使用值型別作為它的key,例如int型別。而列舉型別作為一種值型別,在某些時候特別是需要位操作的時候,也會經常用作key。問題就出現在這裡。
我們知道,Dictionary的key必須是唯一的標識,因此Dictionary需要對key進行判等的操作,如果key的型別沒有實現 IEquatable介面,則預設根據System.Object.Equals()和GetHashCode()方法判斷值是否相等。我們可以看看常用作key的幾種型別在.NET Framework中的定義:
IComparable<string>, IEnumerable<string>, IEnumerable,
IEquatable<string>
public struct Int32 : IComparable, IFormattable,
IConvertible, IComparable<int>, IEquatable<int>
public abstractclass Enum : ValueType,
IComparable, IFormattable, IConvertible
注意Enum型別的定義與前兩種型別的不同,它並沒有實現IEquatable介面。因此,當我們使用Enum型別作為key值時,Dictionary的內部操作就需要將Enum型別轉換為System.Object,這就導致了Boxing的產生。沒錯,我們很難發現這個陷阱,它是導致Enum作為 key值的效能瓶頸。
我們該如何解決這一問題?最簡單的方法是將Enum的值先轉換為int,然後將其作為key傳入Dictionary中。還有一種作法是定義一個實現了IEqualityComparer<T>介面的類。因為Dictionary建構函式的其中一個過載版本,可以接收 IEqualityComparer<T>型別,通過它完成對key的判斷。IEqualityComparer<T>介面的定義如下所示:
{
bool Equals(T x, T y);
int GetHashCode(T obj);
}
遺憾的是我們卻不能直接提供針對Enum的實現,例如:
class EnumComparer<TEnum> : IEqualityComparer<TEnum>{
public bool Equals(TEnum x, TEnum y)
{
return (x == y);
}
public int GetHashCode(TEnum obj)
{
return (int)obj;
}
}
因為我們不能直接對泛型型別進行==操作,以及將泛型物件強制轉換為int型別。在Code Project上,有一篇名為Accelerating Enum-Based Dictionaries with Generic EnumComparer的文章,利用Reflection.Emit實現Equals()和GetHashCode()方法。不過在該文的評論中,提供了更好的一個方法,就是利用C# 3.0的Lambda表示式:
public class EnumComparer<T> : IEqualityComparer<T> where T : struct{
public bool Equals(T first, T second)
{
var firstParam = Expression.Parameter(typeof(T),"first");
var secondParam = Expression.Parameter(typeof(T),"second");
var equalExpression = Expression.Equal(firstParam, secondParam);
return Expression.Lambda<Func<T, T, bool>>
(equalExpression, new[] { firstParam, secondParam }).
Compile().Invoke(first, second);
}
public int GetHashCode(T instance)
{
var parameter = Expression.Parameter(typeof(T),"instance");
var convertExpression = Expression.Convert(parameter, typeof(int));
return Expression.Lambda<Func<T, int>>
(convertExpression, new[]{parameter}).
Compile().Invoke(instance);
}
}
此時,我們就可以如此使用Dictionary物件:
public enum DayOfWeek{//...}var dictionary = new Dictionary<DayOfWeek, int>(new EnumComparer<DayOfWeek>());
採取這樣的做法比直接用Enum型別作為Dictionary的key差不多要快8倍。這難道不讓人為之驚詫嗎?
class LongArray
{
public long[] array;
public LongArray(long[] arrays)
{
array = arrays;
}
public override int GetHashCode()
{
int Result = 0;
for (var i = 0; i < array.Length; i++)
{
Result += (i + 1) * (int)array[i];
}
return Result;
}
public override bool Equals (Object Arrays) //重寫Equals方法
{
var data = ((LongArray)Arrays).array;
if (data.Length == array.Length)
{
for (var i = 0; i < array.Length; i++)
{
if (data[i] == array[i])
{
continue;
}
else
{
return false;
}
}
return true;
}
return false;
}
}