一般狀況下引用類型的相等性是不該該被重定義/重寫的。安全
例如兩個引用類型的變量 x 和 y,若是這樣寫:if(x == y) {...},那麼你們都明白,這個比較的是引用的相等性。ide
可是有少數狀況下,也能夠爲引用類型重寫相等性。性能
例如這個類:測試
這個類裏面只有兩個string類型的屬性和字段,那麼對它的相等性來講,更合理的是去比較值,而不是引用。spa
還有一種狀況,就是表示數學的引用類型。blog
例若有一個類表示矩陣 Matrix,那麼這樣寫 if(matrix1 == matrix2) {...} 更適合表示它們兩個的值相等。繼承
上述的這兩個例子其實也不是十分的必要。因此想爲引用類型重寫相等性的時候仍是應該先想好,重寫後是否可以更加的直觀,使理解便得更簡單了。接口
實際上若是想比較兩個應用類型裏面的值是否相等,你沒必要非得去重寫那些相等性的方法,你能夠經過實現IEqualityComparer<T>接口來寫一個單獨的相等性比較器。可是這樣的話不能使用==操做符,須要這樣寫:if(eqComparer.Equals(x, y)) {...}編譯器
一個類:數學
首先重寫object.Equals()方法:
這個邏輯比較簡單,就是判斷null,引用和類型,而後再判斷各個屬性(字段)的值是否相等。
而後還須要重寫object.GetHashCode()方法:
這個採用了Resharper生成的方法,之前說過,就再也不介紹了。
最佳實踐還要求重寫C#的==操做符:
固然配套的!=也必須重寫。
在以前重寫值類型相等性的文章裏,我還爲值類型實現了IEquatable<T>接口,而對於引用類型來講,就沒有必要去實現該接口了,能夠把相等性判斷邏輯放在object.Equals()方法裏。
這是上面Citizen類的一個子類:
下面我重寫object.Equals() 方法:
大部分邏輯都在base.Equals()方法裏了,首先若是父類的Equals()方法返回false,那麼下面也就不用作啥了。可是若是父類Equals()認爲這兩個實例是相等的,這就意味着父類裏全部的相等性檢查都經過了,而後咱們仍然須要檢查派生類裏面的獨有字段(屬性),而這個例子裏只有一個字段(屬性)。
而後別忘了實現GetHashCode()方法:
(resharper生成的代碼)
這個方法裏使用了父類的GetHashCode()方法,把它按位異或IdCard的GetHashCode()的結果。
而後實現==和!=操做符:
好,如今咱們來測試一下:
其結果以下:
這個結果還都是對值進行比較的,符合預期。
而後你可能覺得這樣實現沒有問題了。。。。
如今我在Citizen這個父類裏修改一下==的實現,我想讓它更有效率:
而後我再執行和上面一樣的測試代碼,其結果輸入是:
😱,全都相等了。。。。確定不對。。
那在父類裏的==方法設一下斷點看看:
這裏面x和y其實都是BeijingCitizen的實例,可是如今所處的位置是其父類Citizen的==方法裏,因此相等性檢查會在這裏發生,因此這個相等性檢查只會檢查父類裏面的字段,Citizen這個類沒法知道其它繼承於它的類型,因此這裏也沒法比較派生類獨有的字段,在這裏就是IdCard。而全部這些實例的不一樣值就去別再IdCard這個派生類的字段上面了,因此全部檢查的結果都是相等的,由於只比較了父類的那兩個字段。
爲何會調用Citizen父類的==方法呢?由於該方法是靜態的,也就不是virtual的。而個人測試代碼:
其參數類型是父類Citizen,因此a==b這句話會在編譯時就決定採起哪一個版本的==實現,而編譯器在這個方法裏會看到a和b的類型都是Citizen,因此它會調用Citizen版本的==實現。
因此這確實是一個陷阱。
可是爲何原來的寫法就沒有問題呢?
原來的寫法裏,在Citizen這個父類裏,==的實現調用了 object的靜態Equals()方法,而在這個靜態Equals方法裏:
又調用了object的virtual Equals()方法,而若是實際類型是BeijingCitizen的話,那麼就會調用override的Equals()方法,咱們單獨看這個比較:
在BeijingCitizen裏設一個斷點:
能夠看到會擊中該斷點。也能夠看一下CallStack:
如今再次運行全部測試,其結果:
就是正確的了。
因此說,相等性檢查的邏輯須要放在virtual的方法裏。
若是再往上一級,把參數都變成object類型:
輸出結果是:
這是由於==的實現不是virtual的,在object類型上使用==就是判斷引用的相等性。而你也沒法在重載操做符來防止上述事情的發生,由於這段代碼永遠不會調用到你的操做符重載方法。
那麼結論就是,在操做符重載方法裏調用vitual的方法,就能夠應付繼承相關的相等性判斷,可是至少也得輸入你定義的父類的類型(Citizen),好讓你定義的操做符重載方法能夠被最早調用。若是要知足繼承、相等性這兩方面的要求,那麼就須要犧牲類型安全:
因此==操做符重載,能夠看做一種方便的語法糖法,同時也把類型不安全的Equals()方法包裝了起來。
若是我在Citizen類裏面實現了該接口:
那麼方法裏的調用也仍是調用virtual的Equals(),不然的話仍是同樣的bug。那麼這樣看的話,實現該接口幾乎沒有什麼新鮮的做用,雖說該方法能夠作到必定程度的類型安全,可是性能上,比直接調用object.Equals()更慢了。
因此針對引用類型,不建議實現IEquatable<T>接口。
例如:
這樣的話,咱們就能夠把判斷相等的邏輯寫在該方法裏了,由於這個類是sealed,因此能傳遞到這個方法裏的變量必定是該類型的,沒有繼承的存在,咱們就能夠同時擁有類型安全和相等性了。
爲sealed的class實現IEquatable<T>接口確定是可行的,可是否值得呢?
優勢:能獲得微小的性能提高,string就是個例子。
缺點:class自己就更復雜了,你須要記住3種實現相等性判斷的方式。。。
綜上我的建議是針對引用類型不去實現IEquatable<T>接口。