在.Net框架中,如果您查看所有類型的的基類:System.Object類,將找到如下4個與相等判斷的方法:
除此之外,Microsoft已經(jīng)提供了9個不同的接口,用于比較類型:
您是否真的理解方法這些方法和接口?如果使用不當(dāng),可能會產(chǎn)生致命的錯誤,并且還會破壞依賴于這些接口的集合。
接下來我們幾篇博客來討論這些方法和接口,重點關(guān)注的是如何正確使用這些方法和接口。
等于的疑惑
因為存在以下四種原因,會阻礙我們理解相等比較是如何執(zhí)行:
引用相等與值相等
判斷值相等的多種方式
浮點數(shù)的準(zhǔn)確性
與OOP存在的沖突
引用相等與值相等
眾所周知,在.Net框架中,引用類型在存儲時不包含實際的值,它們包含一個指向內(nèi)存中保存實際值位置的指針,這意味著對于引用類型,有兩種方式來衡量相等性;兩個變量都是指向內(nèi)存中相同的位置,我們稱為引用相等,也可以說是同一個對象;兩個變量指定的位置包括相同的值, 即使它們指向內(nèi)存中不同的位置,我們稱其之為值相等。
我們可以使用如下示例來說明上述幾點:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 Person p1 = new Person(); 6 p1.Name = "Sweet"; 7 8 Person p2 = new Person(); 9 p2.Name = "Sweet";10 11 Console.WriteLine(p1 == p2);12 } 13 }
我們實例化了兩個Person
對象,并且都包含相同的Name
屬性;顯然,上述兩個Person
類的實例是相同的,它們包含相同的值, 但是運行示例代碼時,控制臺打印輸出的是False
,這意味著它們不相等。
這是因為在.Net框架中,對于引用類型默認(rèn)判斷方式是引用相等,換句話說,"==
"運算符會判斷這兩個變量是否指向內(nèi)存中相同的位置,因此在本示例中,盡管Person
類的兩個實例包含的值相同,但它們是單獨的實例,變量p1
和p2
兩者分別指內(nèi)存不同的位置。
引用相等執(zhí)行速度非???,因為只需檢查兩個變量是否指向內(nèi)存中相同的地址,而對于值相等要慢一些。例如,如果Person
類不是只有一個字段和屬性,而是具有很多,想檢查Person
類的兩個實例是否具有相同的值,您必須檢查每個字段或?qū)傩?。C#中并沒有提供運算符用于檢查兩個類型實例的值是否相等,如果由于某種原因想要實現(xiàn)這種功能,您需要自己編寫代碼來做到這一點。
現(xiàn)在來看另一個例子:
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 string s1 = "Sweet"; 6 7 string s2 = string.Copy(s1); 8 9 Console.WriteLine(s1 == s2);10 } 11 }
上面的代碼與前一個示例代碼非常相擬,但是在這個示例中,我們使用"==
"運算符判斷兩個相同的String
類型的變量。我們先給變量s1
付值后,然后將變量s1
的值復(fù)制并付給另一個變量s2
,運行這段代碼,在控制臺打印輸出為True
,我們可以說兩個String
類型的變量是相等的。
如果"==
"運算符判斷的方式使用的是引用相等, 程序運行時控制臺打印輸出的應(yīng)該是False
,但是用于String
類型時"==
" 運算符判斷方式是值相等。
引用相等與值類型
引用相等和值相等的問題僅適用于引用類型,對于未裝箱的值類型,如整數(shù),浮點型等,變量存儲時已經(jīng)包含了實際的值,這里沒有引用的概念,意味著相等就是比較值。
以下代碼比較兩個整數(shù),兩者是相等的,因為"==
"運算符將比較變量實際的值。
1 class Program 2 { 3 static void Main(String[] args) 4 { 5 int num1 = 2; 6 7 int num2 = 2; 8 9 Console.WriteLine(num1 == num2);10 } 11 }
在上面的代碼中,"==
"運算符是將變量num1
存儲的值與變量num2
存儲的值進(jìn)行比較。但是,如果我們修改此代碼并將這兩個變量轉(zhuǎn)換為Object
類型,代碼如下:
1 int num1 = 2;2 3 int num2 = 2;4 5 Console.WriteLine((object)num1 == (object)num2);
運行示例代碼,您看到結(jié)果將是False
,與上一次代碼的結(jié)果相反。這是因為Object
類型是引用類型,所以當(dāng)我們將整數(shù)轉(zhuǎn)換為Object
類型,實際是兩個整數(shù)被裝箱后兩個不同的引用實例,"==
"運行符比較的是兩個對象的引用,而不是值。
好像上面的例子很少見,因為通常情況下我們不會將值類型轉(zhuǎn)換為引用類型,但是存在另一種常見的情況,我們需要將值類型轉(zhuǎn)換為接口。
1 Console.WriteLine((IComparable<int>)num1 == (IComparable<int>)num2);
為了說明這種情況,我們修改示例代碼,將int
類型的變量轉(zhuǎn)換為接口ICompareable<int>;
這是.Net框架提供的一個接口,int
類型實現(xiàn)這個接口(關(guān)于這個接口我們將其它的博客中討論)。
在.Net框架中,接口實際上是引用類型,如果我們運行這段代碼,返回的結(jié)果是False
。因此,在將值類型轉(zhuǎn)換為接口時,您需要特別小心,如果您進(jìn)行相等檢查,返回的結(jié)果比較的是引用相等。
"=="運算符
如果C#對值類型和引用類型分別提供不同的運算符來判斷相等,也許這些代碼都不是問題,可惜C#只提供一個"==
"運算符,也沒有顯示的方式來告訴運算符實際判斷的類型是什么。例如,下面這一行代碼:
1 Console.WriteLine(var1 == var2)
我們不知道上述的"=="運算符采用的是引用相等還是值相等,因為需要知道"==
"運行算判斷的是什么類型,事實上C#也是這樣設(shè)計的。
在上述內(nèi)容中,我們詳細(xì)介紹了"==
"運算符的作用及判斷方式,在閱讀完這篇博客之后,我希望您能比其他開發(fā)者更多的了解當(dāng)使用"==
"判斷條件的時候到底發(fā)生了什么,您也能夠更進(jìn)一步了解兩個對象之間的是如何判斷相等的。
判斷值相等的多種方式
復(fù)雜的值相等的還存在另一個問題,通常存在多種方式來比較指定類型的值,String
類型是一個最好的例子。
經(jīng)常存在這樣一種情況,字符串比較時,可能需要忽略字母的大小寫;例如:在一個電商平臺中搜索一個英文名稱的商品,此時比較商品名稱時,我們需要忽略大小寫,幸運的是在Sql Server數(shù)據(jù)庫中,默認(rèn)使用的是這種比較方式,在.Net框架中有沒有辦法滿足我們的要求?幸運的是在String
類型中提供了一個Equals
方法的重載,看下面的示例:
1 string s1 = "SWEET";2 3 string s2 = "sweet";4 5 Console.WriteLine(s1.Equals(s2,StringComparison.OrdinalIgnoreCase));
在程序中運行上面的示例,在控制臺打印輸出的是True
。
當(dāng)然.Net框架也提供了多種方式來判斷類型的值相等。最常見方法,類型可以通過實現(xiàn)IEquatable<T>
接口定義類型默認(rèn)值相等的判斷方式。如果您不想重新定義自己的類型,.Net框架也提供了其另一種機(jī)制來實現(xiàn)一點,通過實現(xiàn)IEqualityComparer<T>
接口來自定義一個比較器,用于判斷同一種類型的兩個實例是否相等。例如:如果您想忽略String類型中的空格進(jìn)行比較,可以自己定義一個比較器,來實現(xiàn)這一功能。
.Net還提供了一個接口ICompareable<T>
,用于判斷當(dāng)前類型大于或小于的比較,也可以通過IComparer<T>
接口來實現(xiàn)一個比對器,一般在對象排序時,會用到這些接口。
浮點數(shù)的準(zhǔn)確性
在.Net框架中,您如果使用到浮點數(shù),可以帶來一些意想不到的問題,讓我們來看一個例子:
1 float num1 = 2.000000f;2 float num2 = 2.000001f;3 4 Console.WriteLine(num1 == num2);
我們有兩個幾乎相等的浮點數(shù),但是很明顯,它們不一樣,因為它們在末尾的數(shù)字是不同的,我們運行程序,控制臺打印輸出的結(jié)果是True
。
從程序來角度來講,它們是相等的,這與我們預(yù)期結(jié)果矛盾。不過您可能已經(jīng)猜測到問題出在哪里了,數(shù)字類型存在一個精度問題,float
類型不能存儲足夠的有效數(shù)來區(qū)分這兩個特定的數(shù)字,并且它還存在其它運算的問題??催@個例子:
1 float num1 = 0.7f;2 float num2 = 0.6f + 0.1f;3 4 Console.WriteLine(num2);5 Console.WriteLine(num1 == num2);
這是一個簡單的計算,我們將0.6與0.1相加,非常明顯,相加后的結(jié)果是0.7,但是我們運行程序,控制臺打印輸出的結(jié)果是False
,注意結(jié)果是False,這說明計算結(jié)果不等于0.7。其原因是,浮點數(shù)在運算的過程中出現(xiàn)了舍入誤差導(dǎo)致了存儲一個非常接近的數(shù)字,雖然num2
轉(zhuǎn)換成String
類型后,在控制臺打印輸出的結(jié)果是0.7,但是num2
的值并不等于0.7。
舍入誤差意味著判斷相等通常會給您一個錯誤的結(jié)果,.Net框架沒有提供解決方案。給您的建議是,不要嘗試比較浮點數(shù)是否相等,因為可能不是預(yù)期結(jié)果。這個問題只會影響等于比較,通常不會影響小于和大于比較,在大多數(shù)情況下,比較一個浮點數(shù)是大于還是小于另一個浮點數(shù)不會出該問題。
在stackoverflow上提供這樣一個解決辦法,供大家參考:https://stackoverflow.com/questions/6598179/the-right-way-to-compare-a-system-double-to-0-a-number-int。
值相等與面向?qū)ο笾g的矛盾
這個問題對經(jīng)驗豐富的開發(fā)人員來說可能會感到很詫異,實際上這是等于比較、類型安全和良好的面向?qū)ο髮嵺`之間的沖突。這三個問題如果沒有處理好,將會帶來其它的Bug。
現(xiàn)在我們來舉這樣一個例子,假設(shè)我們有基類Animal
表示動物,派生類Dog
來表示狗。
1 public class Animal2 {3 4 }5 6 public class Dog : Animal7 {8 9 }
如果我們希望在Animal
類實現(xiàn)當(dāng)前實例是否等于其它Animal
實例,則可能需要實現(xiàn)接口IEquatable<Animal>
。這要求它定義一個Equals()
方法并以Animal
類型的實例作為參數(shù)。
1 public class Animal : IEquatable<animal>2 {3 public virtual bool Equals(Animal other)4 {5 throw new NotImplementedException();6 }7 }
如果我們希望Dog
類也實現(xiàn)當(dāng)前實例是否等于其它Dog
實例,那么可能需要實現(xiàn)接口IEquatable<Dog>
,這意味著它也定義一個Equals()
方法并以Dog
類型的實例作為參數(shù)。
1 public class Dog : Animal, IEquatable<Dog>2 {3 public virtual bool Equals(Dog other)4 {5 throw new NotImplementedException();6 }7 }
現(xiàn)在問題出現(xiàn)了,在這個一個精心設(shè)計的OOP代碼中,您可能會認(rèn)為Dog
類會覆蓋Animal
類的Equals()
方法,但是麻煩的是Dog
的Equals()
方法與Animal
類的Equals()
方法使用的是不同的參數(shù)類型,實際是重寫不了Animal
類的Equals()
方法。如果您不夠仔細(xì),可能會調(diào)用錯誤的Equals
方法,最終返回錯誤的結(jié)果。
通常的解決辦法是重寫Object
類型Equals
方法;該方法采用一個Object
類型為參數(shù)類型,這意味著它不是類型安全的,但它能夠正常重寫基類的方法,并且這也是最簡單的解決辦法。
總結(jié)
C#在語法上不區(qū)分值相等和引用相等,這意味著有時候很難預(yù)測在特定情況下"
==
"運算符是如何執(zhí)行;存在多種方式實現(xiàn)值相等判斷,.Net框架允許類型定義默認(rèn)的值比較方式,同時提供自己編寫比較器的機(jī)制來實現(xiàn)每種類型的值比較;
不建議使用浮點數(shù)進(jìn)行值相等比較,因為舍入誤差可能導(dǎo)致結(jié)果超出預(yù)期;
值相等、類型安全和良好的面向?qū)ο笾g存在沖突。
轉(zhuǎn)載請注明出自,原文鏈接:http://www.cnblogs.com/tdfblog/p/About-Equality-in-NET.html
http://www.cnblogs.com/tdfblog/p/About-Equality-in-NET.html