C# ReferenceEquals(), static Equals(), instance Equals(), 和运算行符==之间的关系

时间:2021-02-09 16:18:49

C#充许你同时创建值类型和引用类型。两个引用类型的变量在引用同一个对象时,它们是相等的,就像引用到对象的ID一样。两个值类型的变量在它们的类型和内容都是相同时,它们应该是相等的。这就是为什么相等测试要这么多方法了。
先从两个你可能从来不会修改的方法开始。
ReferenceEquals():
Object.ReferenceEquals()在两个变量引用到同一个对象时返回true,也就是两个变量具有相同的对象ID。不管比较的类型是引用类型还是值类型的,这个方法总是检测对象ID,而不是对象内容。是的,这就是说当你测试两个值类型是否相等时,ReferenceEquals()总会返回false,即使你是比较同一个值类型对象,它也会返回false。这里有两个装箱,因为参数要求两个引用对象,所以用两个值类型来调用该方法,会先使两个参数都装箱,这样一来,两个引用 对象自然就不相等了。

int i = 5;
int j = 5;
if ( Object.ReferenceEquals( i, j ))
  Console.WriteLine( "Never happens." );
else
  Console.WriteLine( "Always happens." );
if ( Object.ReferenceEquals( i, i ))
  Console.WriteLine( "Never happens." );
else
  Console.WriteLine( "Always happens." );


都会输出:Never happens.

Object.Equals():
静态的Object.Equals()方法是像下面这样实现的:

public static bool Equals( object left, object right )
{
  // Check object identity
  if (left == right )
    return true;
  // both null references handled above
  if ((left == null) || (right == null))
    return false;
  return left.Equals (right);
}


也就是说,静态的Equals()是使用左边参数实例的Equals()方法来断定两个对象是否相等。
与ReferenceEquals()一样,你或许从来不会重新定义静态的Object.Equals()方法,因为它已经确实的完成了它应该完成的事:在你不知道两个对象的确切类型时断定它们是否是一样的。因为静态的Equals()方法把比较委托给左边参数实例的Equals(),它就是用这一原则来处理另一个类型的。
Object.Equals()方法使用对象的ID来断定两个变量是否相等。这个默认的Object.Equals()函数的行为与Object.ReferenceEquals()确实是一样的。
现在你应该明白为什么你从来不必重新定义静态的ReferenceEquals()以及静态的Equals()方法了吧。
但是请注意,值类型是不一样的。System.ValueType并没有重载Object.Equals(),记住,System.ValueType是所有你所创建的值类型(使用关键字struct创建)的基类。两个值类型的变量相等,如果它们的类型和内容都是一样的。
ValueType.Equals()是所有值类型的基类。为了提供正确的行为,它必须比较派生类的所有成员变量,而且是在不知道派生类的类型的情况下。在C#里,这就意味着要使用反射。对反射而言它们有太多的不利之处,特别是在以性能为目标的时候。

Object.Equals():
现在来讨论你须要重载的方法。但首先,让我们先来讨论一下这样的一个与相等相关的数学性质。你必须确保你重新定义的方法的实现要与其它程序员所期望的实现是一致的。这就是说你必须确保这样的一个数学相等性质:相等的自反性,对称性和传递性。自反性就是说一个对象是等于它自己的,不管对于什么类型,a==a总应该返回true;对称就是说,如果有a==b为真,那么b==a也必须为真;传递性就是说,如果a==b为真,且b==c也为真,那么a==c也必须为真,这就是传递性。
现在是时候来讨论实例的Object.Equals()函数了,包括你应该在什么时候来重载它。
在大多数情况下,你可以为你的任何值类型重载一个快得多的Equals()。简单的推荐一下:在你创建一个值类型时,总是重载ValueType.Equals()。
现在你知道什么时候应该重载你自己的Object.Equals(),你应该明白怎样来实现它。值类型的比较关系有很多装箱的实现。对于用户类型,你的实例方法须要遵从原先定义行为(译注:前面的数学相等性质),从而避免你的用户在使用你的类时发生一些意想不到的行为。这有一个标准的模式:

public class Foo
{
  public override bool Equals( object right )
  {
    // check null:
    // the this pointer is never null in C# methods.
    if (right == null)
      return false;
    if (object.ReferenceEquals( this, right ))
      return true;
    // Discussed below.
    if (this.GetType() != right.GetType())
      return false;
    // Compare this type's contents here:
    return CompareFooMembers(
      this, right as Foo );
  }
}

 


首先,Equals()决不应该抛出异常,这感觉不大好。两个变量要么相等,要么不等;没有其它失败的余地。直接为所有的失败返回false,例如null引用或者错误参数。
第一个检测断定右边的对象是否为null,这样的引用上没有方法检测,在C#里,这决不可能为null。在你调用任何一个引用到null的实例的方法之前,CLR可能抛出异常。下一步的检测来断定两个对象的引用是否是一样的,检测对象ID就行了。这是一个高效的检测,并且相等的对象ID来保证相同的内容。这个步骤是很重要的,首先,应该注意到它并不一定是Foo类型,它调用了this.GetType(),这个实际的类型可能是从Foo类派生的。其次,这里的代码在比较前检测了对象的确切类型。这并不能充分保证你可以把右边的参数转化成当前的类型。这个测试会产生两个细微的BUG。考虑下面这个简单继承层次关系的例子:

public class B
{
  public override bool Equals( object right )
  {
    // check null:
    if (right == null)
      return false;

    // Check reference equality:
    if (object.ReferenceEquals( this, right ))
      return true;

    // Problems here, discussed below.
    B rightAsB = right as B;
    if (rightAsB == null)
      return false;

    return CompareBMembers( this, rightAsB );
  }
}

public class D : B
{
  // etc.
  public override bool Equals( object right )
  {
    // check null:
    if (right == null)
      return false;

    if (object.ReferenceEquals( this, right ))
      return true;

    // Problems here.
    D rightAsD = right as D;
    if (rightAsD == null)
      return false;

    if (base.Equals( rightAsD ) == false)
      return false;

    return CompareDMembers( this, rightAsD );
  }

}

//Test:
B baseObject = new B();
D derivedObject = new D();

// Comparison 1.
if (baseObject.Equals(derivedObject))
  Console.WriteLine( "Equals" );
else
  Console.WriteLine( "Not Equal" );

// Comparison 2.
if (derivedObject.Equals(baseObject))
  Console.WriteLine( "Equals" );
else
  Console.WriteLine( "Not Equals" );

 


返回Equals和Not Equal
在任何可能的情况下,你都希望要么看到两个Equals或者两个Not Equals。因为一些错误,这并不是先前代码的情形。这里的第二个比较决不会返回true。这里的基类,类型B,决不可能转化为D。然而,第一个比较可能返回true。派生类,类型D,可以隐式的转化为类型B。如果右边参数以B类型展示的成员与左边参数以B类型展示的成员是同等的,B.Equals()就认为两个对象是相等的。你将破坏相等的对称性。这一架构被破坏是因为自动实现了在继承关系中隐式的上下转化。
当你这样写时,类型D被隐式的转化为B类型:
baseObject.Equals( derived )
如果baseObject.Equals()在它自己所定义的成员相等时,就断定两个对象是相等的。另一方面,当你这样写时,类型B不能转化为D类型,
derivedObject.Equals( base )
B对象不能转化为D对象,derivedObject.Equals()方法总是返回false。如果你不确切的检测对象的类型,你可能一不小心就陷入这样的窘境,比较对象的顺序成为一个问题。
当你重载Equals()时,这里还有另外一个可行的方法。你应该调用基类的System.Object或者System.ValueType的比较方法,除非基类没有实现它。前面的代码提供了一个示例。类型D调用基类,类型B,定义的Equals()方法,然而,类B没有调用baseObject.Equals()。它调用了Systme.Object里定义的那个版本,就是当两个参数引用到同一个对象时它返回true。这并不是你想要的,或者你是还没有在第一个类里的写你自己的方法。
原则是不管什么时候,在创建一个值类型时重载Equals()方法,并且你不想让引用类型遵从默认引用类型的语义时也重载Equals(),就像System.Object定义的那样。当你写你自己的Equals()时,遵从要点里实现的内容。重载Equals()就意味着你应该重写GetHashCode()。

==:
操作符==(),任何时候你创建一个值类型,重新定义操作符==()。原因和实例的Equals()是完全一样的。默认的版本使用的是引用的比较来比较两个值类型。效率远不及你自己任意实现的一个,所以,你自己写。当你比较两个值类型时,遵从原则来避免装箱。

C#给了你4种方法来检测相等性,但你只须要考虑为其中两个提供你自己的方法。你决不应该重载静态的Object.ReferenceEquals()和静态的Object.Equals(),因为它们提供了正确的检测,忽略运行时类型。你应该为了更好的性能而总是为值类型实例提供重载的Equals()方法和操作符==()。当你希望引用类型的相等与对象ID的相等不同时,你应该重载引用类型实例的Equals()。