前置知識
在看本文之前,要先知道什麼是 Value Type (實質型別) 和 Reference Type (參考型別),不熟的朋友可以參考這篇文章
隱含繼承 Object 類別
微軟官方教學文件有提到,所有的型別都隱含繼承自 Object 類別。
因此用所有型別建立出來的物件都可以使用從 Object 繼承來的方法,如下圖
Object 的 Equals 方法
宣告一個 Data 類別,並分別建立出兩個物件給 data1 和 data2 變數,這裡刻意將欄位都填入相同的值,再用繼承自 Object 的 Equals 方法比較兩個物件是否相等。
因為是 Reference Type (參考型別),雖然 data1 和 data2 所有欄位的值都相同,但兩個變數在 Stack 內儲存的記憶體位址的值是不同的,因此 Equals 回傳的結果為 false。
覆寫 Equals 方法
如果我們希望將比對條件改為物件內欄位的值,可以覆寫 Equals 方法,自訂比對的條件,例如希望這個物件的 Equals 要同時比對 Id 和 Name 兩個欄位的值,可以這樣寫:
自訂比對的條件,覆寫 Equals 方法:
- 若傳入 Equals 的物件參數(要比對的對象)不是 Data 型別,回傳 false。
- 若傳入的物件參數是 Data 型別,比對 Id 和 Name 兩個欄位的值是否相等,並回傳比對結果。
此時 IDE 在 Data 類別的宣告處出現綠色波浪提示訊息,提示我們覆寫 Equals 方法時,也要覆寫 GetHashCode 方法。
什麼是 GetHashCode?為什麼要覆寫這個方法?在說明前,先科普什麼是「雜湊表」。
關於雜湊表
資料結構中有一種組織資料的方式稱為「雜湊表」,在 C# 語言中 HashSet<T>、Dictionary<TKey, TValue>、…等集合類別就是用雜湊表實現的。
可以把雜湊表想像成很多個桶子,每個桶子都有唯一的識別編號,而且每個桶子都可放入不止一件物品。
這種資料結構的優點是,當我們知道物品所在桶子的編號,只要先找到指定編號的桶子,再到這個桶子裡去找,一定會找到我們要的東西,找東西的速度會很快。比起每個桶子都打開來找,會省下不少時間。
雜湊表的概念中,把每個物品平均分配到不同編號的桶子是很重要的,如果把所有東西通通放進同一個編號的桶子,找東西時雖然只打開這一個桶子,卻還要每個物品逐一巡過比對才能找到確定要的東西,那用雜湊表就沒有意義了,不如放到陣列或 List 裡用迴圈比對。
雜湊表的概念在現實生活中的應用:
- 圖書館的書本分類存放方式
書籍分門別類放在不同編號的書櫃,在相同編號的書櫃裡,放著許多相同分類的書。
找書的時候,依分類先找到該編號的書櫃,再到該編號的書櫃即可找到需要的書(不用每一櫃都找)。 - 書本、字典的索引頁
索引頁會列出每個關鍵字詞所在的頁數,不同字詞可能會出現在相同的頁數。
找內容時,先找到關鍵字詞所在的頁數,直接翻到該頁數即可找到指定字詞的內文(不用從第一頁找到最後一頁)。
覆寫 GetHashCode 方法
從上面的說明可知,雜湊表會用到雜湊值,GetHashCode 就是產生雜湊值的方法,將物件放進這個雜湊值的桶子裡,需要時,先呼叫物件的 GetHashCode 得到雜湊值,在雜湊表找出這個雜湊值的桶子,從桶子裡取出這個物件,速度就加快了。
如果物件不放入雜湊表,則 GetHashCode 方法用不到,但我們無法預期別人會如何使用這個物件,所以在覆寫 Equals 方法時,先將 GetHashCode 寫好就很重要了。
上圖可看到 data1 和 data2 即使欄位的值都相同,但兩個物件是不同執行個體 (不同 new Data 所產生的物件),繼承自 Object 的 GetHashCode 方法就會產生不同的雜湊值。
但前面我們覆寫 Equals 方法比較是否相等的條件,比對的是 Id 和 Name 兩個欄位的值,在 data1 和 data2 的內容都相同的情況下,我們希望他的雜湊值也要一樣,此時可以覆寫 GetHashCode 方法,用自訂的條件去產生雜湊值(如下圖)。
上面在 GetHashCode 方法用到一個實用的技巧是大神 Joey Chen 教的,借用匿名型別實作的 GetHashCode 方法,在 new 匿名型別時,只要傳入欄位的值都一樣,產生出來的雜湊值就會相同。
C# 用雜湊表實現的集合類別
在 C# 語言中,用雜湊表實現的集合類別有很多種,例如 HashSet<T>、Dictionary<TKey, TValue>、SortedList<TKey, TValue>、HashTable、SortedList、…等。
當 GetHashCode 已存在雜湊表內,且 Equals 回傳為 true (物件比對相等),表示新加入的物件已經存在雜湊表內,不再重複加入。
- 在 HashSet 加入重複的物件會略過,只會加入唯一不重複的物件進去。
- 在 Dictionary 加入重複的物件會發生例外。
接下來用 Data 類別建立 data1、data2 兩個物件,並將這兩個物件加入到 HashSet<Data> 內,試試以下不同情境,看看會發生什麼事。
用 Object 的 GetHashCode 方法產生雜湊碼。
data1、data2 兩個物件的 GetHashCode 都不同,所以如預期的,兩個物件都會加入到 HashSet<Data> 中。用自訂的 GetHashCode 方法產生雜湊碼,條件是用 Id + Name 欄位的值產生。
data1、data2 是兩個執行個體,但我們讓兩個物件的 GetHashCode 的值相同,在加入到雜湊表集合時,會先呼叫 GetHashCode 檢查雜湊表內是否存已存在這組雜湊碼,接著:- 若雜湊碼不存在,將物件加入雜湊表
data1.GetHashCode() 不存在雜湊表,將 data1 物件加入。 - 若雜湊碼已存在,取出表內雜湊碼相同的所有物件,逐一呼叫物件的 Equals 方法和新加入物件相比是否相同,當回傳為 false 才加入雜湊表。
data1.Equals(data2) == true,因此 data2 不會加入雜湊表。
此例最終只有 data1 會加入到 hashSet 中。
- 若雜湊碼不存在,將物件加入雜湊表
同樣的情形,在 Dictionary 會出現例外
什麼情況會遇到 GetHashCode 相同,但 Equals 為 false 的情況?
當 GetHashCode、Equals 兩個方法使用不同欄位條件時,就會遇到。
如下圖,GetHashCode 只用 Id 欄位去產生,但 Equals 是用 Id + Name 欄位的值比對,此時三個 Data 物件都會加入到雜湊表集合內,並且都放在同一個雜湊碼的桶子裡。
我們在 Equals 和 GetHashCode 方法內加入 Console.WriteLine() 觀察雜湊表取資料的過程,當遇到相同 GetHashCode 時,會取出同一個桶子內的所有資料,再逐一呼叫 Equals 方法比對,最後取出比對結果相符的物件。
在這個案例,沒有發揮出雜湊表的優勢。
總結
比較物件是否相等,用的是 Equals 方法,覆寫此方法可自訂比對的條件。
雜湊表會用到 GetHashCode 方法,因為無法預期別人是否會將物件用於雜湊表,建議可先依據 Equals 的條件覆寫 GetHashCode 方法,避免將來在雜湊表使用此物件時遇到非預期的結果。
加到雜湊表的物件不能重複。
判斷雜湊表內物件是否「重複」的條件:「GetHashCode 相同」 且 「Equals 為 true」。
判斷雜湊表內物件是否重複的流程:
先呼叫 GetHashCode 方法,檢查雜湊表是否存在這個雜湊值,若不存在,表示新加入的物件不重複,加入至雜湊表。
若雜湊值存在,再呼叫 Equals 方法,比對物件,若不相等,表示新加入的物件不重複,加入至雜湊表。
從雜湊表取出物件的流程:
先呼叫 GetHashCode 方法,從雜湊表取出這個雜湊值的所有物件,物件數量越少越好。
再呼叫 Equals 方法,逐一比對物件是否相等,若相等,回傳此物件。