2012年1月10日

C# - Equals和等於等於( 等於比較運算式 )

這個故事的起源,是在寫一個測試程式,其內容要比較兩個物件的相等,想當然爾,原生的Object.Equals和等於等於是不能使用的,而後,衍生出一堆的問題出來XDD,因為年紀大了,要預防老年癡呆,過了一兩天就忘記的狀況,所以趕快把它紀錄下來。

Ojbect.Equals

這個的起源,必須要從Object.Equals講起,我們先看一下Object.Equals反組譯後的內容。

public virtual bool Equals(object obj)
{
    return RuntimeHelpers.Equals(this, obj);
}

就是這樣,他只是簡單的呼叫RuntimeHelpers.Equals,而這個方法,其實只是去比較是否為相同的執行個體,簡單的說,如果new了兩個物件,就算內容相同,還是會傳回False,而我們自己撰寫的類別,預設當然也是繼承Object,所以自然而然,也也繼承了Equals這個方法。

Object.Equals靜態方法

接下來是Object.Equals的靜態方法,以下是反組譯後的程式碼,最核心的一段是objA.Equals(objB),簡單的說,他還是去調用了objA的Equals方法。

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

所以說Object.Equals靜態方法依舊是使用了自身的Equals。

等於等於 ( = = ) 比較運算子

關於等於的比較運算子,我先引用一下MSDN的一段話。

對於預先定義的實值型別 (Value Type),等號比較運算子 (==) 在運算元相等時傳回 true;否則傳回 false。 對於 string 以外的參考型別,若兩個運算元參考到同一物件,== 會傳回 true。 對於 string 型別,== 會比較字串的值。

這段話的意思是說,如果是int這種Value Type,值相等的時候,就會傳回True,否則會傳回Fase,而參考型別,則必須要指到同一個物件,否則會傳回False,除了string以外,所以簡單的說,int、String等等我們常用的型別,都可以正確的幫我們比較到值,但如果是物件與物件內容的比較,很抱歉,只要它們參照到的是不同位置,就算內容相同,還是會傳回False。

String.Equals(Object)

這個就是Override了Object.Equals的方法,並且判斷傳入進來的型別是否為String,比較重要的是後面那段,第一步它會判斷是否參考到同一個位置( 利用object.ReferenceEquals ),如果是同一個位置,當然不用講,就絕對相同了,但如果是不同位置,他還會利用底層的EqualsHelper方法來看看內容值是否相等。

public override bool Equals(object obj)
{
    if (this == null)
    {
        throw new NullReferenceException();
    }
    string strB = obj as string;
    if (strB == null)
    {
        return false;
    }
    return (object.ReferenceEquals(this, obj) || EqualsHelper(this, strB));
}

所以雖然String也繼承於Object,但他複寫了Equals,所以我們可以輕鬆地去比對兩個字串是否相等。

String.Equals(String)

另外一個string.Equals(String)反組譯後如下,基本上和上面的差不多,差別於,這個是直接傳入String並非Object型別。

public bool Equals(string value)
{
    if (this == null)
    {
        throw new NullReferenceException();
    }
    if (value == null)
    {
        return false;
    }
    return (object.ReferenceEquals(this, value) || EqualsHelper(this, value));
}

接下來是String的等於等於。

String的等於比較運算子

在String裡面寫了等於比較運算子,寫法如下,使用operator關鍵字來處理,所以我們這邊可以看到,其實String的等於比較運算子,實際上是呼叫String.Equals(String,String)這個靜態方法

public static bool operator ==(string a, string b)
{
    return Equals(a, b);
}

接下來我們看一下String的靜態方法。

String.Equals(String,String)

String也有Equals靜態方法,內容如下。

public static bool Equals(string a, string b)
{
    return ((a == b) || (((a != null) && (b != null)) && EqualsHelper(a, b)));
}

這裡比較奇怪的是a==b這段,原則上如果a和b都是字串了,則應該又會進入string靜態的Equals,但這邊感覺上只是去比對位置是否相等;畢竟程式碼是反組譯出來的,真實程式碼是否這樣子,也不能確定,但可以確定的是,String的等於比較運算式,實際上還是回到了Equals方法裡面,這點MSDN也有提到。

Int32.Equals(Object)

接下來,我們看看Int32的部分,這部分也是Override,沒有甚麼太特別之處。

