淺析c#中==操作符和equals方法
在之前的文章中,我們講到了使用C#中提供的Object類的虛Equals方法來判斷Equality,但實際上它還提供了另外一種判斷Equality的方法,那就是使用==運算符。許多童鞋也許會想當然的認為==不過是Equals方法的語法糖而已,然而事實卻並非如此。盡管從實現上來說,它給出的判定結果往往和Object.Equals方法一致,但這不是必須的,因為兩者的實現機制完全相同。下面就讓我們看看兩者的區別。
1、==和基元類型
讓我們首先從最基礎的基元類型,如int、float、double等開始,看看==是如何對他們進行相等性測試的。
class Program { static void Main(String[] args) { int number1 = 9; int number2 = 9; Console.WriteLine(number1.Equals(number2)); Console.WriteLine(number1 == number2); Console.Read(); } }
上面的代碼中,我們比較兩個整數的相等性,其中用到了兩種比較方式,第一種調用Int32類型對Object.Equals方法的重載版本,第二種調用==運算符。兩種比較方法的結果都顯示true,似乎兩者的實現機制都是調用的Equals方法進行比較一樣。下面讓我們使用ildasm.exe實用程序來驗證一下這個猜想是否正確吧。
上面這條IL語句對應於源碼中的第一個比較方式,可以看到它是通過調用Object.Equals實現的,該方法的定義位於System.Int32類型中,並且是實現了IEquatable<int>接口。
下面來看==操作符對應的IL語句。
可以看到,==操作符生成的IL並沒有調用Object.Equals,而是生成了一條ceq指令,該指令的作用是比較加載到棧上的兩個值並且是通過CPU寄存器進行相等性比較的。
總結:對於基元類型的相等性判斷而言,C#中==操作符是通過ceq指令實現的,而非Object.Equals方法。
2、==和引用類型
接下來讓我們看下==如何對引用類型進行相等性判定。
static void Main(string[] args) { Customer C1 = new Customer(); C1.FirstName = "Si"; C1.LastName = "Li"; Customer C2 = new Customer(); C2.FirstName = "Si"; C2.LastName = "LI"; Console.WriteLine(C1.Equals(C2));Console.WriteLine(C1==C2); Console.Read(); } public class Customer { public string FirstName { get; set; } public string LastName { get; set; } }
運行上面的代碼,結果會顯示兩個False。這不由得讓我們猜測:==運算符和Equals方法一樣,也是作引用相等性判定,而不是值相等性判定。接下來讓我們通過ildasm.exe查看IL代碼確認一下。
可以看到,對於C1.Equals(C2)而言,它生成的IL代碼,調用的是Object.Equals方法,由於沒有重載,故默認進行引用相等性測試。而對於C1==C2,它對於生成的IL代碼,僅是一條ceq指令,這裏用來判定引用相等性。
也許有童鞋好奇==是如何作引用相等性判定的。這是因為引用類型的變量持有的是同類型的實例對象的內存地址,而內存地址不過是一個數字,因此可以用ceq判等性測試,就像對整數類型的判等測試一樣。
總結:對於引用類型的判等而言,==操作符是通過ceq指令比較內存地址來實現的。
3、==和String類型
同上,還是通過一段簡單的代碼測試==如何對String類型作相等性測試。
class Program { static void Main(String[] args) { string str1 = "hello"; string str2 = String.Copy(str1); Console.WriteLine(ReferenceEquals(str1, str2)); Console.WriteLine(str1 == str2); Console.WriteLine(str1.Equals(str2)); Console.Read(); } }
測試以上代碼,結果顯示:False True True。答案已經很明朗了,==操作符對String類型作值相等性測試。下面通過IL代碼揭秘一下==的運行機制。
從以上的IL代碼片段中,我們並未發現ceq指令,而是調用了一個op_equality(string, string)方法。那麽該方法是如何產生的呢?實際上,這是由於String類重載了==運算符導致的。在C#中,如果重載了==運算符,那麽編譯器就會編譯生成一個static方法,名字為op_equality。
若在VS中通過導航到定義來查看String類的源代碼,會發現String類實現了兩個運算符重載方法,一個是相等性測試,另一個是不等性測試。
當我們為自己的類型重載==操作符的實現時,應該牢記一點:為了通過編譯,應為==和!=同時提供重載實現。
總結:
1、通過上面的學習,我們知道對於引用類型,==操作符會以下面兩種方式之一進行判等測試。
-
- 若存在==的重載實現,那麽編譯器將把它編譯為一個static方法。
- 若不存在==的重載實現,編譯器將它編譯為一條ceq指令,比較內存地址。
2、當我們更改一個類型的判等邏輯時,應該同時為Equals方法和==提供實現,並且應保證兩者的比較結果一致,否則,使用該類型的開發人員將感到困惑。
4、==和值類型
通過上面的學習,我們已經曉得==如何對基元類型和引用類型作判等測試。但還未提及非基元值類型,下面讓我們看看==操作符如何
還是以上面提到的Customer類為例,只是這次我們將它變更為struct。代碼如下:
static void Main(string[] args) { Customer C1 = new Customer(); C1.FirstName = "Si"; C1.LastName = "Li"; Customer C2 = new Customer(); C2.FirstName = "Si"; C2.LastName = "LI"; Console.WriteLine(C1.Equals(C2)); Console.WriteLine(C1==C2); Console.Read(); } public struct Customer { public string FirstName { get; set; } public string LastName { get; set; } }
若運行上面的程序,會產生如下的編譯錯誤:
該錯誤清楚地向我們指明:不存在用於非基元值類型的重載==操作符。若要使用==進行判等,必須由我們提供。現在讓我們在Customer結構的定義中添加以下代碼:
public static bool operator ==(Customer p1, Customer p2) { }
此時,Main方法中的代碼就不會產生編譯錯誤了,但是該程序仍不能編譯通過,因為operator ==方法的返回值類型為bool,而我們並沒有提供任何返回值。但這並不重要,我們僅僅在這裏探討了應為非基元值類型提供==操作符的重載實現,具體的實現代碼根據自己的業務需求填充就行了。
5、總結
下面讓我們來總結一下==和Equals方法對各種類型進行判等性測試的邏輯:
- 對於基元類型,比如
int
,float
,long
,bool等,兩者均比較值,故比較結果一致。
- 對於大多數引用類型而言,==和Object.Equals方法均默認比較引用,但可以選擇重載==和Equals方法,但為了不使該類型的使用者感到困惑,應同時overload或override兩者,並使兩者的判定結果保持一致。
- 對於非基元值類型而言,Object.Equals方法將通過反射進行值相等性測試,但它的性能低下,實踐中最好override該方法以實現快速判等;而==操作符默認情況下不可用,若想用需自己實現。
由於==操作符比較簡潔,因此在平常開發中更受喜愛,但它也存在缺陷,我將在下篇博文中進行介紹。
淺析c#中==操作符和equals方法