C#中==運算符
==運算符與基元類型
我們分別用兩種方式比較兩個整數,第一個使用的是Equals(int)
方法,每二個使用的是==運算符:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 int num1 = 5; 6 int num2 = 5; 7 8 Console.WriteLine(num1.Equals(num2)); 9 Console.WriteLine(num1 == num2);10 }11 }
運行上面的示例,兩個語句出的結果均為true
。我們通過ildasm.exe工具進行反編譯,查看IL代碼,了解底層是如何執行的。
如果您以前從來沒有接觸過IL指令,不過沒關系,在這裏您不需要理解所有的指令,我們只是想了解這兩個比較方式的差異。
您可以看到這樣一行代碼:
1 IL_0008: call instance bool [mscorlib]System.Int32::Equals(int32)
在這裏調用的是int
類型Equals(Int32)
方法(該方法是IEquatable<Int>
接口的實現)。
現在再來看看使用==運算符比較生成的IL指令:
1 IL_0015: ceq
您可以看到,==運行符使用的是ceq
指令,它是使用CPU寄存器來比較兩個值。C#==運算符底層機制是使用ceq
指令對基元類型進行比較,而不是調用Equals
方法。
==運算符與引用類型
修改上面的示例代碼,將int
類型改為引用類型,編譯後通過ildasm.exe工具反編譯查看IL代碼。
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Person p1 = new Person(); 6 p1.Name = "Person1"; 7 8 Person p2 = new Person(); 9 p2.Name = "Person1";10 11 Console.WriteLine(p1.Equals(p2));12 Console.WriteLine(p1 == p2);13 }14 }
上述C#代碼的IL代碼如下所示:
我們看到p1.Equals(p2)
代碼,它是通過調用Object.Equals(Object)
虛方法來比較相等,這是在意料之中的事情;現在我們來看==運算符生成的IL代碼,與基元類型一致,使用的也是ceq
指令。
==運算符與String類型
接來下來看String
類型的例子:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 string s1 = "Sweet"; 6 string s2 = String.Copy(s1); 7 8 Console.WriteLine(ReferenceEquals(s1, s2)); 9 Console.WriteLine(s1 == s2);10 Console.WriteLine(s1.Equals(s2));11 }12 }
上面的代碼與我們以前看過的非常相似,但是這次我們使用String
類型的變量。我們建一個字符串,並付給s1
變量,在下一行代碼我們創建這個字符串的副本,並付給另一個變量名稱s2
。
運行上面的代碼,在控制臺輸出的結果如下:
您可以看到ReferenceEquals
返回false
,這意味著這兩個變量是不同的實例,但是==運算符和Equals
方法返回的均是true。在String
類型中,==運算符執行的結果與Equals
執行的結果一樣。
同樣我們使用過ildasm.exe工具反編譯查看生成IL代碼。
在這裏我們沒有看到ceq
指令,對String
類型使用==運算符判斷相等時,調用的是一個op_equality(string,string)
的新方法,該方法需要兩個String
類型的參數,那麽它到底是什麽呢?
答案是String
類型提供了==運算符的重載。在C#中,當我們定義一個類型時,我們可以重載該類型的==運算符;例如,對於以前的例子中我們實現的Person
類,如果我們為它重載==運算符,大致的代碼如下:
1 public class Person 2 { 3 4 public string Name { get; set; } 5 6 public static bool operator ==(Person p1, Person p2) 7 { 8 // 註意這裏不能使用==,否則會導致StackOverflowException 9 if (ReferenceEquals(p1, p2))10 return true;11 12 if (ReferenceEquals(p1, null) || ReferenceEquals(p2, null)) 13 return false; 14 15 return p1.Name == p2.Name;16 }17 18 public static bool operator !=(Person p1, Person p2)19 {20 return !(p1 == p2);21 }22 }
上面的代碼很簡單,我們實現了==運算符重載,這是一個靜態方法,但這裏要註意的是,方法的名稱是perator ==
,與靜態方法的相似性;事實上,它們會被由編譯器成一個名稱為op_Equality()
的特殊靜態方法。
為了使用事情更加清楚,我們查看微軟實現的String
類型。
在上面的截圖中,我們可以看到,有兩個運算符的重載,一個用於相等,另一個是不等式運算符,其運算方式完全相同,但是否定等於運算符輸出。需要註意的一點是,如果您想重載一個類型的==運行符的實現,那麽您還需要重載!=操作符的實現,否則編譯會報錯。
==運算符與值類型
在演示值類型的示例前,我們先將Person類型從引用類型改為值類型,Person定義如下:
1 public struct Person 2 { 3 public string Name { get; set; } 4 5 public Person(string name) 6 { 7 Name = name; 8 } 9 10 public override string ToString()11 {12 13 return Name;14 }15 }
我們將示例代碼改為如下:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Person p1 = new Person("Person1"); 6 Person p2 = new Person("Person2"); 7 8 Console.WriteLine(p1.Equals(p2)); 9 Console.WriteLine(p1 == p2);10 }11 }
當我們在嘗試編譯上述代碼時,VS將提示如下錯誤:
根據錯誤提示,我們需要實現Person結構體的==運算符重載,重載的語句如下(忽略具體的邏輯):
1 public static bool operator ==(Person p1, Person p2)2 {3 }4 public static bool operator !=(Person p1, Person p2)5 {6 }
當添加上面代碼後,重新編譯程序,通過ildasm.exe工具反編譯查看IL代碼,發現值類型==運算符調用也是op_Equality
方法。
關於值類型,我們還需要說明一個問題,在不重寫Equals(object)
方法時,該方法實現的原理是通過反射遍歷所有字段並檢查每個字段的相等性,關於這一點,我們不演示;對於值類型,最好重寫該方法。
==運算符與泛型
我們編寫另一段示例代碼,聲明兩個String
類型變量,通過4種不同的方式比較運算:
1 public class Program 2 { 3 public static void Main(string[] args) 4 { 5 string str = "Sweet"; 6 string str1 = string.Copy(str); 7 8 Console.WriteLine(ReferenceEquals(str, str1)); 9 Console.WriteLine(str.Equals(str1));10 Console.WriteLine(str == str1);11 Console.WriteLine(object.Equals(str, str1));12 }13 }
輸出的結果如下:
首先,我們使用ReferenceEquals
方法判斷兩個String
變量都引用相同,接下來我們再使用實例方法Equals(string)
,在第三行,我們使用==運算符,最後,我們使用靜態方法Object.quals(object,object)
(該方法最終調用的是String
類型重寫的Object.Equals(object)
方法)。我們得到結論是:
ReferenceEquals
方法返回false
,因為它們不是同一個對象的引用;String
類型的Equals(string)
方法返回也是true
,因為兩個String
類型是相同的(即相同的序列或字符);==運算符也將返回
true
,因為這兩個String
類型的值相同的;虛方法
Object.Equals
也將返回true
,這是因為在String
類型重寫了方法,判斷的是String
是否值相同。
現在我們來修改一下這個代碼,將String
類型改為Object
類型:
1 public class Program 2 { 3 public static void Main(string[] args) 4 { 5 object str = "Sweet"; 6 object str1 = string.Copy((string)str); 7 8 Console.WriteLine(ReferenceEquals(str, str1)); 9 Console.WriteLine(str.Equals(str1));10 Console.WriteLine(str == str1);11 Console.WriteLine(object.Equals(str, str1));12 }13 }
運行的結果如下:
第三種方法返回的結果與修改之前不一致,==運算符返回的結果是false
,這是為什麽呢?
這是因為==運算符實際上是一個靜態的方法,對一非虛方法,在編譯時就已經決定用調用的是哪一個方法。在上面的例子中,引用類型使用的是ceq
指令,而String
類型調用是靜態的op_Equality
方法;這兩個實例不是同一個對象的引用,所以ceq
指令執行後的結果是false
。
再來說一下==運算符與泛型的問題,我們創建一個簡單的方法,通過泛型方法判斷兩個泛型參數是否相等並在控制臺上打印出結果:
1 static void Equals<T>(T a, T b)2 {3 Console.WriteLine(a == b);4 }
但是當我們編譯這段代碼時,VS提示如下錯誤:
上面顯示的錯誤很簡單,不能使用==運算符比較兩個泛型T。因為T可以是任何類型,它可以是引用類型、值類型,不能提供==運算符的具體實現。
如果像下面這樣修改一下代碼:
1 static void Equals<T>(T a, T b) where T : class2 {3 Console.WriteLine(a == b);4 }
當我們將泛型類型T改為引用類型,能成功編譯;修改Main
方法中的代碼,創建兩個相同的String
類型,和以前的例子一樣:
1 public class Program 2 { 3 static void Main(string[] args) 4 { 5 string str = "Sweet"; 6 string str1 = string.Copy(str); 7 8 Equals(str, str1); 9 }10 11 static void Equals<T>(T a, T b) where T : class12 {13 Console.WriteLine(a == b);14 }15 }
輸出的結果如下:
結果與您預期的結果不一樣吧,我們期待的結果是true
,輸出的結果是false
。不過仔細思考一下,也許會找到答案,因為泛型的約束是引用類型,==運算符對於引用類型使用的是引用相等,IL代碼可以證明這一點:
如果我們泛型方法中的==運算符改為使用Equals
方法,代碼如下:
1 static void Equals<T>(T a, T b)2 {3 Console.WriteLine(object.Equals(a, b));4 }
我們改用Equals
,也可以去掉class
約束;如果我們再次運行代碼,控制臺打印的結果與我們預期的一致,這是因為調用是虛方法object.Equals(object)
重寫之後的實現。
但是其它的問題來了,如果對於值類型,這裏就會產生裝箱,有沒有解決的辦法呢?關於這一點,我們直接給出答案,有時間專門來討論這個問題。
將比較的值類型實現IEquatable<T>
接口,並將比較的代碼改為如下,這樣可以避免裝箱(關於這一點,可以參考老趙的博客:http://blog.zhaojie.me/2013/04/dont-go-half-way-of-preventing-boxing.html):
1 static void Equals<T>(T a, T b)2 {3 Console.WriteLine(EqualityComparer<T>.Default.Equals(a, b));4 }
C#中==運算符