GetHashCode函式所存在的陷阱

2022-02-01 01:45:38 字數 4761 閱讀 4749

gethashcode函式,看了它的名字就知道它會被用在**。沒錯,這個函式一般是在操作hashtable或者dictionary之類的資料集的時候被呼叫。每個型別,不管是值型別還是引用型別,都提供這個基本函式,同樣也可以像重寫tostring或者equals函式一樣去重寫它。但是我這裡要說的,不建議重寫此函式,而且在使用這個函式也需要加倍小心。

why? 有些人看了我所說的,會產生類似的疑問。我這裡要提的一點就是,對於引用型別自帶的gethashcode函式來說,基本上是正確的,但是效率不高;而對於值型別自帶的gethashcode函式而言,基本上是不正確的,即使正確也是效率不高。如果重寫型別的gethashcode函式,想要達到既正確又高效是不可能的。

為了解開如上的疑問之前,先來說說實現乙個型別的gethashcode函式,需要滿足那幾點才能是合格的。顯然gethashcode的目的是產生乙個key,為了方便在hashtable或者dictionary中的檢索。既然是這樣,那麼對於gethashcode來說,要滿足如下三點,這也是判斷乙個gethashcode函式是否有效的標準。

第一,兩個相等的物件,通過gethashcode函式產生的結果要相等,此外兩個不相等的物件,通過gethashcode函式的返回值要不相等;否則,通過其產生hashcode而存入hashtable中的資料就無法取出來了。

第二,對於乙個型別的物件來說,其gethashcode函式的返回值要自始至終要保持一致。否則,和第一點一樣。

第三,在gethashcode函式中需要提供乙個比較好的雜湊函式,也就是在最小的範圍內來實現資料分散,換句話說它的離散度決定hashtable訪問效率。

知道了gethashcode函式的驗證標準,接下來就來解開前面的疑團。

首先說說引用型別自帶的gethashcode函式實現。乙個.net程式在執行的時候會對引用型別的物件進行標記,大致操作類似如下:

標記起始為0,當建立乙個引用型別物件的時候,這個標記會自動加一,物件釋放後標記並不做減一操作,這有點兒像資料庫中的自增欄位。

那麼對於引用型別的gethashcode其實就是返回當前引用標記。這也就是為什麼說引用型別的gethashcode函式基本是正確的原因。

因為對於第一條來說,如果型別沒有過載equals或者operator函式的話,型別自帶的equals函式只是在物件引用層進行驗證,也就是說,乙個物件等於另外乙個物件就說明這個物件要麼是另外乙個物件的引用,要麼另外乙個物件是這個物件的引用。這說明沒有新的引用物件產生,那麼當引用標記也不會發生變化,所以對於第一條來說滿足(如果要是過載了equals或者operator函式的話,那麼相應要提供此版本的gethashcode函式,這一點在後面進行敘說)。

至於第二條來說,由於物件資料成員發生改變不會影響到引用標記的改變,所以對於第二條來說也是滿足的。

前兩點的滿足,說明了引用型別的gethashcode是正確地。但是對於第三點來說,由於引用標記是相對於整個程式而言的,並不是型別所特有的,那麼它的效率不高是不言而喻的。

那麼對於值型別自帶的gethashcode函式呢,就更有趣了,為了更形象地說明它的有趣,請先參看如下的**,猜猜debug的輸出是什麼。

public struct errormessage

public bool testhashcode()

}// test "gethashcode" function in value type

errormessage err = new errormessage( "test", 0 );

if( err.testhashcode() )

debug.writeline( "both hash code equal!" );

else

debug.writeline( "not equal!" );可能誰都沒有想到,debug中的輸出是「both hash code equal!」。為什麼呢?原因很簡單,值型別自帶的gethashcode是以其第乙個成員的gethashcode值作為其的返回值。

顯然對於第一條來說,兩個相等值型別物件,其的gethashcode函式返回值是相等的,這沒什麼問題;但是對於不相等的兩個物件來說,它們的gethashcode返回值則有可能相等。顯然違反了第一條。

其次對於第二條來說,由於值型別的gethashcode返回值等於其第乙個成員的gethashcode函式值,那麼修改了第乙個成員的值,也就間接的修改了物件的gethashcode值,從而對於一致性來說也是不滿足的。

對於一二兩條都不滿足,去談第三條是沒有意義的,不過就函式本身來說,效率也和引用型別基本一樣,沒有採用特殊的演算法,所以想得到比較好的效率也是不可能的。

經過逐個分析,再來重複一下我前面所說的。對於引用型別的gethashcode函式來說,基本上是正確的,但是效率不高;而對於值型別而言,基本上是不正確的,而且效率也是不理想的。

那有人就說了,程式會要用到hashtable去存,必然會用到型別的gethashcode函式,如何避免如上的錯誤,或者說提供乙個比較正確的gethashcode函式呢。那麼接下來就分別說說如何去實現(這裡所說的實現主要滿足前兩條即可,最後一條牽扯到hash函式演算法,這裡不做討論)。

