1. 程式人生 > >C# Dictionary中做Key的類應該注意重寫getHashCode和Equals

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中的定義:

public sealed class String : IComparable, ICloneable, IConvertible,  
    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>介面的定義如下所示:

public interface 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;
        }
    }