继上一篇对象类型后,这里我们一起探讨相等的判定。

相等判断有关的4个方法

CLR中,和相等有关系的方法有这么4种:

(1) 最常见的 == 运算符

(2) Object的静态方法ReferenceEquals

(3) Object的静态方法Equals

(4) Object.Equals()方法,这是一个virtual method

"==" 运算符

首先要知道"==" 是一个运算符,它只有在两边都为相同类型时才能通过编译。

假设“==” 没有被我们显示地重载过,当它的两边都是引用类型时,"=="在左右两边引用同一个对象时返回true,它的作用和(1)中的System.Object.ReferenceEquals相同;

当"=="两边都是没被装箱的值类型时,只有值类型重载了"=="才能通过编译,也就是说,如果我们通过struct定义了新的值类型,然后通过"=="来比较,那只有我们在struct中显示重载了"=="才能通过编译。

FCL中Int32等这些自带的值类型,虽然查看代码时没有看到其对"=="的重载,但是我相信应该有隐示地对其进行重载,重载的内容应该和Int32中的Equals()函数一致。

Object的静态方法ReferenceEquals

它的定义如下

public static bool ReferenceEquals(object objA, object objB)
{
return objA == objB;
}

显然,这个方法的作用是为了判定两个object是否指向同一个堆对象。这里有个小实例

int n = ;

Object.ReferenceEquals( n, n );

object o1 = (object)n;
object o2 = (object)n;
if(o1 == o2) ...

两次的判定结果结果应该都为false,这里需要我们前一节装箱的知识。在每一次比较中,n在代码中都会两次被装箱,既然被装箱了两次,自然就是不同的对象,所以返回为false。

Object的静态方法Equal

public static bool Equals(object objA, object objB)
{
return objA == objB || (objA != null && objB != null && objA.Equals(objB));
}

从其源码可以看出,它其实就是==运算符和 (4)中所提到的object.Equals 方法的结合版。就是先判断对象的identical,再比较内容。虽然很全面,但是编程中不常用到,因为程序员在写代码时,对于到底是判断identical还是比较内容,都有明确的选择性。

Sytem.Object的提供的Equals 方法

这里才是我们的重头戏,这个方法将真正被用来比较对象的内容。

FCL中的万物之源System.Object提供的虚函数Equals的定义如下:

public virtual bool Equals(object obj){
if(this == obj) return true;
return false;
}

你没有看错,就这样没有了。

是不是有点意外?

这就是Microsoft的思路,这个virtual 方法只是意思意思罢了,真正的基于内容的比较,定义在具体的类中。

Equals 方法

System.Object所提供的virtual 方法只有被比较的对象引用同一个托管堆对象,才会返回true。而且,参数是object,也就是说如果我们自定义的类型如果没有显示override Equals的话,需要比较的时候类会被装箱。

那么,既然Object的equals如此的“弱”,那么系统本身的那些int,string等这些常见的值类型的比较的时候,内部是什么情况?

System.ValueType的Equals方法

有了第一篇笔记的铺垫,我们知道int等都是值类型,值类型是继承自System.ValueType的,System.ValueType又是继承自System.Object的。在System.ValueType中,不出意外,重写了Equals方法的实现。使用ILSpy打开System.ValueType中关于Equals的实现:

// System.ValueType
/// <summary>Indicates whether this instance and a specified object are equal.</summary>
/// <returns>true if <paramref name="obj" /> and this instance are the same type and represent the same value; otherwise, false.</returns>
/// <param name="obj">Another object to compare to. </param>
/// <filterpriority></filterpriority>
[__DynamicallyInvokable, SecuritySafeCritical]
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
RuntimeType runtimeType = (RuntimeType)base.GetType();
RuntimeType left = (RuntimeType)obj.GetType();
if (left != runtimeType)
{
return false;
}
if (ValueType.CanCompareBits(this))
{
return ValueType.FastEqualsCheck(this, obj);
}
FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (int i = ; i < fields.Length; i++)
{
object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);
if (obj2 == null)
{
if (obj3 != null)
{
return false;
}
}
else
{
if (!obj2.Equals(obj3))
{
return false;
}
}
}
return true;
}

