今天寫一下C#里的“==”這個操作符。 原始類型 假象 在剛學C#的時候,我以為C#里的==和.NET里的object.Equals()方法是一樣的,就是一個語法糖而已。其實它們的底層機制是不一樣的,只不過它們給出的結果在大多數情況下恰好相同。 看個例子: 這倆方法給出的結果都是True。 看起來這 ...
今天寫一下C#里的“==”這個操作符。
原始類型
假象
在剛學C#的時候,我以為C#里的==和.NET里的object.Equals()方法是一樣的,就是一個語法糖而已。其實它們的底層機制是不一樣的,只不過它們給出的結果在大多數情況下恰好相同。
看個例子:
這倆方法給出的結果都是True。
看起來這兩種方式做了同樣的動作,就是比較兩個值。
底層原理
Build項目,然後使用ildasm看一下生成的il語言(ildasm位置大致在:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools)。
使用ildasm打開生成的dll,首先查看Program類裡面的ByEqualMethod方法:
可以看到C#源碼里調用Equals()的地方直接被翻譯成il語言里相應的Equals()方法了。。。。
然後看一下ByEqualOperator這個方法:
在C#里該方法使用了==操作符,而在il語言里,我們只看到了一個叫做ceq的指令。ceq的意思是compare for equality,就是比較兩個值是否相等,在運行時,它將會被轉換為硬體上的比較,也許用的是CPU的寄存器。
針對原始類型,C#的==操作符並沒有使用.NET里提供的那些Equals方法,這時==操作符使用專用的彙編語言指令來進行判斷相等性的。
使用 == 判斷引用類型的相等性
這裡的引用類型不包含string。
看例子,這裡我使用==來比較自定義類MyClass的兩個實例是否相等:
而結果是兩個False:
使用ildasm看一下ByEqualMethod()這個方法:
可以看到,a.Equals(b)調用的是virtual的object.Equals()方法,參數類型是object,這個應該都能理解。
再看一下ByEqualOperator()方法:
== 操作符翻譯過來還是使用ceq對兩個參數進行的比較,和之前int類型的例子一樣,除了參數類型不同。
所以這應該也是使用CPU的硬體來進行判斷相等性的,那麼像這種引用類型是怎麼通過CPU硬體來比較的呢?因為這兩個類型是引用類型,所以c1,c2兩個變數裡面保存的是它們對應的實例在托管堆中的記憶體地址,也就是兩個數字而已,所以當然可以進行比較了。
string
我們都知道,==用來判斷string相等性的時候,比較的是string值,而不是引用地址。
看例子:
結果是兩個True:
首先,使用string.Copy()方法可以保證str1和str2是兩個不同的引用。
使用ildasm,先看ByEqualMethod():
可以看到,這裡a.Equals(b)實際調用的是string實現的IEquatable<T>介面的Equals方法,它的參數是string。
再看一下ByEqualOperator():
這次沒有使用ceq指令,而是調用了一個叫做op_Equality()的方法,這是個什麼方法?
其實它是C#里 == 操作符的一個重載:static bool op_Equality(string, string)。
在C#里,當你定義一個類型的時候,你可以對==操作符進行重載,格式大概如下:
因為il語言里沒有操作符的概念,而只有方法才能作為操作符的重載而存在於il里,所以這裡使用的是靜態方法,它會被翻譯為一個特殊的靜態方法叫做op_Equality()。
我們也可以直接看一下string類的源碼,裡面也是這樣對==進行重載的:
當然,重載了==,也需要重載 !=。
小結
總結一下,使用==來判斷引用類型的相等性,需要按下麵的思路順序進行考慮:
1. 該類型是否對 == 進行了重載?如果是,那就是用該重載方法;否則看2
2. 使用ceq指令來比較引用指向的記憶體地址。
另外還需要再提醒一下的是,string類的==和Equals()方法永遠都會給出一樣的結果。
還有一個原則就是,當你改變某個類型的相等性判斷方法是,要確保==和Equals()方法做的是同樣的事情。
值類型
非原始類型
看例子,這裡有兩個值類型:
當我使用==對它們進行比較的時候,直接報錯了。
因為預設情況下,不可以使用==來對非原始類型的值類型進行相等性判斷。要想使用==,就必須提供重載方法。
Tuple
直接看例子:
結果如下:針對這兩個tuple,我做了三個相等性判斷,通過第一個ReferenceEquals方法我們可以知道這兩個tuple變數指向不同的實例。
而tp1.Equals(tp2)返回的是True,這是因為Tuple類(引用類型)重寫了object.Equals()方法,從而比較的是Tuple裡面的值。
儘管微軟為Tuple把object.Equals()方法重寫了,但是它並沒有處理==操作符,所以==還是在比較引用的相等性,所以會返回False。
這樣做確實挺讓人迷惑的。。。
比較==和object.Equals()方法
通常情況下,儘量使用==操作符,但是有時候==不行,需要使用object.Equals()方法,例如涉及到繼承或者泛型的時候。
繼承
直接看例子:
這兩個字元串我做了4個相等性判斷,其結果為:
無論是object的virtual Equals()方法,還是==操作符,還是object的static Equals()方法,都會返回True。
但是我做一下小小的改動:
我們看看結果會不會變:
結果發生了變化,str1==str2這次返回了False。
這是因為==操作符不是virtual的,它相當於是static的,而static的是無法virtual的。
現在 str1 == str2 這句話,我們比較的是兩個類型為object的變數,儘管我們知道它們都是string,但是編譯器並不知道。而針對於非virtual的方法或操作符,到底調用哪個方法是在編譯時決定的,因為這兩個變數的類型是object,所以編譯器會選擇用來比較object的代碼,而object又沒有==操作符的重載,所以==做的就是比較引用的相等性,而這兩個string是不同的實例,所以結果會返回False。
所以(object)x == (object)y和ReferenceEquals(x, y)的結果總是一樣的。
針對涉及繼承的相等性判斷,最好還是使用object.Equals()方法,而不是==操作符。
泛型
另一種不適合使用==操作符的情景是涉及泛型的時候,直接看例子:
這個泛型方法直接報錯了,因為==操作符無法應用於這兩個操作數T,T可以是任何類型,例如T是非原始類型的struct,那麼==就不可用。我們無法為泛型指定約束讓其實現某個操作符。針對這個例子,我可以這樣做,來保證可以編譯:
現在T是引用類型了,代碼可以編譯了。我們使用以下該方法:
按理說這就相當於調用了Equals()方法,結果應該返回True。而實際結果是:
之所以返回了False,是因為泛型方法里的==操作符比較的是引用,而這又是因為儘管編譯器知道可以把==操作符應用於類型T,但是它仍然不知道具體是哪個類型T會重載該操作符,所以它會假設T不會重載==操作符,從而對待這兩個操作數如同object類型一樣並編譯,所以判斷的是引用相等性。
所以泛型方法不會選擇任何的操作符重載,它對待泛型類就像對待object類型一樣。
綜上,針對泛型方法,應該使用Equals()方法,而不是==操作符。