我們分別用兩種方式比較兩個(gè)整數(shù),第一個(gè)使用的是Equals(int)
方法,每二個(gè)使用的是==運(yùn)算符:
成都創(chuàng)新互聯(lián)公司專注于叢臺(tái)企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站開發(fā),商城開發(fā)。叢臺(tái)網(wǎng)站建設(shè)公司,為叢臺(tái)等地區(qū)提供建站服務(wù)。全流程按需設(shè)計(jì),專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)
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 }
運(yùn)行上面的示例,兩個(gè)語句出的結(jié)果均為true
。我們通過ildasm.exe工具進(jìn)行反編譯,查看IL代碼,了解底層是如何執(zhí)行的。
如果您以前從來沒有接觸過IL指令,不過沒關(guān)系,在這里您不需要理解所有的指令,我們只是想了解這兩個(gè)比較方式的差異。
您可以看到這樣一行代碼:
1 IL_0008: call instance bool [mscorlib]System.Int32::Equals(int32)
在這里調(diào)用的是int
類型Equals(Int32)
方法(該方法是IEquatable
接口的實(shí)現(xiàn))。
現(xiàn)在再來看看使用==運(yùn)算符比較生成的IL指令:
1 IL_0015: ceq
您可以看到,==運(yùn)行符使用的是ceq
指令,它是使用CPU寄存器來比較兩個(gè)值。C#==運(yùn)算符底層機(jī)制是使用ceq
指令對(duì)基元類型進(jìn)行比較,而不是調(diào)用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)
代碼,它是通過調(diào)用Object.Equals(Object)
虛方法來比較相等,這是在意料之中的事情;現(xiàn)在我們來看==運(yùn)算符生成的IL代碼,與基元類型一致,使用的也是ceq
指令。
接來下來看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
類型的變量。我們建一個(gè)字符串,并付給s1
變量,在下一行代碼我們創(chuàng)建這個(gè)字符串的副本,并付給另一個(gè)變量名稱s2
。
運(yùn)行上面的代碼,在控制臺(tái)輸出的結(jié)果如下:
您可以看到ReferenceEquals
返回false
,這意味著這兩個(gè)變量是不同的實(shí)例,但是==運(yùn)算符和Equals
方法返回的均是true。在String
類型中,==運(yùn)算符執(zhí)行的結(jié)果與Equals
執(zhí)行的結(jié)果一樣。
同樣我們使用過ildasm.exe工具反編譯查看生成IL代碼。
在這里我們沒有看到ceq
指令,對(duì)String
類型使用==運(yùn)算符判斷相等時(shí),調(diào)用的是一個(gè)op_equality(string,string)
的新方法,該方法需要兩個(gè)String
類型的參數(shù),那么它到底是什么呢?
答案是String
類型提供了==運(yùn)算符的重載。在C#中,當(dāng)我們定義一個(gè)類型時(shí),我們可以重載該類型的==運(yùn)算符;例如,對(duì)于以前的例子中我們實(shí)現(xiàn)的Person
類,如果我們?yōu)樗剌d==運(yùn)算符,大致的代碼如下:
1 public class Person 2 { 3 4 public string Name { get; set; } 5 6 public static bool operator ==(Person p1, Person p2) 7 { 8 // 注意這里不能使用==,否則會(huì)導(dǎo)致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 }
上面的代碼很簡(jiǎn)單,我們實(shí)現(xiàn)了==運(yùn)算符重載,這是一個(gè)靜態(tài)方法,但這里要注意的是,方法的名稱是perator ==
,與靜態(tài)方法的相似性;事實(shí)上,它們會(huì)被由編譯器成一個(gè)名稱為op_Equality()
的特殊靜態(tài)方法。
為了使用事情更加清楚,我們查看微軟實(shí)現(xiàn)的String
類型。
在上面的截圖中,我們可以看到,有兩個(gè)運(yùn)算符的重載,一個(gè)用于相等,另一個(gè)是不等式運(yùn)算符,其運(yùn)算方式完全相同,但是否定等于運(yùn)算符輸出。需要注意的一點(diǎn)是,如果您想重載一個(gè)類型的==運(yùn)行符的實(shí)現(xiàn),那么您還需要重載!=操作符的實(shí)現(xiàn),否則編譯會(huì)報(bào)錯(cuò)。
在演示值類型的示例前,我們先將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 }
當(dāng)我們?cè)趪L試編譯上述代碼時(shí),VS將提示如下錯(cuò)誤:
根據(jù)錯(cuò)誤提示,我們需要實(shí)現(xiàn)Person結(jié)構(gòu)體的==運(yùn)算符重載,重載的語句如下(忽略具體的邏輯):
1 public static bool operator ==(Person p1, Person p2)2 {3 }4 public static bool operator !=(Person p1, Person p2)5 {6 }
當(dāng)添加上面代碼后,重新編譯程序,通過ildasm.exe工具反編譯查看IL代碼,發(fā)現(xiàn)值類型==運(yùn)算符調(diào)用也是op_Equality
方法。
關(guān)于值類型,我們還需要說明一個(gè)問題,在不重寫Equals(object)
方法時(shí),該方法實(shí)現(xiàn)的原理是通過反射遍歷所有字段并檢查每個(gè)字段的相等性,關(guān)于這一點(diǎn),我們不演示;對(duì)于值類型,最好重寫該方法。
我們編寫另一段示例代碼,聲明兩個(gè)String
類型變量,通過4種不同的方式比較運(yùn)算:
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 }
輸出的結(jié)果如下:
首先,我們使用ReferenceEquals
方法判斷兩個(gè)String
變量都引用相同,接下來我們?cè)偈褂脤?shí)例方法Equals(string)
,在第三行,我們使用==運(yùn)算符,最后,我們使用靜態(tài)方法Object.quals(object,object)
(該方法最終調(diào)用的是String
類型重寫的Object.Equals(object)
方法)。我們得到結(jié)論是:
ReferenceEquals
方法返回false
,因?yàn)樗鼈儾皇峭粋€(gè)對(duì)象的引用;
String
類型的Equals(string)
方法返回也是true
,因?yàn)閮蓚€(gè)String
類型是相同的(即相同的序列或字符);
==運(yùn)算符也將返回true
,因?yàn)檫@兩個(gè)String
類型的值相同的;
虛方法Object.Equals
也將返回true
,這是因?yàn)樵?code>String類型重寫了方法,判斷的是String
是否值相同。
現(xiàn)在我們來修改一下這個(gè)代碼,將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 }
運(yùn)行的結(jié)果如下:
第三種方法返回的結(jié)果與修改之前不一致,==運(yùn)算符返回的結(jié)果是false
,這是為什么呢?
這是因?yàn)?=運(yùn)算符實(shí)際上是一個(gè)靜態(tài)的方法,對(duì)一非虛方法,在編譯時(shí)就已經(jīng)決定用調(diào)用的是哪一個(gè)方法。在上面的例子中,引用類型使用的是ceq
指令,而String
類型調(diào)用是靜態(tài)的op_Equality
方法;這兩個(gè)實(shí)例不是同一個(gè)對(duì)象的引用,所以ceq
指令執(zhí)行后的結(jié)果是false
。
再來說一下==運(yùn)算符與泛型的問題,我們創(chuàng)建一個(gè)簡(jiǎn)單的方法,通過泛型方法判斷兩個(gè)泛型參數(shù)是否相等并在控制臺(tái)上打印出結(jié)果:
1 static void Equals(T a, T b)2 {3 Console.WriteLine(a == b);4 }
但是當(dāng)我們編譯這段代碼時(shí),VS提示如下錯(cuò)誤:
上面顯示的錯(cuò)誤很簡(jiǎn)單,不能使用==運(yùn)算符比較兩個(gè)泛型T。因?yàn)門可以是任何類型,它可以是引用類型、值類型,不能提供==運(yùn)算符的具體實(shí)現(xiàn)。
如果像下面這樣修改一下代碼:
1 static void Equals(T a, T b) where T : class2 {3 Console.WriteLine(a == b);4 }
當(dāng)我們將泛型類型T改為引用類型,能成功編譯;修改Main
方法中的代碼,創(chuàng)建兩個(gè)相同的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 a, T b) where T : class12 {13 Console.WriteLine(a == b);14 }15 }
輸出的結(jié)果如下:
結(jié)果與您預(yù)期的結(jié)果不一樣吧,我們期待的結(jié)果是true
,輸出的結(jié)果是false
。不過仔細(xì)思考一下,也許會(huì)找到答案,因?yàn)榉盒偷募s束是引用類型,==運(yùn)算符對(duì)于引用類型使用的是引用相等,IL代碼可以證明這一點(diǎn):
如果我們泛型方法中的==運(yùn)算符改為使用Equals
方法,代碼如下:
1 static void Equals(T a, T b)2 {3 Console.WriteLine(object.Equals(a, b));4 }
我們改用Equals
,也可以去掉class
約束;如果我們?cè)俅芜\(yùn)行代碼,控制臺(tái)打印的結(jié)果與我們預(yù)期的一致,這是因?yàn)檎{(diào)用是虛方法object.Equals(object)
重寫之后的實(shí)現(xiàn)。
但是其它的問題來了,如果對(duì)于值類型,這里就會(huì)產(chǎn)生裝箱,有沒有解決的辦法呢?關(guān)于這一點(diǎn),我們直接給出答案,有時(shí)間專門來討論這個(gè)問題。
將比較的值類型實(shí)現(xiàn)IEquatable
接口,并將比較的代碼改為如下,這樣可以避免裝箱(關(guān)于這一點(diǎn),可以參考老趙的博客:http://blog.zhaojie.me/2013/04/dont-go-half-way-of-preventing-boxing.html):
1 static void Equals(T a, T b)2 {3 Console.WriteLine(EqualityComparer .Default.Equals(a, b));4 }