在.Net框架中,如果您查看所有類型的的基類:System.Object類,將找到如下4個與相等判斷的方法: static Equals() virtual Equals() static ReferenceEquals() virtual GetHashCode() 除此之外,Microsoft已 ...
在.Net框架中,如果您查看所有類型的的基類:System.Object類,將找到如下4個與相等判斷的方法:
除此之外,Microsoft已經提供了9個不同的介面,用於比較類型:
- IEquatable<T>
- IComparable
- IComparable<T>
- IComparer
- IComparer<T>
- IEqualityComparer
- IEqualityComparer<T>
- IStructuralEquatable
- IStructuralComparable
您是否真的理解方法這些方法和介面?如果使用不當,可能會產生致命的錯誤,並且還會破壞依賴於這些介面的集合。
接下來我們幾篇博客來討論這些方法和介面,重點關註的是如何正確使用這些方法和介面。
等於的疑惑
因為存在以下四種原因,會阻礙我們理解相等比較是如何執行:
- 引用相等與值相等
- 判斷值相等的多種方式
- 浮點數的準確性
- 與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 Animal 2 { 3 4 } 5 6 public class Dog : Animal 7 { 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
類型為參數類型,這意味著它不是類型安全的,但它能夠正常重寫基類的方法,並且這也是最簡單的解決辦法。
總結
- C#在語法上不區分值相等和引用相等,這意味著有時候很難預測在特定情況下"
==
"運算符是如何執行; - 存在多種方式實現值相等判斷,.Net框架允許類型定義預設的值比較方式,同時提供自己編寫比較器的機制來實現每種類型的值比較;
- 不建議使用浮點數進行值相等比較,因為舍入誤差可能導致結果超出預期;
- 值相等、類型安全和良好的面向對象之間存在衝突。
轉載請註明出自,原文鏈接:http://www.cnblogs.com/tdfblog/p/Story-of-Equality-in-NET-Part1.html