這次先說說值型別,因為值型別本身提供的基本不正確。如果不想做過多處理,畢竟提供乙個好的雜湊函式不容易,那麼從值型別的gethashcode規律出發,即從型別自身元素出發。對於乙個值型別,如果其本身存在某個資料可以唯一標明此型別物件,有點兒像資料庫中的key欄位,那麼用它作為型別的第乙個元素。例如就前面所說的errormessage來說,dtinvoked成員可以唯一表示這個型別資料,那麼就可以如下修改。

public struct errormessage

這樣就滿足了驗證第一條,對於第二條,就是要保證這個型別的物件通過gethashcode能自始至終一樣,就要防止第乙個成員被修改,比較好的做法就是給它加上readonly標示,那麼比較完整的樣式應該如下。

public struct errormessage

這樣對於errormessage型別的gethashcode至少是正確的。有人說了,如果定義的型別沒有乙個單獨成員能作為唯一標示,那我就建議你不要把這種型別的資料來產生key。

接下來說說對於引用型別的gethashcode函式改寫。對於第一條來說,在引用型別中有可能重新編寫equals函式,那麼型別自帶的gethashcode函式將不能適應這個要求,需要進行重寫來適應這種改變。如何簡便的改寫gethashcode函式而達到效果呢,這裡可以延用前面值型別的做法,即選擇乙個能唯一標示這個物件的成員來生成hashcode,同時要避免這個成員被修改。

例如乙個比較合理的引用型別的gethashcode函式大致如下(此例引用於原書):

public class customer

public customer( string name, decimal revenue )

/// /// name property which only can be accessed in reading mode

///

public string name

}/// /// create a new object with new name

///

///

///

public customer changename( string newname )

/// /// customer hash code generated by name

///

///

public override int gethashcode()

}

對於customer型別物件來說,它的hashcode是由其_name成員所決定的,所以不能輕易改變,如果通過呼叫changename方法來替換原先的物件的時候,要首先操作hashtable,先把原先的刪除,建立新的之後再儲存。具體如下:

customer c1 = new customer( "test1" );

object orders = new object();

myhashtable.add( c1, orders );

//change name

customer c2 = c1.changename( "test2" );

object o = myhashtable[ c1 ];

myhashtable.remove( c1 );

myhashtable.add( c2, o );

對於如上中custemer物件來說,只是為了產生在hashtable中所存物件的hashcode,當然在實際應用中,兩者需要關聯,否則使用hashtable存這些資料就沒有任何意義了。

這樣對於值型別和引用型別的gethashcode改寫到此基本已經結束了,顯然如上的改寫,只是為了保證型別的gethashcode正確,但是對於其的效率並沒有得到長足的進步,或者換句話來說,改寫後的gethashcode函式仍然保留hashtable使用效率不高。如何在gethashcode函式使用比較好的雜湊函式,使產生的hashcode具有比較好的分布,我在此不對它進行討論,因為光這個問題就足夠寫好幾本書的。

對於gethashcode函式,大致就說到這兒,最後為了加深記憶,總結一下。

首先,在不重寫此函式的情況下,這裡主要說說使用當中應該注意的。

1. 不建議使用值型別物件的gethashcode函式返回值來作為hashtable物件的key;

2. 引用型別是可以使用的,但是要注意如果重寫了equals函式,一定要重寫gethashcode函式來達到一致;

再說說重寫此函式時需要注意的。

1. 不管是值型別還是引用型別,要保證產生hashcode的成員不能被修改;

2. 對於產生hashcode的成員修改,要以產生新物件進行處理,同時要在使用端作相應的修改,即先刪除舊的在新增新的。

虛函式所造成的效能損失

假設在乙個執行緒同步環境中,有類似下面所示的 段 進入執行緒同步 nnum 退出執行緒同步 以win32為例,如我們所知,執行緒同步工具有臨界區,互斥體,訊號量。我們可以任意選擇乙個,為了簡單很可能我們就選擇了臨界區。假如我們需要同步的 非常簡單,我非常建議不需要使用c 的任何功能。但是,很可能沒這...

Focal loss損失函式 所解決的問題

解決的問題 消除正負樣本比例不平衡 one stage演算法需要產生超大量的預選框,模型被大量負樣本所主導,focal loss對此種情況卓有成效。並且挖掘難負樣本 難負樣本即為一些很難區分是正樣本還是負樣本的負樣本 其對立的就是一些簡單的負樣本,很容易區分出來是負樣本,其前向傳播的loss很小,模...

C 日期函式所有樣式大全

datetime dt datetime.now label1.text dt.tostring 2005 11 5 13 21 25 label2.text dt.tofiletime tostring 127756416859912816 label3.text dt.tofiletimeutc...