[C#] 类型学习笔记二:详解对象之间的比较
继上一篇对象类型后,这里我们一起探讨相等的判定。
相等判断有关的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#] 类型学习笔记二:详解对象之间的比较的更多相关文章
- JavaScript学习笔记-实例详解-类(二)
实例详解-类(二) //===给Object.prototype添加只读\不可枚举\不可配置的属性objectId(function(){ Object.defineProperty(Object ...
- JavaScript学习笔记-实例详解-类(一)
实例详解-类(一): //每个javascript函数(除了bind())都自动拥有一个prototype对象// 在未添加属性或重写prototype对象之前,它只包含唯一一个不可枚举属性const ...
- Angular6 学习笔记——组件详解之组件通讯
angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...
- Angular6 学习笔记——组件详解之模板语法
angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...
- Angular6 学习笔记——路由详解
angular6.x系列的学习笔记记录,仍在不断完善中,学习地址: https://www.angular.cn/guide/template-syntax http://www.ngfans.net ...
- Android学习笔记-Dialog详解
1.对话框的使用 1.1AlertDialog的显示 简单对话框以及监听的设置:重点掌握三个按钮(也就是三上单词): PositiveButton(确认按钮);NeutralButton(忽略按钮) ...
- [CSS3] 学习笔记-选择器详解(二)
1.选择器first-child.last-child.nth-child和nth-last-child 利用first-child.last-child.nth-child和nth-last-chi ...
- 【转载】自定义View学习笔记之详解onMeasure
网上对自定义View总结的文章都很多,但是自己还是写一篇,好记性不如多敲字! 其实自定义View就是三大流程,onMeasure.onLayout.onDraw.看名字就知道,onMeasure是用来 ...
- C++并发与多线程学习笔记--unique_lock详解
unique_lock 取代lock_quard unique_lock 的第二个参数 std::adopt_lock std::try_to_lock std::defer_lock unique_ ...
随机推荐
- maven项目中没有resource文件夹的问题
之前使用eclipse创建maven项目,文件夹都是建好的,这几次创建,都没有resource文件夹,需要手动创建resource. 现象描述 在eclipse中,创建maven项目有两种方式: 一种 ...
- day-15 用opencv怎么扫描图像,利用查找表和计时
一.本节知识预览 1. 怎样遍历图像的每一个像素点? 2. opencv图像矩阵怎么被存储的? 3. 怎样衡量我们算法的性能? 4. 什么是查表,为什么要使用它们? 二.什么是查表,为什么要使 ...
- 词频统计 SPEC 20170914 1 1 1 1 1
功能1 小文件输入,为表明程序能跑,结果真实而不是迫害老五,请他亲自键盘在控制台下输入命令. #include<stdio.h> #include<string.h> #inc ...
- 算法与数据结构3.3 calculator
★实验任务 小 V 发明了一个神奇的整数计算器: 给定一个合法的表达式,这个计算器能求出这个表达式的最终答案. 表达式可能包含: +:运算符,整数加法.如 1+1=2 -:运算符,整数减法.如 1-1 ...
- P4编程环境搭建
本文参照了sdnlab上相关文章的搭建推荐. 使用的系统环境为ubuntu 18.04 组件介绍 主要安装五个组件: BMv2:是一款支持P4编程的软件交换机 p4c:是一款P4的编译器 PI:是P4 ...
- JAVA单态设计模式
核心--在类的内部把构造器私有化,同时在内部产生对象,并通过类.静态方法(static)返回实例化对象的引用 设计模式是在大量的实践总结和理论化之后优选的代码结果,编程风格,以及解决问题的思考方式 ...
- PART1 一些想法
其实我一直是一个后知后觉的人,这点也是我过了好久才发现的问题,之所以晚发现自己这个毛病,是因为后知后觉==,这有点像是个悖论或者是笑话,但的确是真实存在于我的身上.其实当初为啥来这个学校选计算机的专业 ...
- android gradle打包常见问题及解决方案
背景: 问题: Q1: UNEXPECTED TOP-LEVEL ERROR: java.lang.OutOfMemoryError: Java heap space at com.android.d ...
- C#中的unsafe
为了保持类型安全性,默认情况下,C# 不支持指针算法. 但是,通过使用 unsafe 关键字,可以定义可在其中使用指针的不安全上下文. 有关指针的详细信息,请参阅主题指针类型. 备注 在公共语言运行时 ...
- 总结 java 学习
想着想把以前学的java学习笔记整理下发上来,慢慢整理吧.