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