ReferenceEquals()、static Equals() 、instance Equals() 与 operator==之间的联系与区别

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

当你创建一个用户自定义类型(类或结构)时,你需要给你的类型定义相等操作.C#中提供了几个不同的函数来验证两个对象是否满足“相等”的含义.
public static bool ReferenceEquals
( object left, object right );
public static bool Equals
( object left, object right );
public virtual bool Equals( object right);
public static bool operator==( MyClass left, MyClass right );
语言本身可以让你为以上的这些函数定义自己的版本,但是往往你可以做到的事情不代表你应该这么做.正确的做法是永远不要尝试重定义前两个静态函数,你可以从语义的角度出发为你自己的类型定义实例的(instance)Equals()方法,或者偶尔的重载==操作符,这样,当你改变了其中的一个,你也同时影响了另一个.没错,也许验证四个函数的的相等是件很复杂的事情,但是不用担心,可以也有一些简化.
向所有C#中复杂的元素一样,C#允许你创建值类型或引用类型.如果两个引用类型引用自同一个对象实例那么他们是相等的.对于值类型,他们的相等条件是他们的类型相同并且它们所包含的内容也相同,这也就是为什么会有这么多测试相等的函数的原因了.
让我们从你永远不应当修改的两个函数开始吧,ReferenceEquals()返回true当且仅当这两个变量引用自同一个对象且这两个变量具有相同的对象标识.不论被用作比较的类型是值类型或是引用类型,这个方法总是会检查对象的标识而不是对象所包含的内容.没错,也就是说当你使用ReferenceEquals()来检查两个值类型是否相等的时候,它总是会返回false的,甚至你将一个值类型和自己做比较的时候也是如此,原因是由于拆装箱(boxing).
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." );
你永远不应当重定义Object.ReferenceEquals()因为它做的正是它应该做的事情:检查两个不同变量的对象标识.
另一个不应当被重定义的函数是静态函数Object.Equlas()方法.这个方法用于检验当你不知道参数的运行时类型时这两个变量是否相等.记住在C#中System.Object是所有对象的基类.当你在任何时候比较两个对象的时,这两个对象都是System.Object的实例.值类型和引用类型都是.那么,当不知道两个变量的类型时,这个函数是怎样来判断它们是否相等的呢?答案很简单:这个方法不会自己决定类型,而是根据其中的一个变量类型来决定,静态方法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);
}
以上的这段代码中出现了我们尚未讨论的两个函数: ==操作符和实例的(instance)Equals()方法.稍后我将会对这两个函数进行详细的研究,但是现在我还不想结束关于静态Equals()的讨论.现在,我想你已经注意到了静态的Equals()方法使用了左边参数的实例Equals()方法来判断两个对象是否相等.
同ReferenceEquals()方法一样,Object.Equals()方法已经做了它应该做的且正确的事情,所以你不必对它重新定义.它在不知道对象的运行时类型的情况下检查它们是否相等,它能够这么做是因为静态的Euqlas()方法将具体任务指派给了左边参数的实例Equlas()方法,它使用了这个参数的类型所定义的(相等)规则.
现在你应该明白了为什么不要修改ReferenceEquals()和静态Equals()方法了.接下来讨论你应当重写的方法,首先,我们来探讨一下等价在数学上的含义,如果你希望你所定义和实现的相等方法能够和其他程序员所期望的一致,那么这就需要你在脑中清楚的记住数学上等价的含义:相等是自反、对称和可传递的.自反是说一个对象和它本身相等,不论a是什么类型,a==a总是返回true的.对称的含义:顺序是可以任意调整:如果a==b返回true那么b==a也是true.最后一个属性是说如果a==b并且b==c两者都成立,那么可以推出a==c也成立,这就是传递属性.
现在,是时候讨论实例的Object.Equals()方法了,包括你应当在什么时候如何来重写它.当你所创建类型的默认行为与你所期望的不一致时,那么你就可以定你自己版本的Equals()方法了.Object.Equals()方法通过鉴别对象的标识来判断两个变量是否相等.默认的Object.Euqals()函数的行为与Object.ReferenceEquals()方法是一样的,但是waitvalue的类型却是不同的.System.ValueType重载了Object.Equals().记住ValueType是你所创建所有值类型的基类(使用struct关键字).两个值类型变量相等的条件是他们的类型相等并且它们有相同的内容.ValueType.Equals()实现了这些行为,但是确不是很有效率.ValueType.Equals()是所有值类型的基类.为了提供正确的行为,它只能在不清楚对象运行时类型的情况下来比较派生类的全部的成员变量.在C#中,也就是使用反射(reflection).使用反射机制有许多的不足之处,尤其是它的效率方面.相等比较在程序中的调用是相等频繁的,因此效率就很值得考虑了.考虑了上述因素,你就可以为你创建的值类型重载更快的Equlas()方法.对于值类型给出的建议很简单:当你在创建任何值类型的时候都重载它相应的ValueType.Equals()方法.
现在你知道何时应当重载Object.Euqals()方法,接下去你就需要如何去重载它.值类型的相等关系用到了许多拆装箱的操作.对于引用类型,你的实例方法需要参考预定义的行为,以下是标准模式:
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 );
}
}
首先,Equlas()方法不应当抛出无意义的异常信息.两个变量之间要么是相等的要么是不等的,不存在其他情况.对于所有比较失败的情况只需要返回false即可,例如参数是null或不匹配的参数类型.现在我们来研究一下方法实现的细节以便我们理解为什么会需要这些验证而不是其他的.第一处验证是判断右边参数是否为空,对于引用本身(this reference)则不需要验证,因为C#中,this是永远不会为空(null)的.CLR会在尝试通过空引用调用实例方法时抛出异常.下一处验证是通过检验这两个对象的标识来判断它们是否引用自同一处.这是个非常有效率的测试,因为如果对象的标识相同决定了它们的内容必定相同.
接下来一处验证判断了它们的类型是否相同.需要精确的写成上面的那种形式,因为首先我们应当注意到,它并没有把自己当作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 Equal" );