整个实现可以分为下面几个部分:

(1) 如果参数是null,不需说,返回false

(2) 随后通过反射获取当前对象和参数对象的类型,比较两个类型是否一致。类型如果都不一致就直接返回false了。

这里大家可能会有疑问:这里明明使用base.GetType(),怎么可以解释为“获取当前对象的类型”?

反射方法GetType()是Sytem.Object中被实现的方法,并且不是虚函数。这就是说,任何类都不可能提供对其重写,任何实例调用GetType(),最后都会调用其基类Sytem.Object的GetType(),而GetType()的作用就是获得一开始 调用这个方法的实例的类型。因此,这段代码中无论是"base.GetType()",还是"this.GetType()",其实结果是一致的。"base.GetType()"写法其实更加规范(不愧是反编译出来的==),因为如上所说,GetType()是Sytem.Object中的方法。这里是题外话,另一篇博文会结合实例介绍GetType()的特点。

(3) 通过CanCompareBits是否能进行bit 比较,可以的话采用FastEqualsCheck进行比较。这两个方法都是System.ValueType的私有方法。关于他们的解释,我直接引用博文 Magic behind ValueType.Equals 中的话

The comment of CanCompareBits says "Return true if the valuetype does not contain pointer and is tightly packed". And FastEqualsCheck uses "memcmp" to speed up the comparison.

(4) 在(2)中我们已经得到this所属的type,然后再通过反射获取这个type所有的field,接着分别获取 this对象和比较对象obj在每个field的值,调用equals函数比较每一个值。如果需要用到这一步,时间上的开销就比较大了。

结论

(1) 如果我们自定义一个值类型而不用override关键字去新写一个equals方法,那么当需要比较时:(1) 值类型会被装箱 (2) 装箱后可能还需要调用反射获取每一个field。

所以在在《Effective C#》, 才会有这样一句话:"Always create an override of ValueType.Equals() whenever you create a value type"。

如果打开Int32,Boolean等的定义,可以看到里面都有对Equals()的显示实现,并且都是实现了System.IComparable 接口。

(2) 现在我也可以回答一开篇的疑问了,当我们定义两个int类型的数,然后通过"=="比较它们的时候,系统的做法是:a. 通过"=="重载的内容,发现是调用Equals()进行比较  b.因为Equals已经被Override关键字定义过,直接调用本地定义的Equals()函数。

知道了这些,我们不难知道为什么C#程序员要拥有下面这两个编程习惯:

(1) 在引用类型之间的比较时,不要使用 "==",而应该使用类型自身的Equals() (当然在此之前记得先override这个方法)。如果想判断两个引用类型的对象是否是同一个对象,使用Object.ReferenceEquals()静态方法。

(2) 在系统自带的值类型之间的比较时,可以使用类型自身的Equals(),但是为了可读性,往往使用 "==" 运算符。

相关阅读:

