[C#] 類型學習筆記二:詳解對象之間的比較


繼上一篇對象類型后,這里我們一起探討相等的判定。

相等判斷有關的4個方法

CLR中,和相等有關系的方法有這么4種:

(1) 最常見的 == 運算符

(2) Object的靜態方法ReferenceEquals

(3) Object的靜態方法Equals

(4) Object.Equals()方法,這是一個virtual method

"==" 運算符

首先要知道"==" 是一個運算符,它只有在兩邊都為相同類型時才能通過編譯。

假設“==” 沒有被我們顯示地重載過,當它的兩邊都是引用類型時,"=="在左右兩邊引用同一個對象時返回true,它的作用和(1)中的System.Object.ReferenceEquals相同

當"=="兩邊都是沒被裝箱的值類型時,只有值類型重載了"=="才能通過編譯,也就是說,如果我們通過struct定義了新的值類型,然后通過"=="來比較,那只有我們在struct中顯示重載了"=="才能通過編譯。

FCL中Int32等這些自帶的值類型,雖然查看代碼時沒有看到其對"=="的重載,但是我相信應該有隱示地對其進行重載,重載的內容應該和Int32中的Equals()函數一致。

 

Object的靜態方法ReferenceEquals

它的定義如下

public static bool ReferenceEquals(object objA, object objB)
{
    return objA == objB;
}

 

顯然,這個方法的作用是為了判定兩個object是否指向同一個堆對象。這里有個小實例

int n = 10;

Object.ReferenceEquals( n, n );

object o1 = (object)n;
object o2 = (object)n;
if(o1 == o2) ...

 

兩次的判定結果結果應該都為false,這里需要我們前一節裝箱的知識。在每一次比較中,n在代碼中都會兩次被裝箱,既然被裝箱了兩次,自然就是不同的對象,所以返回為false。

Object的靜態方法Equal

public static bool Equals(object objA, object objB)
{
    return objA == objB || (objA != null && objB != null && objA.Equals(objB));
}

 

從其源碼可以看出,它其實就是==運算符和 (4)中所提到的object.Equals 方法的結合版。就是先判斷對象的identical,再比較內容。雖然很全面,但是編程中不常用到,因為程序員在寫代碼時,對於到底是判斷identical還是比較內容,都有明確的選擇性。

Sytem.Object的提供的Equals 方法

這里才是我們的重頭戲,這個方法將真正被用來比較對象的內容。

FCL中的萬物之源System.Object提供的虛函數Equals的定義如下:

public virtual bool Equals(object obj){
    if(this == obj) return true;
    return false;
}

你沒有看錯,就這樣沒有了。

是不是有點意外?

這就是Microsoft的思路,這個virtual 方法只是意思意思罷了,真正的基於內容的比較,定義在具體的類中。

Equals 方法

System.Object所提供的virtual 方法只有被比較的對象引用同一個托管堆對象,才會返回true。而且,參數是object,也就是說如果我們自定義的類型如果沒有顯示override Equals的話,需要比較的時候類會被裝箱。

那么,既然Object的equals如此的“弱”,那么系統本身的那些int,string等這些常見的值類型的比較的時候,內部是什么情況?

System.ValueType的Equals方法

有了第一篇筆記的鋪墊,我們知道int等都是值類型,值類型是繼承自System.ValueType的,System.ValueType又是繼承自System.Object的。在System.ValueType中,不出意外,重寫了Equals方法的實現。使用ILSpy打開System.ValueType中關於Equals的實現:

// System.ValueType
/// <summary>Indicates whether this instance and a specified object are equal.</summary>
/// <returns>true if <paramref name="obj" /> and this instance are the same type and represent the same value; otherwise, false.</returns>
/// <param name="obj">Another object to compare to. </param>
/// <filterpriority>2</filterpriority>
[__DynamicallyInvokable, SecuritySafeCritical]
public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType runtimeType = (RuntimeType)base.GetType();
    RuntimeType left = (RuntimeType)obj.GetType();
    if (left != runtimeType)
    {
        return false;
    }
    if (ValueType.CanCompareBits(this))
    {
        return ValueType.FastEqualsCheck(this, obj);
    }
    FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
        object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);
        if (obj2 == null)
        {
            if (obj3 != null)
            {
                return false;
            }
        }
        else
        {
            if (!obj2.Equals(obj3))
            {
                return false;
            }
        }
    }
    return true;
}

 整個實現可以分為下面幾個部分:

(1) 如果參數是null,不需說,返回false

(2) 隨后通過反射獲取當前對象和參數對象的類型,比較兩個類型是否一致。類型如果都不一致就直接返回false了。

這里大家可能會有疑問:這里明明使用base.GetType(),怎么可以解釋為“獲取當前對象的類型”?

反射方法GetType()是Sytem.Object中被實現的方法,並且不是虛函數。這就是說,任何類都不可能提供對其重寫,任何實例調用GetType(),最后都會調用其基類Sytem.Object的GetType(),而GetType()的作用就是獲得一開始 調用這個方法的實例的類型。因此,這段代碼中無論是"base.GetType()",還是"this.GetType()",其實結果是一致的。"base.GetType()"寫法其實更加規范(不愧是反編譯出來的==),因為如上所說,GetType()是Sytem.Object中的方法。這里是題外話,另一篇博文會結合實例介紹GetType()的特點。

(3) 通過CanCompareBits是否能進行bit 比較,可以的話采用FastEqualsCheck進行比較。這兩個方法都是System.ValueType的私有方法。關於他們的解釋,我直接引用博文 Magic behind ValueType.Equals 中的話

The comment of CanCompareBits says "Return true if the valuetype does not contain pointer and is tightly packed". And FastEqualsCheck uses "memcmp" to speed up the comparison.

(4) 在(2)中我們已經得到this所屬的type,然后再通過反射獲取這個type所有的field,接着分別獲取 this對象和比較對象obj在每個field的值,調用equals函數比較每一個值。如果需要用到這一步,時間上的開銷就比較大了。

 

結論

(1) 如果我們自定義一個值類型而不用override關鍵字去新寫一個equals方法,那么當需要比較時:(1) 值類型會被裝箱 (2) 裝箱后可能還需要調用反射獲取每一個field。

所以在在《Effective C#》, 才會有這樣一句話:"Always create an override of ValueType.Equals() whenever you create a value type"。

如果打開Int32,Boolean等的定義,可以看到里面都有對Equals()的顯示實現,並且都是實現了System.IComparable 接口。

(2) 現在我也可以回答一開篇的疑問了,當我們定義兩個int類型的數,然后通過"=="比較它們的時候,系統的做法是:a. 通過"=="重載的內容,發現是調用Equals()進行比較  b.因為Equals已經被Override關鍵字定義過,直接調用本地定義的Equals()函數。

 

知道了這些,我們不難知道為什么C#程序員要擁有下面這兩個編程習慣:

(1) 在引用類型之間的比較時,不要使用 "==",而應該使用類型自身的Equals() (當然在此之前記得先override這個方法)。如果想判斷兩個引用類型的對象是否是同一個對象,使用Object.ReferenceEquals()靜態方法。

(2) 在系統自帶的值類型之間的比較時,可以使用類型自身的Equals(),但是為了可讀性,往往使用 "==" 運算符。

 

相關閱讀:

[C#] 類型學習筆記一:CLR中的類型,裝箱和拆箱

[C#] 類型學習筆記三:自定義值類型


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM