C#中的等值判斷1

来源:https://www.cnblogs.com/iskcal/archive/2019/10/16/csharp-equal-compare-1.html
-Advertisement-
Play Games

在C#中,我們可以通過 a==b 的形式來判斷兩個引用是否相等。然而,在其系統中,相等判斷是根據行為的不同而得到不同的結果:一方面,預設的值類型採用值比較的方式來判斷相等性;另一方面,預設的引用類型通過判斷兩個引用是否引用同一個對象而判斷相等性。此外,在C#中,可以通過重載某些函數和邏輯來達到改寫相... ...


目錄

簡介

最近正在看《C# in a nutshell》這本書,可以看到雖然 .NET 框架有一些不足和缺憾,但是整體上來說其設計還是比較優秀的。這裡,本文打算從C#語言對兩個對象之間的比較進行相關闡述。

值類型和引用類型的相等比較

在C#中,我們知道對於不同的數據類型,其比較的方式不同。最典型的就是,值類型比較的是二者的值是否相等,而引用類型則比較的是二者是否引用了同一個對象。下麵這個例子就可以看到其二者的區別。

int v1 = 3, v2 = 3;
object r1 = v1;
object r2 = v1;
object r3 = r1;
Console.WriteLine($"v1 is equal to v2: {v1 == v2}");    // true
Console.WriteLine($"r1 is equal to r2: {r1 == r2}");    // false
Console.WriteLine($"r1 is equal to r3: {r1 == r3}");    // true

在這個例子中,類型 int 屬於值類型,其變數 v1v2 均為3。從輸出的結果可以看到,二者確實是相等的。但是對於 object 這種引用類型來說,即使是同一個 int 型數據轉換而來(由int型數據裝箱),其二者也不是同一個引用,因而並不相等(即第6行)。但是對於 r3 來說,均是引用 r1 所指的對象,因而 r3r1 相等。

雖然說值類型比較按照值比較,引用類型按照是否引用同一個數據比較。然而,也有一些特別的情況。典型的例子就是字元串 string 以及 System.Uri 。這兩類數據類型雖然是引用類型(本質上都是類),但其在相等判斷上所表現的結果卻和值類型類似。

string s1 = "test";
string s2 = "test";
Uri u1 = new Uri("https://www.bing.com");
Uri u2 = new Uri("https://www.bing.com");
Console.WriteLine($"s1 is equal to s2: {s1 == s2}");    // true
Console.WriteLine($"u1 is equal to u2: {u1 == u2}");    // true

可以看到,這兩個數據類型打破了之前給出的規則。雖然說 stringSystem.Uri 兩個類的比較結果相似,但二者具體實現的行為並不相同。那麼不同的數據類型比較具體是怎麼樣的流程,以及如何自定義比較方式將會在後續部分進行討論。但我們首先來看下在C#中相等邏輯是如何進行處理的。

和相等比較相關的函數

在C#的語言體系中,可以知道類 Object 是整個所有數據類型的根類。從 .NET Core 3.0 中的 Object 可以看到,與等值判斷相關的函數有4個,其中2個為類成員方法,2個為類靜態成員方法,如下所示:

public virtual bool Equals(object? obj);
public virtual int GetHashCode();
public static bool ReferenceEquals(object? objA, object? objB);
public static bool Equals(object? objA, object? objB);

可以註意到一點,這裡和其他資料裡面並不完全一樣,唯一一點區別就是傳入的參數類型是 object? 而不是 object。這主要是C#在8.0版本中引入的可空引用類型。這裡可空引用類型並不是本文的重點,這裡完全可以當作是 object 來處理。

這裡我們對這4個函數一一介紹:

  1. 類成員方法 Equals 。該方法的作用是將當前使用的對象和傳入的對象進行比較,如果一致則認為是相等。該方法被設置為virtual,即在子類中可以重寫該方法。
  2. 類成員方法 GetHashCode 。該方法主要用在哈希處理中,比如哈希表和字典類中。對於這個函數,它有一個基本的要求,如果兩個對象認定為相等,則它們會返回相同的哈希值。對於不同的對象,該函數沒有要求一定要返回不同的哈希值,但是希望儘可能地返回不同地哈希值,以便在哈希處理時能夠區分不同的對象數據。和上面方法一樣,因 virtual 關鍵字修飾,同樣可以在子類中被重寫。
  3. 靜態成員方法 ReferenceEquals 。該方法主要用來判斷兩個引用是否指向同一個對象。在 源碼 中也可以看到,其本質就一句話:return objA == objB;。由於該方法是靜態方法,因此無法重寫。
  4. 靜態成員方法 Equals。對於該方法,從源碼中也可以看到,首先判斷兩個引用是否相同,在不相同的情況下,再利用對象方法 Equals 判斷二者是否相等。同樣的,由於該方法是靜態方法,也是無法重寫的。

stringSystem.Uri 的等值比較

好了,我們回到原先的問題上來,為什麼stringSystem.Uri 表現行為和其他引用類型不一樣,反而和值類型類似。其實,嚴格上來說,stringSystem.Uri 的對象比較雖然表現上類似於值類型,但是二者內部的細節並不一樣。

