欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#相等性 - “==”

程序员文章站 2022-06-10 09:06:13
今天写一下C#里的“==”这个操作符。 原始类型 假象 在刚学C#的时候,我以为C#里的==和.NET里的object.Equals()方法是一样的,就是一个语法糖而已。其实它们的底层机制是不一样的,只不过它们给出的结果在大多数情况下恰好相同。 看个例子: 这俩方法给出的结果都是True。 看起来这 ......

今天写一下c#里的“==”这个操作符。

原始类型

假象

在刚学c#的时候,我以为c#里的==和.net里的object.equals()方法是一样的,就是一个语法糖而已。其实它们的底层机制是不一样的,只不过它们给出的结果在大多数情况下恰好相同。

看个例子:

C#相等性 - “==”

这俩方法给出的结果都是true。

看起来这两种方式做了同样的动作,就是比较两个值。

 

底层原理

build项目,然后使用ildasm看一下生成的il语言(ildasm位置大致在:c:\program files (x86)\microsoft sdks\windows\v10.0a\bin\netfx 4.7.2 tools)。

使用ildasm打开生成的dll,首先查看program类里面的byequalmethod方法:

C#相等性 - “==”

可以看到c#源码里调用equals()的地方直接被翻译成il语言里相应的equals()方法了。。。。

 

然后看一下byequaloperator这个方法:

C#相等性 - “==”

在c#里该方法使用了==操作符,而在il语言里,我们只看到了一个叫做ceq的指令。ceq的意思是compare for equality,就是比较两个值是否相等,在运行时,它将会被转换为硬件上的比较,也许用的是cpu的寄存器。

针对原始类型,c#的==操作符并没有使用.net里提供的那些equals方法,这时==操作符使用专用的汇编语言指令来进行判断相等性的

 

使用 == 判断引用类型的相等性

这里的引用类型不包含string。

看例子,这里我使用==来比较自定义类myclass的两个实例是否相等:

C#相等性 - “==”

而结果是两个false:

C#相等性 - “==”

 

使用ildasm看一下byequalmethod()这个方法:

C#相等性 - “==”

可以看到,a.equals(b)调用的是virtual的object.equals()方法,参数类型是object,这个应该都能理解。

 

再看一下byequaloperator()方法:

C#相等性 - “==”

== 操作符翻译过来还是使用ceq对两个参数进行的比较,和之前int类型的例子一样,除了参数类型不同。

所以这应该也是使用cpu的硬件来进行判断相等性的,那么像这种引用类型是怎么通过cpu硬件来比较的呢?因为这两个类型是引用类型,所以c1,c2两个变量里面保存的是它们对应的实例在托管堆中的内存地址,也就是两个数字而已,所以当然可以进行比较了。

 

string

我们都知道,==用来判断string相等性的时候,比较的是string值,而不是引用地址。

看例子:

C#相等性 - “==”

结果是两个true:

C#相等性 - “==”

 

首先,使用string.copy()方法可以保证str1和str2是两个不同的引用。

 

使用ildasm,先看byequalmethod():

C#相等性 - “==”

可以看到,这里a.equals(b)实际调用的是string实现的iequatable<t>接口的equals方法,它的参数是string。

 

再看一下byequaloperator():

C#相等性 - “==”

这次没有使用ceq指令,而是调用了一个叫做op_equality()的方法,这是个什么方法?

其实它是c#里 == 操作符的一个重载:static bool op_equality(string, string)。

 

在c#里,当你定义一个类型的时候,你可以对==操作符进行重载,格式大概如下:

C#相等性 - “==”

因为il语言里没有操作符的概念,而只有方法才能作为操作符的重载而存在于il里,所以这里使用的是静态方法,它会被翻译为一个特殊的静态方法叫做op_equality()。

 

我们也可以直接看一下string类的源码,里面也是这样对==进行重载的:

C#相等性 - “==”

当然,重载了==,也需要重载 !=。

 

小结

总结一下,使用==来判断引用类型的相等性,需要按下面的思路顺序进行考虑:

1. 该类型是否对 == 进行了重载?如果是,那就是用该重载方法;否则看2

2. 使用ceq指令来比较引用指向的内存地址

 

另外还需要再提醒一下的是,string类的==和equals()方法永远都会给出一样的结果。

还有一个原则就是,当你改变某个类型的相等性判断方法是,要确保==和equals()方法做的是同样的事情。

 

值类型

非原始类型

看例子,这里有两个值类型:

C#相等性 - “==”

当我使用==对它们进行比较的时候,直接报错了。

因为默认情况下,不可以使用==来对非原始类型的值类型进行相等性判断。要想使用==,就必须提供重载方法。

 

tuple

直接看例子:

C#相等性 - “==”
结果如下:
 
C#相等性 - “==”

针对这两个tuple,我做了三个相等性判断,通过第一个referenceequals方法我们可以知道这两个tuple变量指向不同的实例。

而tp1.equals(tp2)返回的是true,这是因为tuple类(引用类型)重写了object.equals()方法,从而比较的是tuple里面的值。

尽管微软为tuple把object.equals()方法重写了,但是它并没有处理==操作符,所以==还是在比较引用的相等性,所以会返回false。

这样做确实挺让人迷惑的。。。

 

比较==和object.equals()方法

 
C#相等性 - “==”

通常情况下,尽量使用==操作符,但是有时候==不行,需要使用object.equals()方法,例如涉及到继承或者泛型的时候。

 

继承

直接看例子:

 
C#相等性 - “==”

这两个字符串我做了4个相等性判断,其结果为:

 C#相等性 - “==”

无论是object的virtual equals()方法,还是==操作符,还是object的static equals()方法,都会返回true。

但是我做一下小小的改动:

 
C#相等性 - “==”

我们看看结果会不会变:

 
C#相等性 - “==”

结果发生了变化,str1==str2这次返回了false。

这是因为==操作符不是virtual的,它相当于是static的,而static的是无法virtual的。

现在 str1 == str2 这句话,我们比较的是两个类型为object的变量,尽管我们知道它们都是string,但是编译器并不知道。而针对于非virtual的方法或操作符,到底调用哪个方法是在编译时决定的,因为这两个变量的类型是object,所以编译器会选择用来比较object的代码,而object又没有==操作符的重载,所以==做的就是比较引用的相等性,而这两个string是不同的实例,所以结果会返回false。

所以(object)x == (object)y和referenceequals(x, y)的结果总是一样的。

针对涉及继承的相等性判断,最好还是使用object.equals()方法,而不是==操作符。

 

泛型

另一种不适合使用==操作符的情景是涉及泛型的时候,直接看例子:

C#相等性 - “==”

这个泛型方法直接报错了,因为==操作符无法应用于这两个操作数t,t可以是任何类型,例如t是非原始类型的struct,那么==就不可用。我们无法为泛型指定约束让其实现某个操作符。针对这个例子,我可以这样做,来保证可以编译:

C#相等性 - “==”

现在t是引用类型了,代码可以编译了。我们使用以下该方法:

C#相等性 - “==”

按理说这就相当于调用了equals()方法,结果应该返回true。而实际结果是:

C#相等性 - “==”

之所以返回了false,是因为泛型方法里的==操作符比较的是引用,而这又是因为尽管编译器知道可以把==操作符应用于类型t,但是它仍然不知道具体是哪个类型t会重载该操作符,所以它会假设t不会重载==操作符,从而对待这两个操作数如同object类型一样并编译,所以判断的是引用相等性。

所以泛型方法不会选择任何的操作符重载,它对待泛型类就像对待object类型一样。

综上,针对泛型方法,应该使用equals()方法,而不是==操作符。

C#相等性 - “==”