1. 程式人生 > >Dictionary使用自定義型別為KEY

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時發生碰撞,導致效能低下。