目录
- 简介
- 值范例和引用范例的相等比较
- 和相等比较相关的函数
string 和 System.Uri 的等值比较
- 泛型接口
IEquatable
- 自定义比较方法
- 举例
- 总结
简介
近来正在看《C# in a nutshell》这本书,可以看到虽然 .NET 框架有一些不敷和缺憾,但是团体上来说其计划照旧比较精良的。这里,本文计划从C#语言对两个对象之间的比较举行相关论述。
值范例和引用范例的相等比较
在C#中,我们知道对于不同的数据范例,其比较的方式不同。最典范的就是,值范例比较的是二者的值是否相等,而引用范例则比较的是二者是否引用了同一个对象。下面这个例子就可以看到其二者的区别。 - <code>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</code>
复制代码在这个例子中,范例 int 属于值范例,其变量 v1 和 v2 均为3。从输出的结果可以看到,二者确实是相等的。但是对于 object 这种引用范例来说,纵然是同一个 int 型数据转换而来(由int 型数据装箱),其二者也不是同一个引用,因而并不相等(即第6行)。但是对于 r3 来说,均是引用 r1 所指的对象,因而 r3 和 r1 相等。
虽然说值范例比较按照值比较,引用范例按照是否引用同一个数据比较。然而,也有一些特别的情况。典范的例子就是字符串 string 以及 System.Uri 。这两类数据范例虽然是引用范例(本质上都是类),但其在相等判定上所体现的结果却和值范例类似。 - <code>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
- </code>
复制代码可以看到,这两个数据范例冲破了之前给出的规则。虽然说 string 和 System.Uri 两个类的比较结果相似,但二者详细实现的行为并不相同。那么不同的数据范例比较详细是怎么样的流程,以及怎样自定义比较方式将会在后续部门举行讨论。但我们首先来看下在C#中相等逻辑是怎样举行处置惩罚的。
和相等比较相关的函数
在C#的语言体系中,可以知道类 Object 是整个所有数据范例的根类。从 .NET Core 3.0 中的 Object 可以看到,与等值判定相关的函数有4个,其中2个为类成员方法,2个为类静态成员方法,如下所示: - <code>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);</code>
复制代码可以留意到一点,这里和其他资料内里并不完全一样,唯逐一点区别就是传入的参数范例是 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 不同,对于如许的类对象来说,新建了多少个对象就会在堆上开辟相对应数目个的内存空间并存放数据。然而在比较时,比较方法采取的是先比较引用再比较值。即当二者并不是引用到同一个对象时再比较其值是否相等(源码)。 - <code>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</code>
复制代码以上例子可以看出,两个字符串变量均指向了同一个数据对象(ReferenceEquals 方法是判定两个引用是否引用同一个对象,这里可以看到返回值为 true )。而对于 System.Uri 来说,两个变量并没有指向同一个对象,然而后续相等判定时二者依旧相等,这时间可以看出此时根据二者的值来判定是否相等。
泛型接口 IEquatable
从以上的例子中可以看到,C#中对两个对象是否相等根本上通过 Equals 方法来判定。然而,Equals 方法也并不是万能的,这一点尤其体现在值范例当中。
由于 Equals 方法要求传入的参数范例是 object 。如果将该方法应用到值范例上,会导致将值范例强制转换到 object 范例上,也就是会装箱(boxing)一次。装箱和拆箱一样平常比较耗时,容易降低服从。别的,object 范例意味着该类对象可以和任意其他类对象举行相等判定,但是一样平常而言,我们判定两个对象是否相等的前提肯定都是同一个类的对象。
C#所采取的管理办法是使用泛型接口 IEquatable 来管理。IEquatable 重要包罗两个方法,如下所示: - <code>public interface IEquatable<T>
- {
- bool Equals(T other);
- }</code>
复制代码和Object.Equals(object? obj) 相比,其内部的函数为泛型方法,如果一个类大概布局体等数据实现了该接口,那么当调用 Equals 方法时,根据范例最顺应的原则,那么会首先调用 IEquatable 内的 Equals(T other) 方法。如许就避免了值范例的装箱操纵。
自定义比较方法
在有时间,为了更好模仿现实中的场景,我们需要自定义两个个体之间的比较。为了实现如许的比较方法,通常有三步需要完成:
- 重写
Equals(object obj) 和 GetHashCode() 方法;
- 重载操纵符
== 和 != ;
- 实现
IEquatable 方法;
对于第一点来说,这两个函数是必须要重写的。对于 Equals(object obj) 的实现的话,如果实现了泛型接口内的方法,可以思量这里直接调用该方法即可。GetHashCode() 用于尽大概区分不同对象,所以如果两个对象相等的话,其哈希值也应该相等,如许在哈希表以及字典类中会有比较好的性能。
对于第二点和第三点来说,并不是必须的,但是一样平常地,为了更好地使用,这两点最好需要举行重载。
可以看到,这三点均涉及到比较的逻辑。一样平常而言,我们倾向于把比较的核心逻辑放在泛型接口中,对于其他方法,通过调用泛型接口内的方法即可。
举例
这里,我们举一个小例子。设想如许一个场景,现在机器学习越来越火热,而谈及机器学习离不开矩阵运算。对于矩阵,我们可以使用二维数组来保存。在数学范畴中,我们判定两个矩阵是否相等,是判定两个矩阵内的每个元素是否相等,也就是值范例的判定方式。而在C#中,由于二维数组是引用范例,直接使用相等判定无法到达这一目标。因此,我们需要修改其判定方式。 - <code> 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</code>
复制代码比较的逻辑实现放在 Equals(Matrix other) 中。在该方法中,首先判定两个矩阵是否引用了同一个二维数组,之后判定行列的数目是否相等,末了再按照每个元素举行判定。整个核心逻辑就在这里。对于 Equals(object obj) 以及 == 和 != 则直接调用 Equals(Matrix other) 方法。留意一点,在重载 == 符号时,不能直接用 m1==null 来判定第一个对象是否为空,否则的话就是无限循环调用 == 操纵符重载函数。在该函数中需要需要举行引用判定的话,可以使用 Object 类中的静态方法ReferenceEquals 来判定。
总结
总体而言,C#中的相等比较参照的是如许一条规律:值范例比较的是值是否相等,而引用范例比较的则是二者是否引用同一个对象。别的,本文还先容了一些和相等判定有关的函数和接口,这些函数和接口的作用在于构建了一个相等比较的框架。通过这些函数和接口,不仅可以使用默认的比较规则,而且我们还可以自定义比较规则。在本文的末了,我们还给出了一个例子来模仿自定义比较规则的用途。通过该例子,我们可以清楚地看到自定义比较的实现。
来源:https://www.cnblogs.com/iskcal/p/csharp-equal-compare-1.html |