public override bool Equals(object obj)
{
    return ((obj is int) && (this == ((int) obj)));
}

其次是Int32.Equals(Int32)的部分

Int32.Equals(Int32)

這部分就更簡單,Int32是實質型別,所以也沒有甚麼比對位置或是值的問題。

public bool Equals(int obj)
{
    return (this == obj);
}

同樣的Int32是實質型別,所以Int32也沒有復寫等於運算式。

初步的結論

所以,到此,我們這邊初步的結論,如果是要比較兩個物件的內容,無論是使用比較運算子(等於等於 == )或是使用Equals,通通都只會比較是否參考到同一個位置,所以只要參考的位置不同,兩個物件的內容就算相同,很不幸的,還是傳回False。

但如果是int或是strting,無論是使用比較運算子(等於等於 ==)或是使用Equals,都會比較值,只要值不同,就會傳回False。

複雜的問題

接下來就是幾個大家常見的複雜問題。

object a = 1;
object b = 1;
bool value = a == b;
bool value2 = a.Equals(b);

第一個最常見的應該就是這個了吧,答案第一個value是false,第二個value2是true,那是因為1 這個值已經被Boxing起來了存成object型別了,所以當使用等於比較運算式時,就等同於object == object,所以依照上面的理論,除了Int和string以外,除非位置相同,不然都會傳回false,而這邊又因為Boxing的關係,所以它們的位置是不同的。

關於第二個答案是True的關係,那是因為雖然被包成Object型別,但骨子裏面還是Int32,基於多型的概念,實際上,執行的這個Equals,是執行Int32.Equals(Object),所以對照上面,就會變成比較值是否相等。

基本上這個問題是一般大家網路上已經可以常常看到的問題了,接下來是這段。

object c = "a";
object d = "a";

bool valueString = c == d;
bool valueString2 = c.Equals(d);

答案是兩個都是True!,為什麼呢?這裡隱藏了一個陷阱,如果以剛剛的理論來說,等於比較運算式,非實質型別和String的時候,都會只會比較位置,但這裡的c == d的部分,兩個都是object,所以只會比較位置,理論上,會產生兩個string”a”,且不同位置,所以應該會是false阿,但這裡卻為True!,所以表示位置一樣!?,是的,沒錯,這裡的位置真的一樣,因為CLR存在著一種機制,當它初始化時,它會創建一個內部hash,其中key是我們所要存放的字串,而value是誰引用。當定義一個新的string 時,系統會檢查是否有相同的。如果找不到,就建立一個新的,如果找到就直接引用,所以這邊位置是相同的。

所以實際上要怎樣做,才會產生兩個字串呢!?我們直接new一個新的字串,這樣他就不會自動幫忙檢查了。

object e = new string('a', 1);
object f = new string('a', 1);

bool valueStrin3 = e == f;
bool valueString4 = e.Equals(f);

如果是這樣,比對出來的結果,第一個就是False,第二個是True。

結尾

其實等於比較運算式在實質型別裡面,是沒有甚麼問題的,因為實際存放的東西,就是實質,例如:1、a這類東西,所以等於比較運算式會去比較兩個記憶體上的實質,其實是沒甚麼問題的,但是如果是參考型別,雖然物件內容可能一樣,但實際上,存放在記憶體的實質,只是個位置,而這兩個物件的位置,一定不一樣,所以等於比較運算式,只會比對出兩個位置不同;在Java的String和C#一樣,都是物件,所以存放在記憶體上的都是位置,但有點不一樣的是,C#有幫忙復寫了等於比較運算式,他裡面實際上會去呼叫String的Equals,所以在C#上使用等於比較運算式,能比較出真正String的值,但Java裡面,就只能使用Equals了。

參考資料

3 則留言:

  1. 我在搜尋等於等於與Equals的差異時,你的這篇文章非常的深入淺出,幫助我理解到底層在做的事情與效果上的差異,非常感佩你寫的這篇文章。
    這樣子我可以放心的用等於等於來比較字串了。

    回覆刪除
  2. Hi aBen
    您過獎了,小弟只是預防老年癡呆,所以查完後,趕快隨手筆記一下~^^~

    回覆刪除
  3. 寫的極好啊~ 叩謝你的分享
    要不然我到80歲還是不知道為什麼常常
    obj == obj 第一次可以true
    第二次就變成false了~

    回覆刪除