简介

最近正在看《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#中的相等比较参照的是这样一条规律:值类型比较的是值是否相等,而引用类型比较的则是二者是否引用同一个对象。此外,本文还介绍了一些和相等判断有关的函数和接口,这些函数和接口的作用在于构建了一个相等比较的框架。通过这些函数和接口,不仅可以使用默认的比较规则,而且我们还可以自定义比较规则。在本文的最后,我们还给出了一个例子来模拟自定义比较规则的用途。通过该例子,我们可以清楚地看到自定义比较的实现。

C#中的等值判断1的更多相关文章

  1. C# 值类型和引用类型等值判断

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  2. js中的等值运算符(抽象相等==与严格相等===的区别)

    js中的等值运算符 js中的相等分为抽象相等和严格相等,他们有什么区别呢. 在说具体算法前,先提下JS数据类型,JS数据类型分为6类:Undefined Null String Number Bool ...

  3. android应用中增加权限判断

    android6.0系统允许用户管理应用权限,可以关闭/打开权限. 所以需要在APP中增加权限判断,以免用户关闭相应权限后,APP运行异常. 以MMS为例,在系统设置——应用——MMS——权限——&g ...

  4. sql 语句中使用条件判断case then else end

    sql 语句中使用条件判断case then else end范例: SELECT les.[nLessonNo] FROM BS_Lesson AS les WHERE les.[sClassCod ...

  5. JAVA 中两种判断输入的是否是数字的方法__正则化_

    JAVA 中两种判断输入的是否是数字的方法 package t0806; import java.io.*; import java.util.regex.*; public class zhengz ...

  6. JavaScript 中 if 条件判断

    在JS中,If 除了能够判断bool的真假外,还能够判断一个变量是否有值. 下面的例子说明了JS中If的判断逻辑: 变量值 true '1' 1 '0' 'null' 2 '2'  false 0 n ...

  7. Java中的空值判断

    Java中的空值判断 /** * 答案选项: * A YouHaidong * B 空 * C 编译错误 * D 以上都不对 */ package com.you.model; /** * @auth ...

  8. Yaml 文件中Condition If- else 判断的问题

    在做项目的CI/ CD 时,难免会用到 Travis.CI 和 AppVeyor 以及 CodeCov 来判断测试的覆盖率,今天突然遇到了一个问题,就是我需要在每次做测试的时候判断是否存在一个环境变量 ...

  9. tips:Java中while的判断条件

    tips:Java中while的判断条件! 在c++中,有时候会遇到这种情况: while(x = y){ dosomething; } 如果x与y相等,这个时候如果循环体中没有跳出的点,那么会无限循 ...

随机推荐

  1. 【Redis】缓存穿透与缓存雪崩

    一.缓存雪崩 1.1 缓存雪崩产生的原因 1.2 解决方案 1.3 锁的方式 1.4 消息中间件 1.5 一级和二级缓存 1.6 均摊分配redis key 失效时间 二.缓存穿透 一.缓存雪崩 1. ...

  2. asp.net core 使用 signalR(二)

    asp.net core 使用 signalR(二) Intro 上次介绍了 asp.net core 中使用 signalR 服务端的开发,这次总结一下web前端如何接入和使用 signalR,本文 ...

  3. window 定时关机小程序bat

    复制以下文本,新建txt文件并修改为bat后缀 如图: @echo off title 定时关机 echo 定时关机程序 echo ---------------------------------- ...

  4. JAVASSM框架面试题

    1.SpringMVC的工作流程? 1. 用户发送请求至前端控制器DispatcherServlet 2. DispatcherServlet收到请求调用HandlerMapping处理器映射器. 3 ...

  5. html5表单与Jquery Ajax配合使用

    html5的表单控件提供了很多格式检测功能,可以省去很多烦人的javascript验证代码,例如pattern属性和require属性,但触发的条件是表单提交,如果想通过ajax提交表单,就出现了不能 ...

  6. hadoop snapshot 备份恢复 .

    1.允许创建快照 首先,在你想要进行备份的文件夹下面 执行命令,允许该文件夹创建快照 hdfs dfsadmin -allowSnapshot <path> 例如:hdfs dfsadmi ...

  7. Metasploit工具----漏洞利用模块

    漏洞利用是指由渗透测试者利用一个系统.应用或者服务中的安全漏洞进行的攻击行为.流行的渗透攻击技术包括缓冲区溢出.Web应用程序攻击,以及利用配置错误等,其中包含攻击者或测试人员针对系统中的漏洞而设计的 ...

  8. 松软科技课堂:SQL-SELECT-INTO语句

    SQL SELECT INTO 语句可用于创建表的备份复件. SELECT INTO 语句 SELECT INTO 语句从一个表中选取数据,然后把数据插入另一个表中. SELECT INTO 语句常用 ...

  9. Linux 笔记 - 第十三章 Linux 系统日常管理之(四)Linux 中 rsync 工具和网络配置

    博客地址:http://www.moonxy.com 一.前言 rsync 命令是一个远程数据同步工具,可通过 LAN/WAN 快速同步多台主机间的文件,可以理解为 remote sync(远程同步) ...

  10. Day 19 磁盘管理

    1.磁盘的基本概念 1.什么是磁盘 磁盘(disk)是指利用磁记录技术存储数据的存储器. 磁盘是计算机主要的存储介质,可以存储大量的二进制数据,并且断电后也能保持数据不丢失. *绝大多数人对硬盘都不陌 ...