C#中的等值判断1
简介
最近正在看《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#中的相等比较参照的是这样一条规律:值类型比较的是值是否相等,而引用类型比较的则是二者是否引用同一个对象。此外,本文还介绍了一些和相等判断有关的函数和接口,这些函数和接口的作用在于构建了一个相等比较的框架。通过这些函数和接口,不仅可以使用默认的比较规则,而且我们还可以自定义比较规则。在本文的最后,我们还给出了一个例子来模拟自定义比较规则的用途。通过该例子,我们可以清楚地看到自定义比较的实现。
C#中的等值判断1的更多相关文章
- C# 值类型和引用类型等值判断
		
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
 - js中的等值运算符(抽象相等==与严格相等===的区别)
		
js中的等值运算符 js中的相等分为抽象相等和严格相等,他们有什么区别呢. 在说具体算法前,先提下JS数据类型,JS数据类型分为6类:Undefined Null String Number Bool ...
 - android应用中增加权限判断
		
android6.0系统允许用户管理应用权限,可以关闭/打开权限. 所以需要在APP中增加权限判断,以免用户关闭相应权限后,APP运行异常. 以MMS为例,在系统设置——应用——MMS——权限——&g ...
 - sql 语句中使用条件判断case then else end
		
sql 语句中使用条件判断case then else end范例: SELECT les.[nLessonNo] FROM BS_Lesson AS les WHERE les.[sClassCod ...
 - JAVA 中两种判断输入的是否是数字的方法__正则化_
		
JAVA 中两种判断输入的是否是数字的方法 package t0806; import java.io.*; import java.util.regex.*; public class zhengz ...
 - JavaScript 中 if 条件判断
		
在JS中,If 除了能够判断bool的真假外,还能够判断一个变量是否有值. 下面的例子说明了JS中If的判断逻辑: 变量值 true '1' 1 '0' 'null' 2 '2' false 0 n ...
 - Java中的空值判断
		
Java中的空值判断 /** * 答案选项: * A YouHaidong * B 空 * C 编译错误 * D 以上都不对 */ package com.you.model; /** * @auth ...
 - Yaml 文件中Condition If- else 判断的问题
		
在做项目的CI/ CD 时,难免会用到 Travis.CI 和 AppVeyor 以及 CodeCov 来判断测试的覆盖率,今天突然遇到了一个问题,就是我需要在每次做测试的时候判断是否存在一个环境变量 ...
 - tips:Java中while的判断条件
		
tips:Java中while的判断条件! 在c++中,有时候会遇到这种情况: while(x = y){ dosomething; } 如果x与y相等,这个时候如果循环体中没有跳出的点,那么会无限循 ...
 
随机推荐
- 第一章(Kotlin:定义和目的)
			
实战Kotlin勘误 Kotlin 资源大全 Kotlin主要特征 目标平台 编写服务器端代码(典型的代表是Web应用后端) 创建Android设备上运行的移动应用(Android开发) 其他:可以让 ...
 - zabbix监控nginx脚本
			
~]# cd /etc/zabbix/scripts/ scripts]# ls nginx_status.sh scripts]# cat nginx_status.sh ############# ...
 - Maven学习归纳(四)——传递依赖和依赖的规则
			
一.传递依赖 官方文档解释的传送门:http://ifeve.com/maven-dependency-mechanism/ 当存在传递依赖的情况时,主工程对间接依赖的jar可以访问吗? 例如:A.j ...
 - jinfo Java配置信息工具
			
jinfo(Configuration info for Java) jinfo的作用是实时地查看和调整虚拟机各项参数. jinfo 命令格式: jinfo [ option ] pid pid是虚拟 ...
 - 算法与数据结构基础 - 递归(Recursion)
			
递归基础 递归(Recursion)是常见常用的算法,是DFS.分治法.回溯.二叉树遍历等方法的基础,典型的应用递归的问题有求阶乘.汉诺塔.斐波那契数列等,可视化过程. 应用递归算法一般分三步,一是定 ...
 - [sonarqube的使用] sonarqube安装
			
一 . SonarQube代码质量检查工具简介 Sonar (SonarQube)是一个开源平台,用于管理源代码的质量 Sonar 不只是一个质量数据报告工具,更是代码质量管理平台 支持Java, C ...
 - Windows server 2008 快速搭建域环境
			
之前根据网上的教程搭建,然后出现了很多问题,最后摸索出了一个比较稳妥一点的方法. 对于选系统这里,虽然上一篇文章已经说过了,这里也再强调一下,我使用的是08的系统,使用其他系统的暂不做评价,使用08系 ...
 - Mybatis逆向工程过程中出现targetRuntime in context mybatisGenerator is invalid
			
最开始设置的Mybatis,但是逆向工程准备就绪后出现问题 报错为targetRuntime in context mybatisGenerator is invalid 后来修改为Mybatis3能 ...
 - 规则引擎 - drools 使用讲解(简单版) - Java
			
drools规则引擎 项目链接 现状: 运维同学(各种同学)通过后台管理界面直接配置相关规则,这里是通过输入框.下拉框等完成输入的,非常简单: 规则配置完毕后,前端请求后端,此时服务端根据参数(即规则 ...
 - 阿里云服务器CentOS6.9 tomcat配置域名访问
			
之前一直是ip访问项目,今天申请到一个测试域名,想要用设置用域名访问项目. 1.进入阿里云服务器中,修改tomcat中server.xml文件 cd /usr/local/apache-tomcat/ ...