對於 string 來說,大部分情況下,在一個程式副本當中,一個字元串只會被保存一次,無論新建多少個字元串變數,只要其值相同,那麼均會引用到同一個記憶體地址上。所以對於字元串的比較,其依舊是比較引用,只不過值相同的大多是引用到同一個對象上。

System.Uri 不同,對於這樣的類對象來說,新建了多少個對象就會在堆上開闢相對應數目個的記憶體空間並存放數據。然而在比較時,比較方法採用的是先比較引用再比較值。即當二者並不是引用到同一個對象時再比較其值是否相等(源碼)。

string s1 = "test";
string s2 = "test";
Uri u1 = new Uri("https://www.bing.com");
Uri u2 = new Uri("https://www.bing.com");
Console.WriteLine($"s1 is equal to s2 by the reference: {Object.ReferenceEquals(s1, s2)}"); // true
Console.WriteLine($"s1 is equal to s2: {s1 == s2}");    // true
Console.WriteLine($"u1 is equal to u2 by the reference: {Object.ReferenceEquals(u1, u2)}"); // false
Console.WriteLine($"u1 is equal to u2: {u1 == u2}");    // true

以上例子可以看出,兩個字元串變數均指向了同一個數據對象(ReferenceEquals 方法是判斷兩個引用是否引用同一個對象,這裡可以看到返回值為 true)。而對於 System.Uri 來說,兩個變數並沒有指向同一個對象,然而後續相等判斷時二者依舊相等,這時候可以看出此時根據二者的值來判斷是否相等。

泛型介面 IEquatable<T>

從以上的例子中可以看到,C#中對兩個對象是否相等基本上通過 Equals 方法來判斷。然而,Equals 方法也並不是萬能的,這一點尤其體現在值類型當中。

由於 Equals 方法要求傳入的參數類型是 object。如果將該方法應用到值類型上,會導致將值類型強制轉換到 object 類型上,也就是會裝箱(boxing)一次。裝箱和拆箱一般比較耗時,容易降低效率。此外,object類型意味著該類對象可以和任意其他類對象進行相等判斷,但是一般而言,我們判斷兩個對象是否相等的前提肯定都是同一個類的對象。

C#所採用的解決辦法是使用泛型介面 IEquatable<T> 來解決。IEquatable<T> 主要包含兩個方法,如下所示:

public interface IEquatable<T>
{
    bool Equals(T other);
}

Object.Equals(object? obj) 相比,其內部的函數為泛型方法,如果一個類或者結構體等數據實現了該介面,那麼當調用 Equals 方法時,根據類型最適應的原則,那麼會首先調用 IEquatable<T> 內的 Equals(T other) 方法。這樣就避免了值類型的裝箱操作。

自定義比較方法

在有時候,為了更好模擬現實中的場景,我們需要自定義兩個個體之間的比較。為了實現這樣的比較方法,通常有三步需要完成:

  1. 重寫 Equals(object obj)GetHashCode() 方法;
  2. 重載操作符 ==!=
  3. 實現 IEquatable<T> 方法;

對於第一點來說,這兩個函數是必須要重寫的。對於 Equals(object obj) 的實現的話,如果實現了泛型介面內的方法,可以考慮這裡直接調用該方法即可。GetHashCode() 用於儘可能區分不同對象,所以如果兩個對象相等的話,其哈希值也應該相等,這樣在哈希表以及字典類中會有比較好的性能。

對於第二點和第三點來說,並不是必須的,但是一般地,為了更好地使用,這兩點最好需要進行重載。

可以看到,這三點均涉及到比較的邏輯。一般而言,我們傾向於把比較的核心邏輯放在泛型介面中,對於其他方法,通過調用泛型介面內的方法即可。

舉例

這裡,我們舉一個小例子。設想這樣一個場景,目前機器學習越來越火熱,而談及機器學習離不開矩陣運算。對於矩陣,我們可以使用二維數組來保存。在數學領域中,我們判斷兩個矩陣是否相等,是判斷兩個矩陣內的每個元素是否相等,也就是值類型的判斷方式。而在C#中,由於二維數組是引用類型,直接使用相等判斷無法達到這一目的。因此,我們需要修改其判斷方式。

   public class Matrix : IEquatable<Matrix>
    {
        private double[,] matrix;

        public Matrix(double[,] m)
        {
            matrix = m;
        }

        public bool Equals([AllowNull] Matrix other)
        {
            if (Object.ReferenceEquals(other, null))
                return false;
            if (matrix == other.matrix)
                return true;
            if (matrix.GetLength(0) != other.matrix.GetLength(0) ||
                matrix.GetLength(1) != other.matrix.GetLength(1))
                return false;
            for (int row = 0; row < matrix.GetLength(0); row++)
                for (int col = 0; col < matrix.GetLength(1); col++)
                    if (matrix[row,col] != other.matrix[row,col])
                        return false;
            return true;
        }

        public override bool Equals(object obj)
        {
            if (!(obj is Matrix)) return false;
            return Equals((Matrix)obj);
        }

        public override int GetHashCode()
        {
            int hashcode = 0;
            for (int row = 0; row < matrix.GetLength(0); row++)
                for (int col = 0; col < matrix.GetLength(1); col++)
                    hashcode = (hashcode + matrix[row, col].GetHashCode()) % int.MaxValue;
                return hashcode;
        }

        public static bool operator == (Matrix m1, Matrix m2)
        {
            return Object.ReferenceEquals(m1, null) ? Object.ReferenceEquals(m2, null) : m1.Equals(m2);

        }
        public static bool operator !=(Matrix m1, Matrix m2)
        {
            return !(m1 == m2);

        }
    }
    