[C#] 类型学习笔记一:CLR中的类型,装箱和拆箱

[C#] 类型学习笔记三:自定义值类型

[C#] 类型学习笔记二:详解对象之间的比较的更多相关文章

  1. JavaScript学习笔记-实例详解-类(二)

    实例详解-类(二)   //===给Object.prototype添加只读\不可枚举\不可配置的属性objectId(function(){ Object.defineProperty(Object ...

  2. JavaScript学习笔记-实例详解-类(一)

    实例详解-类(一): //每个javascript函数(除了bind())都自动拥有一个prototype对象// 在未添加属性或重写prototype对象之前,它只包含唯一一个不可枚举属性const ...

  3. Angular6 学习笔记——组件详解之组件通讯

    angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...

  4. Angular6 学习笔记——组件详解之模板语法

    angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...

  5. Angular6 学习笔记——路由详解

    angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...

  6. Android学习笔记-Dialog详解

    1.对话框的使用 1.1AlertDialog的显示 简单对话框以及监听的设置:重点掌握三个按钮(也就是三上单词): PositiveButton(确认按钮);NeutralButton(忽略按钮) ...

  7. [CSS3] 学习笔记-选择器详解(二)

    1.选择器first-child.last-child.nth-child和nth-last-child 利用first-child.last-child.nth-child和nth-last-chi ...

  8. 【转载】自定义View学习笔记之详解onMeasure

    网上对自定义View总结的文章都很多,但是自己还是写一篇,好记性不如多敲字! 其实自定义View就是三大流程,onMeasure.onLayout.onDraw.看名字就知道,onMeasure是用来 ...

  9. C++并发与多线程学习笔记--unique_lock详解

    unique_lock 取代lock_quard unique_lock 的第二个参数 std::adopt_lock std::try_to_lock std::defer_lock unique_ ...

随机推荐

  1. Python+Opencv实现把图片转为视频

    1. 安装Opencv包 在Python命令行输入如下命令(如果你使用的Anaconda,直接进入Anaconda Prompt键入命令即可.如果你不知道Anaconda是什么,可以参考王树义老师的文 ...

  2. nodejs笔记--基础篇(一)

    Sublime Node.js开发环境配置 下载并安装Node.js安装包后再开始配置 1.先安装好Sublime Text 2 2.运行Sublime,菜单上找到Tools ---> Buil ...

  3. windows下cudnn的安装过程

    在CUDA安装成功之后,系统环境变量中会有如下两个变量显示:CUDA_PATH和CUDA_PATH_8 在安装完CUDA之后,到官网下载与其版本对应的CUDNN        下载地址:https:/ ...

  4. activiti工作流已办和待办查询sql

    最近项目中遇到一个问题,需要activiti的工作流表和业务表关联分页查询,然而我对于工作流的查询并不太熟悉,所以学习并总结如下. 想看看activiti到底怎么查询的待认领和待办.已办的查询sql, ...

  5. gcc 学习笔记(一) - 编译C程序 及 编译过程

    一. C程序编译过程 编译过程简介 : C语言的源文件 编译成 可执行文件需要四个步骤, 预处理 (Preprocessing) 扩展宏, 编译 (compilation) 得到汇编语言, 汇编 (a ...

  6. TCP系列14—重传—4、Karn算法和TSOPT的RTTM

    一.Karn算法 在RTT采样测量过程中,如果一个数据包初传后,RTO超时重传,接着收到这个数据包的ACK报文,那么这个ACK报文是对应初传TCP报文还是对应重传TCP报文呢?这个问题就是retran ...

  7. navicat for mysql 10.1.7 注册码

    NAVN-LNXG-XHHX-5NOO名:组织:注册码:均为NAVN-LNXG-XHHX-5NOO 下载地址:http://www.cr173.com/soft/38153.html

  8. springMVC视图有哪些?-009

    html,json,pdf等. springMVC 使用ViewResolver来根据controller中返回的view名关联到具体的view对象. 使用view对象渲染返回值以生成最终的视图,比如 ...

  9. 【Python】Python中的列表操作

    Python的列表操作可谓是功能强大且方便(相对于Java)简单.常规的操作就不说了(这不是一个入门教程),介绍几个很有特点的例子 添加 # 追加到结尾(append) li = [1, 2, 3, ...

  10. 【bzoj2318】Spoj4060 game with probability Problem 概率dp

    题目描述 Alice和Bob在玩一个游戏.有n个石子在这里,Alice和Bob轮流投掷硬币,如果正面朝上,则从n个石子中取出一个石子,否则不做任何事.取到最后一颗石子的人胜利.Alice在投掷硬币时有 ...