今天写一下C#里的“==”这个操做符。ui
在刚学C#的时候,我觉得C#里的==和.NET里的object.Equals()方法是同样的,就是一个语法糖而已。其实它们的底层机制是不同的,只不过它们给出的结果在大多数状况下刚好相同。spa
看个例子:翻译
这俩方法给出的结果都是True。3d
看起来这两种方式作了一样的动做,就是比较两个值。blog
Build项目,而后使用ildasm看一下生成的il语言(ildasm位置大体在:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools)。继承
使用ildasm打开生成的dll,首先查看Program类里面的ByEqualMethod方法:接口
能够看到C#源码里调用Equals()的地方直接被翻译成il语言里相应的Equals()方法了。。。。内存
而后看一下ByEqualOperator这个方法:字符串
在C#里该方法使用了==操做符,而在il语言里,咱们只看到了一个叫作ceq的指令。ceq的意思是compare for equality,就是比较两个值是否相等,在运行时,它将会被转换为硬件上的比较,也许用的是CPU的寄存器。编译器
针对原始类型,C#的==操做符并无使用.NET里提供的那些Equals方法,这时==操做符使用专用的汇编语言指令来进行判断相等性的。
这里的引用类型不包含string。
看例子,这里我使用==来比较自定义类MyClass的两个实例是否相等:
而结果是两个False:
使用ildasm看一下ByEqualMethod()这个方法:
能够看到,a.Equals(b)调用的是virtual的object.Equals()方法,参数类型是object,这个应该都能理解。
再看一下ByEqualOperator()方法:
== 操做符翻译过来仍是使用ceq对两个参数进行的比较,和以前int类型的例子同样,除了参数类型不一样。
因此这应该也是使用CPU的硬件来进行判断相等性的,那么像这种引用类型是怎么经过CPU硬件来比较的呢?由于这两个类型是引用类型,因此c1,c2两个变量里面保存的是它们对应的实例在托管堆中的内存地址,也就是两个数字而已,因此固然能够进行比较了。
咱们都知道,==用来判断string相等性的时候,比较的是string值,而不是引用地址。
看例子:
结果是两个True:
首先,使用string.Copy()方法能够保证str1和str2是两个不一样的引用。
使用ildasm,先看ByEqualMethod():
能够看到,这里a.Equals(b)实际调用的是string实现的IEquatable<T>接口的Equals方法,它的参数是string。
再看一下ByEqualOperator():
此次没有使用ceq指令,而是调用了一个叫作op_Equality()的方法,这是个什么方法?
其实它是C#里 == 操做符的一个重载:static bool op_Equality(string, string)。
在C#里,当你定义一个类型的时候,你能够对==操做符进行重载,格式大概以下:
由于il语言里没有操做符的概念,而只有方法才能做为操做符的重载而存在于il里,因此这里使用的是静态方法,它会被翻译为一个特殊的静态方法叫作op_Equality()。
咱们也能够直接看一下string类的源码,里面也是这样对==进行重载的:
固然,重载了==,也须要重载 !=。
总结一下,使用==来判断引用类型的相等性,须要按下面的思路顺序进行考虑:
1. 该类型是否对 == 进行了重载?若是是,那就是用该重载方法;不然看2
2. 使用ceq指令来比较引用指向的内存地址。
另外还须要再提醒一下的是,string类的==和Equals()方法永远都会给出同样的结果。
还有一个原则就是,当你改变某个类型的相等性判断方法是,要确保==和Equals()方法作的是一样的事情。
看例子,这里有两个值类型:
当我使用==对它们进行比较的时候,直接报错了。
由于默认状况下,不可使用==来对非原始类型的值类型进行相等性判断。要想使用==,就必须提供重载方法。
直接看例子:
针对这两个tuple,我作了三个相等性判断,经过第一个ReferenceEquals方法咱们能够知道这两个tuple变量指向不一样的实例。
而tp1.Equals(tp2)返回的是True,这是由于Tuple类(引用类型)重写了object.Equals()方法,从而比较的是Tuple里面的值。
尽管微软为Tuple把object.Equals()方法重写了,可是它并无处理==操做符,因此==仍是在比较引用的相等性,因此会返回False。
这样作确实挺让人迷惑的。。。
一般状况下,尽可能使用==操做符,可是有时候==不行,须要使用object.Equals()方法,例如涉及到继承或者泛型的时候。
直接看例子:
这两个字符串我作了4个相等性判断,其结果为:
不管是object的virtual Equals()方法,仍是==操做符,仍是object的static Equals()方法,都会返回True。
可是我作一下小小的改动:
咱们看看结果会不会变:
结果发生了变化,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()方法,而不是==操做符。
另外一种不适合使用==操做符的情景是涉及泛型的时候,直接看例子:
这个泛型方法直接报错了,由于==操做符没法应用于这两个操做数T,T能够是任何类型,例如T是非原始类型的struct,那么==就不可用。咱们没法为泛型指定约束让其实现某个操做符。针对这个例子,我能够这样作,来保证能够编译:
如今T是引用类型了,代码能够编译了。咱们使用如下该方法:
按理说这就至关于调用了Equals()方法,结果应该返回True。而实际结果是:
之因此返回了False,是由于泛型方法里的==操做符比较的是引用,而这又是由于尽管编译器知道能够把==操做符应用于类型T,可是它仍然不知道具体是哪一个类型T会重载该操做符,因此它会假设T不会重载==操做符,从而对待这两个操做数如同object类型同样并编译,因此判断的是引用相等性。
因此泛型方法不会选择任何的操做符重载,它对待泛型类就像对待object类型同样。
综上,针对泛型方法,应该使用Equals()方法,而不是==操做符。