簡介
C#(.NET)的object類裡面有三個關於判斷相等性的方法:
- public virtual bool Equals(object obj)
- public static bool Equals(object objA, object objB)
- public static bool ReferenceEquals(object objA, object objB)
還有一個介面:IEquatable<T>也可以用來判斷相等性。
virtual bool Equals()
比較自定義Class
比較這個Class的兩個例項,它們的屬性值是一樣的:
輸出結果:
之所以結果是False,是因為object.Equals()評估的是引用的相等性,除非進行了重寫。
比較string
這是兩個字串,而且使用string.Copy()可以保證它們不指向同一個地址(如果不使用string.Copy(),而直接賦兩個同樣的值,那麼可能會發生字串駐留問題:https://www.cnblogs.com/artech/archive/2007/03/04/663728.aspx):
這時輸出的結果是:
但是我們看一下string這個類,可以發現string有很多Equals()方法:
如果按照上面這麼寫的話,它並沒有呼叫object.Equals()方法。所以我們改一下程式碼:
這時呼叫的是object.Equals()方法,它的輸出依然是:
這是因為string類對object的Equals()方法進行了重寫,重寫後比較的是字串的值。
除了string之外,delegates和Tuples也對object.Equals()方法進行了重寫。不過對大部分的.NET型別來說,object.Equals()比較的是引用。
比較值型別
值型別是存放在Stack上面的,它們通常沒有引用,除非你對它們進行裝箱操作。
那麼對值型別使用object.Equals()方法,應該沒有什麼意義。。。
有這麼一個自定義的Struct:
然後進行兩組比較:
輸出結果是:
很顯然,結果有點出乎我的意料,針對這個Struct型別,object.Equals()比較的是它們的值。
這是因為所有的struct都繼承於System.ValueType,而System.ValueType繼承於System.Object,System.ValueType它對object.Equals()方法進行了重寫,重寫的方法裡會比較值型別裡面所有的欄位(Field),如果所有欄位都相等,那麼就返回true。
但是System.ValueType的重寫是使用反射來找到所有的欄位(Fields),所以效能比較差。
所以針對值型別最好的辦法是自己重寫一下Equals()方法。
總結
預設情況下,針對引用型別,object.Equals()比較的是引用;針對值型別,object.Equals()比較的是值。
但是所有的型別都可以重寫object.Equals()方法,例如string。
靜態的 Equals() 方法
比較null
使用object virtual的Equals()方法可以應付大部分情況,但是如果該引用是null,那麼使用該方法就會報錯了:
這時候我們就可以使用object類的靜態Equals()方法:
(也可以不寫object)
而結果當然是:
比較兩個null
結果是:
在.NET/.NET Core 裡面,null和null是相等的。
原始碼
靜態Equals()方法的原始碼其實很簡單,除了檢查null之外,它會給出和virtual Equals()方法同樣的結果。
如果你對virtual的Equals()方法進行了重寫,而由於靜態的Equals()方法就會呼叫重寫的virtual Equals()方法,所以這兩個方法要保持一貫性。
靜態 Reference Equals() 方法
它和前兩種方法有點像,但是也不盡相同。
雖然virtual和靜態的Equals()方法通常會比較引用,但是virutal的方法可以被重寫,從而比較的是值,例如string。所以使用ReferenceEquals()來比較兩個變數是否指向同一個例項是更安全準確的。
看下面這兩個比較:
第一個比較呼叫的是object的virtual Equals()方法,但是string對其進行了重寫,比較的是值:
而第二個比較是object的靜態的ReferenceEquals()方法,由於是靜態的,所以沒法重寫:
而C#裡的==是什麼原理,以後再說。
IEquatable<T>
System.Object的static bool Equals(object obj)這個方法,因為其引數是object型別,所以它可以對任何引用型別進行比較。但是如果想比較值型別的話,那麼值型別就會被裝箱,然後再進行比較。但是裝箱的動作會有效能損耗,而之所以採用值型別的主要原因就是因為效能。所以這是一個問題。
再者,使用該方法來比較兩個不相干的型別,比如Apple和Book這兩個Class,比較的時候不會報錯,但是這沒有任何意義。這就是因為引數不是強型別,才會出現這些問題。
而IEquatable<T>這個介面就可以解決這些問題。
它只定義了一個方法:bool Equals(T other)。
例子,三個int:
使用它的Equals()方法:
可以看到除了object.Equals(object obj)這個方法外,它還有一個Equals(int obj)這個方法,它的引數是強型別的,這是因為int實現了IEquatable<T>介面。
而其原始碼大致如下:
所以平時比較int的時候使用==即可。
所有的原始型別都實現了IEquatable<T>介面。int, byte...
而IEquatable<T>對值型別非常有用。
但是對引用型別沒有太大的用處,因為引用型別比較時不存在裝箱問題,而且IEquatable<T>在繼承方面還是存在問題的,但是string還是實現了IEquatable<T>介面,因為string是seal的,不存在繼承。
需要注意的是如果實現了IEquatable<T>,那麼它的實現方法和重寫的object.Equals()方法應該保持一致,做同樣的事。