Matrix m1 = new Matrix(new double[,] { { 1, 2, 3 }, { 4, 5, 6 } });
Matrix m2 = new Matrix(new double[,] { { 1, 2, 3 }, { 4, 5, 6 } });

Console.WriteLine($"m1 is equal to m2 by the reference: {Object.ReferenceEquals(m1, m2)}");     // false
Console.WriteLine($"m1 is equal to m2: {m1 == m2}");    //true

比較的邏輯實現放在 Equals(Matrix other) 中。在該方法中,首先判斷兩個矩陣是否引用了同一個二維數組,之後判斷行列的數目是否相等,最後再按照每個元素進行判斷。整個核心邏輯就在這裡。對於 Equals(object obj) 以及 ==!= 則直接調用 Equals(Matrix other) 方法。註意一點,在重載 == 符號時,不能直接用 m1==null 來判斷第一個對象是否為空,否則的話就是無限迴圈調用 == 操作符重載函數。在該函數中需要需要進行引用判斷的話,可以使用 Object 類中的靜態方法ReferenceEquals 來判斷。

總結

總體而言,C#中的相等比較參照的是這樣一條規律:值類型比較的是值是否相等,而引用類型比較的則是二者是否引用同一個對象。此外,本文還介紹了一些和相等判斷有關的函數和介面,這些函數和介面的作用在於構建了一個相等比較的框架。通過這些函數和介面,不僅可以使用預設的比較規則,而且我們還可以自定義比較規則。在本文的最後,我們還給出了一個例子來模擬自定義比較規則的用途。通過該例子,我們可以清楚地看到自定義比較的實現。


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

-Advertisement-
Play Games
更多相關文章
  • 1. 統計字元(可以在jieba分詞之後使用) 2. 多次覆蓋,迴圈寫入文件 比如,迴圈兩次的結果是: 3. 一次性寫入文件,中間不會覆蓋和多次寫入;但是如果重覆運行代碼,則會覆蓋之前的全部內容,一次性重新寫入所有新內容 ...
  • IntelliJ快捷鍵 導入包 alt + enter 刪除游標所在行 ctrl + y 複製游標所在行 ctrl + d 格式代碼 ctrl + alt + l 單行註釋 ctrl + / 多行註釋 ctrl + shift + / 自動生成代碼 alt + ins 移動代碼 alt + shif ...
  • "Python3 多進程編程(Multiprocess programming)" "為什麼使用多進程" "具體用法" "Python多線程的通信" "進程對列Queue" "生產者消費者問題" "JoinableQueue" "Queue實例" "管道Pipe" Python3 多進程編程(Mul ...
  • Java開發過程中的常用工具類庫 [TOC] Apache Commons類庫 Apache Commons是一個非常有用的工具包,為解決各種實際的問題提供了通用現成的代碼,不需要我們程式員再重覆造輪子。關於這個類庫的詳細介紹可以訪問 "官網介紹" 。下麵表格列出了部分的工具包。我們平時開發過程中可 ...
  • 今天被一個很簡單的坑到了,還想了很長時間,insert 函數,真的知道它內部執行的操作嗎? 開始其實是在看一本演算法的書,書裡面給了兩段工作內容差不多的偽代碼 第一段如下: 第二段如下: 最開始感覺第二中代碼中計算量不是應該比第一段多了一個計算長度的部分嗎?應該是最二種時間花費更多,事實上len(da ...
  • 前言 學習路線圖往往是學習一樣技術的入門指南。網上搜到的Java學習路線圖也是一抓一大把。 今天我只選一張圖,僅此一圖,足以包羅Java後端技術的知識點。所謂不求最好,但求最全,學習Java後端的同學完全可以參考這張圖進行學習路線安排。 當然,有一些知識點是可選的,並不是說上面有的你都要會啦。我在復 ...
  • 阿裡面經 "阿裡中間件研發麵經" "螞蟻金服研發麵經" 崗位是研發工程師,直接找螞蟻金服的大佬進行內推。 我參與了阿裡巴巴中間件部門的提前批面試,一共經歷了四次面試,拿到了口頭offer。 然後我也參加了螞蟻金服中間件部門的面試,經歷了三次面試,但是沒有走流程,所以面試中止了。 最後我走的是螞蟻金服 ...
  • 最近由於工作原因,一直忙於公司的各種項目(大部份都是基於spring cloud的微服務項目),故有一段時間沒有與大家分享總結最近的技術研究成果的,其實最近我一直在不斷的深入研究學習Spring、Spring Boot、Spring Cloud的各種框架原理,同時也隨時關註著.NET CORE的發展 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...