考虑了所有可能发生的情况,你现在肯定期望看到两次比较的结果.然而因为一些错误,我们并不会看到代码所描述的所有情况,第二个比较表达永远都不会返回ture的,原因是基类型B是不能被转换成派生类D的.第一个比较的结果可能会返回true.派生类D类的对象可以被隐示的转换成基类B.如果右侧B的成员变量与左侧的相同,B.Equals()会认为这两个对象相同,即使这两个对象的类型不同.因此你的方法最终还是认为它们是相等的.然而你违反了等价的对称原则.这个比较逻辑之所以会违背等价原则是因为其中的自动转换替代了派生的结构.
当你这么写的时候,D对象是被显示的转换到了B:
baseObject.Euqlas(derived)
如果baseObject.Equals()定义了两个类型中的域相匹配,那么这两个对象是相等的.另外一方面,如果你写成下面这样的话,B对象是不能被转换成D对象的:
derivedObject.equals(base)
B对象不能被转换成D对象.因此derivedObject.Equals()方法总是会返回false.如果你不仔细的检查对象的类型,那么你就会经常碰到由于比较顺序的问题而带来的上述情况.
下面是重载Equals()的另一个练习.你应当仅在基类不被System.Object或System.ValueType支持时才调用方法.前面的代码就是一个例子.D类调用了它基类所定义的Equals()方法,然而B类却没有调用baseObject.Equals(),而是调用的System.Object中定义的版本,也就是当两个参数引用自同一对象时会返回true.这不是你所希望的,否则你也不会在第一处就写你自己的方法了.
正确的方式是一旦你创建了值类型,你就要重载它的Equals()方法.当你重载Equals()方法时需要记住上面的要点.重载Equals()方法同时意味着你需要重载GetHashCode()方法.
已经讨论了三个,还剩下最后一个:perator==().任何时候你创建值类型你都应当重新定义operator==().原因和实例的Equals()方法一摸一样.默认的方法通过反射来比较两个值类型的内容,这是最没有效率可言的写法,所以你应当定义你自己的.同时当你比较两个值类型时应同样尽量避免拆装箱操作.
这里你需要注意的是,我从来没有说过在你重载实例的Equals()方法时重载operator==(),而是说你应当在建立值类型的同时重载operator==().当你创建引用类型时你几乎不需要重载operator==()..NetFramework中的类期望operator==()对于所有的引用类型都遵从引用的语意
C#给了你四种验证相等的方法,但是你只需要考虑重定义其中的两种.你永远不需要重定义静态的Object.ReferenceEquals()和静态的Object.Equals()方法,因为它们提供了正确的测试并且与运行时类型无关.你只需要为值类型提供实例的Equals()和operator==(),目的是提高效率.当你想定义除对象标识之外的相等含义时,你也需要为引用类型重载实例的Equals()方法,很简单,对吗?

说明:翻译的不好,大家可以参考这篇文章来辅助理解本文。 区别和认识四个判等函数

补充:
对于string这个引用类型是非常特殊一个引用类型。 
它有两点特殊的地方。 
第一点对象分配的特殊。 
例如 
string str1 = "abcd" 
string str2 = "abcd" 
那么.net在分配string类型的时候,先查看当前string类型列表是否有相同的,如果有的话,直接返回其的引用,否则重新分配。 
第二点对象引用操作的特殊,可以说不同于真正意义上的引用操作。 
例如: 
string str1 = "abcd" 
string str2 = str1; 
str2 = "efgh"// str1 is still "abcd" here 
当对于一个新的string类型是原有对象引用的时候,这点和一般的引用类型一样,但是当新的对象发生变化的时候,要重新分配一个新的地方,然后修改对象指向。 
因此对于string操作的时候,尤其发生变化的时候,会显得比较慢,因为其牵扯到内存地址的变化。 
对于数据量比较大的字符操作时候,使用StringBuilder来说效率会提升很高。