歡迎光臨
每天分享高質量文章

C# – 為取用型別重定義相等性 – 繼承相關

派生類

這是上面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()方法包裝了起來。

為什麼不實現IEquatable

如果我在Citizen類裡面實現了該介面:

那麼方法裡的呼叫也還是呼叫virtual的Equals(),否則的話還是一樣的bug。那麼這樣看的話,實現該介面幾乎沒有什麼新鮮的作用,雖然說該方法可以做到一定程度的型別安全,但是效能上,比直接呼叫object.Equals()更慢了。

所以針對取用型別,不建議實現IEquatable介面。

非得實現的話建議sealed

例如:

這樣的話,我們就可以把判斷相等的邏輯寫在該方法裡了,因為這個類是sealed,所以能傳遞到這個方法裡的變數一定是該型別的,沒有繼承的存在,我們就可以同時擁有型別安全和相等性了。

為sealed的class實現IEquatable介面肯定是可行的,但是否值得呢?

優點:能得到微小的效能提升,string就是個例子。

缺點:class本身就更複雜了,你需要記住3種實現相等性判斷的方式。。。

綜上個人建議是針對取用型別不去實現IEquatable介面

    已同步到看一看
    贊(0)

    分享創造快樂