.Net中關於相等的問題

来源:http://www.cnblogs.com/tdfblog/archive/2017/06/21/About-Equality-in-NET.html
-Advertisement-
Play Games

在.Net框架中,如果您查看所有類型的的基類:System.Object類,將找到如下4個與相等判斷的方法: static Equals() virtual Equals() static ReferenceEquals() virtual GetHashCode() 除此之外,Microsoft已 ...


  在.Net框架中,如果您查看所有類型的的基類:System.Object類,將找到如下4個與相等判斷的方法:

  除此之外,Microsoft已經提供了9個不同的介面,用於比較類型:

   您是否真的理解方法這些方法和介面?如果使用不當,可能會產生致命的錯誤,並且還會破壞依賴於這些介面的集合。

  接下來我們幾篇博客來討論這些方法和介面,重點關註的是如何正確使用這些方法和介面。

 

等於的疑惑

  因為存在以下四種原因,會阻礙我們理解相等比較是如何執行:

  1. 引用相等與值相等
  2. 判斷值相等的多種方式
  3. 浮點數的準確性
  4. 與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類的兩個實例包含的值相同,但它們是單獨的實例,變數p1p2兩者分別指記憶體不同的位置。

  引用相等執行速度非常快,因為只需檢查兩個變數是否指向記憶體中相同的地址,而對於值相等要慢一些。例如,如果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()方法,但是麻煩的是DogEquals()方法與Animal類的Equals()方法使用的是不同的參數類型,實際是重寫不了Animal類的Equals()方法。如果您不夠仔細,可能會調用錯誤的Equals方法,最終返回錯誤的結果。

  通常的解決辦法是重寫Object類型Equals方法;該方法採用一個Object類型為參數類型,這意味著它不是類型安全的,但它能夠正常重寫基類的方法,並且這也是最簡單的解決辦法。

 

總結

  • C#在語法上不區分值相等和引用相等,這意味著有時候很難預測在特定情況下"=="運算符是如何執行;
  • 存在多種方式實現值相等判斷,.Net框架允許類型定義預設的值比較方式,同時提供自己編寫比較器的機制來實現每種類型的值比較;
  • 不建議使用浮點數進行值相等比較,因為舍入誤差可能導致結果超出預期;
  • 值相等、類型安全和良好的面向對象之間存在衝突。

 

  轉載請註明出自,原文鏈接:http://www.cnblogs.com/tdfblog/p/Story-of-Equality-in-NET-Part1.html

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • using System.Text.RegularExpressions; namespace DotNet.Utilities { /// <summary> /// 漢字轉拼音類 /// </summary> public class EcanConvertToCh { //定義拼音區編碼數組 ...
  • 因為沒有AspNetCoreModule所以無法正常運行 http://download.microsoft.com/download/3/8/1/381CBBF3-36DA-4983-BFF3-5881548A70BE/DotNetCore.1.0.4_1.1.1-WindowsHosting.e ...
  • using System.Xml; using System.Data; namespace DotNet.Utilities { /// <summary> /// Xml的操作公共類 /// </summary> public class XmlHelper { #region 欄位定義 /// ...
  • https加密鏈接,在訪問的過程中,可以保護你的隱私,保證你的敏感數據不會被別人偷窺,竊取。如果你的伺服器在境外,使用https,可以有更穩定的訪問體驗。接下來我們看一下ZKEACMS如何配置使用HTTPS。 ...
  • using System; using System.Security.Cryptography; using System.Text; namespace DotNet.Utilities { /// <summary> /// Encrypt 的摘要說明。 /// </summary> publ ...
  • 一、使用線程的理由 1、可以使用線程將代碼同其他代碼隔離,提高應用程式的可靠性。 2、可以使用線程來簡化編碼。 3、可以使用線程來實現併發執行。 二、基本知識 1、進程與線程:進程作為操作系統執行程式的基本單位,擁有應用程式的資源,進程包含線程,進程的資源被線程共用,線程不擁有資源。 2、前臺線程和 ...
  • using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; namespace Utils { /// <summary> /// <para> </para> /// 常用 ...
  • WPF簡介 Windows Presentation Foundation(WPF)是微軟新一代圖形系統,運行在.NET Framework 3.0架構下,為用戶界面、2D/3D 圖形、文檔和媒體提供了統一的描述和操作方法。基於DirectX 9/10技術的WPF不僅帶來了前所未有的3D界面,而且其 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...