[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 = 10;

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>2</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 = 0; 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#] 类型学习笔记三:自定义值类型

posted on 2014-03-30 06:24  Felix Fang  阅读(6606)  评论(0编辑  收藏  举报

导航