目錄
- 簡介
- 值型別和參考型別的相等比較
- 和相等比較相關的函式
- string 和 System.Uri 的等值比較
- 泛型介面 IEquatable
- 自定義比較方法
- 舉例
- 總結
簡介
最近正在看《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 屬于值型別,其變數 v1 和 v2 均為3,從輸出的結果可以看到,二者確實是相等的,但是對于 object 這種參考型別來說,即使是同一個 int 型資料轉換而來(由int型資料裝箱),其二者也不是同一個參考,因而并不相等(即第6行),但是對于 r3 來說,均是參考 r1 所指的物件,因而 r3 和 r1 相等,
雖然說值型別比較按照值比較,參考型別按照是否參考同一個資料比較,然而,也有一些特別的情況,典型的例子就是字串 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
可以看到,這兩個資料型別打破了之前給出的規則,雖然說 string 和 System.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個函式一一介紹:
- 類成員方法
Equals,該方法的作用是將當前使用的物件和傳入的物件進行比較,如果一致則認為是相等,該方法被設定為virtual,即在子類中可以重寫該方法, - 類成員方法
GetHashCode,該方法主要用在哈希處理中,比如哈希表和字典類中,對于這個函式,它有一個基本的要求,如果兩個物件認定為相等,則它們會回傳相同的哈希值,對于不同的物件,該函式沒有要求一定要回傳不同的哈希值,但是希望盡可能地回傳不同地哈希值,以便在哈希處理時能夠區分不同的物件資料,和上面方法一樣,因virtual關鍵字修飾,同樣可以在子類中被重寫, - 靜態成員方法
ReferenceEquals,該方法主要用來判斷兩個參考是否指向同一個物件,在 原始碼 中也可以看到,其本質就一句話:return objA == objB;,由于該方法是靜態方法,因此無法重寫, - 靜態成員方法
Equals,對于該方法,從原始碼中也可以看到,首先判斷兩個參考是否相同,在不相同的情況下,再利用物件方法Equals判斷二者是否相等,同樣的,由于該方法是靜態方法,也是無法重寫的,
string 和 System.Uri 的等值比較
好了,我們回到原先的問題上來,為什么string 和 System.Uri 表現行為和其他參考型別不一樣,反而和值型別類似,其實,嚴格上來說,string 和 System.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) 方法,這樣就避免了值型別的裝箱操作,
自定義比較方法
在有時候,為了更好模擬現實中的場景,我們需要自定義兩個個體之間的比較,為了實作這樣的比較方法,通常有三步需要完成:
- 重寫
Equals(object obj)和GetHashCode()方法; - 多載運算子
==和!=; - 實作
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#中的相等比較參照的是這樣一條規律:值型別比較的是值是否相等,而參考型別比較的則是二者是否參考同一個物件,此外,本文還介紹了一些和相等判斷有關的函式和介面,這些函式和介面的作用在于構建了一個相等比較的框架,通過這些函式和介面,不僅可以使用默認的比較規則,而且我們還可以自定義比較規則,在本文的最后,我們還給出了一個例子來模擬自定義比較規則的用途,通過該例子,我們可以清楚地看到自定義比較的實作,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/117676.html
標籤:C#
上一篇:QR 碼詳解(下)
