Effective C# 9:nderstand the Relationships Among ReferenceEquals(), static Equals(), instance Equals(), and operator==

本文详细介绍了C#中四种相等性测试方法:ReferenceEquals(), staticEquals(), instanceEquals(), 和 operator==() 的作用及使用场景。文章强调了何时以及如何重写这些方法以提高性能或满足特定需求。
rel="File-List" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml"> rel="themeData" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_themedata.thmx"> rel="colorSchemeMapping" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_colorschememapping.xml">

Item 9: Understand the Relationships Among ReferenceEquals(), static Equals(), instance Equals(), and operator==

理解下列各项之间的关系:ReferenceEquals(), static Equals(), instance Equals(), and operator==

When you create your own types (either classes or structs), you define what equality means for that type. C# provides four different functions that determine whether two different objects are "equal":

当你创建自己的类型时(要么是类要么是结构体),要为该类型定义什么是相等。C#提供了4个不同的方法来决定2个不同的对象是否“相等”:

  1.     public static bool ReferenceEquals(object left, object right);
  2.     public static bool Equals(object left, object right);
  3.     public virtual bool Equals(object right);
  4.    public static bool operator==(MyClass left, MyClass right);

The language enables you to create your own versions of all four of these methods. But just because you can doesn't mean that you should. You should never redefine the first two static functions. You'll often create your own instance Equals() method to define the semantics of your type, and you'll occasionally override operator==(), but only for performance reasons in value types. Furthermore, there are relationships among these four functions, so when you change one, you can affect the behavior of the others. Yes, needing four functions to test equality is complicated. But don't worry you can simplify it.

C#语言允许你创建所有4个方法的自己的版本。但是,你能并不意味着你应该。从不应该重新定义前面2个静态方法,经常要创建自己的实例Equals()方法来定义自己类型的语义,仅仅为了值类型中的效率问题才偶尔会重写==()操作符。进一步说,在4个方法之间存在着相互关系,因此当你改变一个的时候,可能会影响其它的行为。是的,需要用4个方法来测试相等性是复杂的,但是,不用担心,你可以简化它。

Like so many of the complicated elements in C#, this one follows from the fact that C# enables you to create both value types and reference types. Two variables of a reference type are equal if they refer to the same object, referred to as object identity. Two variables of a value type are equal if they are the same type and they contain the same contents. That's why equality tests need so many different methods.

C#语言里面那么多的复杂元素一样,该条款遵从这个事实:C#允许你创建值类型和引用类型。当同一个引用类型的2个变量引用同一个对象时,即它们有同样的对象标识,它们就是相等的。当同一个值类型的2个变量是类型相同并且包含的内容相同时,它们就是相等的。这就是为什么相等性测试需要那么多不同的方法。

Let's start with the two functions you should never change. Object.ReferenceEquals() returns True if two variables refer to the same object that is, the two variables have the same object identity. Whether the types being compared are reference types or value types, this method always tests object identity, not object contents. Yes, that means that ReferenceEquals() always returns false when you use it to test equality for value types. Even when you compare a value type to itself, ReferenceEquals() returns false. This is due to boxing, which is covered in Item 16.

让我们从你从不应该改变的两个方法入手。Object.ReferenceEquals()在下列条件下返回True:两个变量引用同一个对象,即,两个变量拥有同样的对象标识。无论被比较的对象是引用类型还是值类型,该方法总是检测对象标识,而不是对象的内容。是的,这就意味着:当使用ReferenceEquals()来检测两个值类型的相等性时,总是返回false。甚至当你用一个值类型和它自己比较时,ReferenceEquals()也返回false。这是因为装箱的原因,会在Item16讲到。

 

  1.    Int32 i = 5;
  2.     Int32 j = 5;
  3.     if (Object.ReferenceEquals(i, j))
  4.         Console.WriteLine("Never happens.");
  5.     else
  6.         Console.WriteLine("Always happens.");
  7.     if (Object.ReferenceEquals(i, i))
  8.         Console.WriteLine("Never happens.");
  9. else
  10.         Console.WriteLine("Always happens.");
  11.  
  12. 翻译时添加:
  13.     A a1 = new A();
  14.     A a2 = new A();
  15.     A a3 = a1;//a1 and a3 refer to the same object
  16.  
  17.     Console.WriteLine(Object.ReferenceEquals(a1,a1));//true
  18.     Console.WriteLine(Object.ReferenceEquals(a1,a2));//false
  19.     Console.WriteLine(Object.ReferenceEquals(a1,a3));//true
  20.  

You'll never redefine Object.ReferenceEquals() because it does exactly what it is supposed to do: test the object identity of two different variables.

决不应该重新定义Object.ReferenceEquals(),因为它精确的做了该做的事:检测两个不同变量的对象标识。

The second function you'll never redefine is static Object.Equals(). This method tests whether two variables are equal when you don't know the runtime type of the two arguments. Remember that System.Object is the ultimate base class for everything in C#. Anytime you compare two variables, they are instances of System.Object. Value types and reference types are instances of System.Object. So how does this method test the equality of two variables, without knowing their type, when equality changes its meaning depending on the type? The answer is simple: This method delegates that responsibility to one of the types in question. The static Object.Equals() method is implemented something like this:

第二个决不应该重新定义的方法是static Object.Equals()。当你不知道2个参数的运行时类型时,该方法可以检测2个变量是否相等。记住,在C#里面System.Object是一切的最终基类。无论何时比较2个变量,它们都是System.Object的实例。值类型和引用类型都是System.Objec的实例。那么,在相等性的意义会依赖于类型而产生变化的情况下,该方法不知道它们的类型,如何检测2个变量的相等性呢?答案很简单:该方法将职责委托给摆出问题的其中一个类型。static  Object.Equals()方法实现起来有点像这个样子:

 

  1.    public static bool Equals( object left, object right )
  2.     {
  3.       // Check object identity
  4.       if (left == right )
  5.         return true;
  6.       // both null references handled above
  7.       if ((left == null) || (right == null))
  8.         return false;
  9.       return left.Equals (right);
  10. }

This example code introduces both of the methods I have not discussed yet: operator==() and the instance Equals() method. I'll explain both in detail, but I'm not ready to end my discussion of the static Equals() just yet. For right now, I want you to understand that static Equals() uses the instance Equals() method of the left argument to determine whether two objects are equal.

这个示例代码引入了我目前还没有讨论的2个方法:==()操作符和instance Equals()方法。我将会仔细的解释它们,但是我现在还不准备结束static Equals()的讨论。现在,我想要你理解static Equals()使用左边参数的instance Equals()方法来判定2个对象是否相等。

As with ReferenceEquals(), you'll never redefine the static Object.Equals() method because it already does exactly what it needs to do: determines whether two objects are the same when you don't know the runtime type. Because the static Equals() method delegates to the left argument's instance Equals(), it uses the rules for that type.

ReferenceEquals()一样,决不应该重新定义static  Object.Equals()方法,因为它已经精确的做了它需要做的事:在不知道运行时类型时,决定2个对象是否相等。因为static Equals()方法将职责委托给左侧参数的static Equals(),它使用了那个类型的规则。

Now you understand why you never need to redefine the static ReferenceEquals() and static Equals() methods. It's time to discuss the methods you will override. But first, let's briefly discuss the mathematical properties of an equality relation. You need to make sure that your definition and implementation are consistent with other programmers' expectations. This means that you need to keep in mind the mathematical properties of equality: Equality is reflexive, symmetric, and transitive. The reflexive property means that any object is equal to itself. No matter what type is involved, a == a is always true. The symmetric property means that order does not matter: If a == b is true, b == a is also true. If a == b is false, b == a is also false. The last property is that if a == b and b == c are both true, then a == c must also be true. That's the transitive property.

现在,你理解了为什么从不需要重新定义static ReferenceEquals()static Equals()。是讨论你要重写的方法的时候了,但是首先让我们简要的讨论相等关系的数学特性。你需要确保你的定义和实现与其他程序员的期望是一致的。这意味着,你需要在头脑中记得相等性的数学特性:相等性是自反的、对称的、可传递的。自反性的意思是,任何对象都和自己相等。无论涉及到什么类型,a==a永远是正确的。对称性的意思是,顺序是无关的:如果a==b是正确的,那么b==a也是正确的,如果a==b是错误的,那么b==a也是错误的。最后一个特性,如果a==bb==c都是正确的,那么a==C必须是正确的,这就是可传递性。

Now it's time to discuss the instance Object.Equals() function, including when and how you override it. You create your own instance version of Equals() when the default behavior is inconsistent with your type. The Object.Equals() method uses object identity to determine whether two variables are equal. The default Object.Equals() function behaves exactly the same as Object.ReferenceEquals(). But waitvalue types are different. System.ValueType does override Object.Equals(). Remember that ValueType is the base class for all value types that you create (using the struct keyword). Two variables of a value type are equal if they are the same type and they have the same contents. ValueType.Equals() implements that behavior. Unfortunately, ValueType.Equals() does not have an efficient implementation. ValueType.Equals() is the base class for all value types. To provide the correct behavior, it must compare all the member variables in any derived type, without knowing the runtime type of the object. In C#, that means using reflection. As you'll see in Item 44, there are many disadvantages to reflection, especially when performance is a goal. Equality is one of those fundamental constructs that gets called frequently in programs, so performance is a worthy goal. Under almost all circumstances, you can write a much faster override of Equals() for any value type. The recommendation for value types is simple: Always create an override of ValueType.Equals() whenever you create a value type.

现在,是讨论instance Object.Equals()方法的时候了,包括何时以及如何来重写它。当实例的Equals()的默认行为和你的类型不一致的时候,就应该创建自己的实例Equals()版本。Object.Equals()方法使用对象标识来决定2个变量是否相等。默认的Object.Equals()方法和Object.ReferenceEquals()的行为是一样的,但是等等——值类型是不同的。System.ValueType重写了Object.Equals(),记住ValueType是所有你创建的值类型(使用struct关键字)的基类。对于值类型的2个变量,如果它们的类型相等并且拥有同样的内容,那么它们就相等。ValueType.Equals()实现了该行为。不幸的是,它的实现并不高效。ValueType.Equals()是所有值类型的基类,为了提供正确的行为,它必须在任何派生类里面比较所有的成员变量,而同时它又不知道对象的运行时类型。在C#里面,这就意味着要使用反射。正如你将会在Item44看到的一样,反射存在着很多的劣势,尤其当我们的目标是性能时。相等性是在程序中会被频繁调用的很多基础性结构中的一个,因此性能是一个值得考虑的目标。在几乎所有的环境下,你可以为任何值类型写出更快的Equals()重写版本。对值类型的建议很简单:无论何时创建一个值类型时,永远创建一个对ValueType.Equals()的重写。

You should override the instance Equals() function only when you want to change the defined semantics for a reference type. A number of classes in the .NET Framework Class Library use value semantics instead of reference semantics for equality. Two string objects are equal if they contain the same contents. Two DataRowView objects are equal if they refer to the same DataRow. The point is that if your type should follow value semantics (comparing contents) instead of reference semantics (comparing object identity), you should write your own override of instance Object.Equals().

对于引用类型,只有当你想改变它对相等的定义语义的时候,才来重写实例的Equals()方法。在.Net类库里面,有很多类使用了值类型对相等的定义来取代引用类型对相等的定义。对于2个字符串,如果它们包含的内容相等,我们就认为它们相等。对于2DataRowView对象,如果它们引用同一个DataRow,我们就认为它们相等。关键在于:如果你的类型应该遵守值类型的语义(比较内容),而不是遵守引用类型的语义(比较对象标识符),那么你应该编写自己的实例方法,重写Object.Equals()

Now that you know when to write your own override of Object.Equals(), you must understand how you should implement it. The equality relationship for value types has many implications for boxing and is discussed in Item 17. For reference types, your instance method needs to follow predefined behavior to avoid strange surprises for users of your class. Here is the standard pattern:

既然你知道了何时重写Object.Equals(),那么就必须理解该如何实现。值类型的相等关系对于装箱操作有很多实现,会在Item17中讨论。对于引用类型,你的实例方法需要遵循预定义的行为来避免你类型的用户的奇怪的惊讶。这里有一个标准的模式:

 

  1.        public class Foo
  2.         {
  3.             public override bool Equals(object right)
  4.             {
  5.                 // check null:
  6.                 // the this pointer is never null in C# methods.
  7.                 if (right == null)
  8.                     return false;
  9.                 if (object.ReferenceEquals(this, right))
  10.                     return true;
  11.                 // Discussed below.
  12.                 if (this.GetType() != right.GetType())
  13.                     return false;
  14.                 // Compare this type's contents here:
  15.                 return CompareFooMembers(
  16.                   this, right as Foo);
  17.             }
  18.       }

First, Equals() should never throw exceptionsit doesn't make much sense. Two variables are or are not equal; there's not much room for other failures. Just return false for all failure conditions, such as null references or the wrong argument types. Now, let's go through this method in detail so you understand why each check is there and why some checks can be left out. The first check determines whether the right-side object is null. There is no check on this reference. In C#, this is never null. The CLR throws an exception before calling any instance method through a null reference. The next check determines whether the two object references are the same, testing object identity. It's a very efficient test, and equal object identity guarantees equal contents.

首先,Equals()从来就不应该抛出异常——没什么意义。2个变量要么相等,要么不等,没有给其它失败的更多的空间。就为所有的失败条件返回false好了,比如空引用或者错误的参数类型。现在,让我们来从细节上认识该方法,那样你就能理解为什么一些检查保存下来而一些检查被去掉了。最先的检查判定右侧的对象是否为空。在这个引用上没有检查。在C#里面,从来不会为nullCLR在通过空引用调用任何实例方法之前,就会抛出异常。接下来的检查通过测试对象标识来决定2个对象的引用是否相同。这是一个很高效的测试,而且相等的对象标识保证了相等的内容。

The next check determines whether the two objects being compared are the same type. The exact form is important. First, notice that it does not assume that this is of type Foo; it calls this.GetType(). The actual type might be a class derived from Foo. Second, the code checks the exact type of objects being compared. It is not enough to ensure that you can convert the right-side parameter to the current type. That test can cause two subtle bugs. Consider the following example involving a small inheritance hierarchy:

接下来的检查判定2个正被比较的对象是否类型相同。精确的形式是重要的。首先要注意,并不假设thisFoo类型,而是调用this.GetType()。实际的类型可能是Foo类型的派生类。其次,检查被比较的对象的精确类型。能够保证可以将右侧的参数转换成当前的类型并不充足。这个测试会引起2个细微的bug。考虑下面的例子,其中涉及到了一个小小的继承体系:

  1.     public class B
  2.     {
  3.         public override bool Equals(object right)
  4.         {
  5.             // check null:
  6.             if (right == null)
  7.                 return false;
  8.  
  9.             // Check reference equality:
  10.             if (object.ReferenceEquals(this, right))
  11.                 return true;
  12.  
  13.             // Problems here, discussed below.
  14.             B rightAsB = right as B;
  15.             if (rightAsB == null)
  16.                 return false;
  17.  
  18.             return CompareBMembers(this, rightAsB);
  19.         }
  20.     }
  21.  
  22.     public class D : B
  23.     {
  24.         // etc.
  25.         public override bool Equals(object right)
  26.         {
  27.             // check null:
  28.             if (right == null)
  29.                 return false;
  30.  
  31.             if (object.ReferenceEquals(this, right))
  32.                 return true;
  33.  
  34.             // Problems here.
  35.             D rightAsD = right as D;
  36.             if (rightAsD == null)
  37.                 return false;
  38.  
  39.             if (base.Equals(rightAsD) == false)
  40.                 return false;
  41.  
  42.             return CompareDMembers(this, rightAsD);
  43.         }
  44.    }
  45.     //Test:
  46.     B baseObject = new B();
  47.     D derivedObject = new D();
  48.  
  49.     // Comparison 1.
  50.     if (baseObject.Equals(derivedObject))
  51.         Console.WriteLine("Equals");
  52.     else
  53.         Console.WriteLine("Not Equal");
  54.  
  55.     // Comparison 2.
  56.     if (derivedObject.Equals(baseObject))
  57.         Console.WriteLine("Equals");
  58.     else
  59.         Console.WriteLine("Not Equal");

Under any possible circumstances, you would expect to see either Equals or Not Equal printed twice. Because of some errors, this is not the case with the previous code. The second comparison will never return TRue. The base object, of type B, can never be converted into a D. However, the first comparison might evaluate to true. The derived object, of type D, can be implicitly converted to a type B. If the B members of the right-side argument match the B members of the left-side argument, B.Equals() considers the objects equal. Even though the two objects are different types, your method has considered them equal. You've broken the symmetric property of Equals. This construct broke because of the automatic conversions that take place up and down the inheritance hierarchy.

在任何可能的环境下,你可能期望看到Equals或者Not Equals被打印两次。由于一些错误,这不是前面代码会出现的情况。第二个比较永远不会返回true。基类B的对象,不可能被转换成一个D的对象。然而,第一个比较可能会计算出true。派生类D的对象,能够隐士的被转换为一个B的对象。如果右侧参数的B成员与左侧参数的B成员相匹配,B.Equals()会认为对象相等。即使2个对象时不同类型的,你的方法已经认为它们相等了。你已经打破了相等性的对称性。这个构造是不成立的,因为在继承体系里面会发生向上或者向下的自动转换。

When you write this, the D object is explicitly converted to a B:

当你写下这些的时候,D对象已经被隐士的转换成了一个B类型的对象:

  1. baseObject.Equals(derived);

If baseObject.Equals() determines that the fields defined in its type match, the two objects are equal. On the other hand, when you write this, the B object cannot be converted to a D object:

如果baseObject.Equals()判定了在该类型内部定义的字段是匹配的,那么2个对象就相等。从另一方面说,当你写下这些时,B对象不能被转换成一个D对象:

  1. derivedObject.Equals(base)

The B object cannot be converted to a D object. The derivedObject.Equals() method always returns false. If you don't check the object types exactly, you can easily get into this situation, in which the order of the comparison matters.

B对象不能被转换成D对象。derivedObject.Equals()方法总是放回false。如果你不精确的检查对象的类型,就可能陷入这种情况:比较的左右次序影响了结果。

There is another practice to follow when you override Equals(). You should call the base class only if the base version is not provided by System.Object or System.ValueType. The previous code provides an example. Class D calls the Equals() method defined in its base class, Class B. However, Class B does not call baseObject.Equals(). It calls the version defined in System.Object, which returns true only when the two arguments refer to the same object. That's not what you want, or you wouldn't have written your own method in the first place.

当你重写Equals()时,存在另外一种实践。只有当基类版本不是由System.Object或者System.ValueType提供的时候,你才应该调用基类。前面的代码提供了一个示例。类D调用了在它的基类B里面定义的Equals()方法,然而,类B并不调用baseObject.Equals(),它调用在System.Object里面定义的版本:只有当2个参数引用同一个对象的时候才返回true。这不是你想要的,否则你就不会在第一个地方写下你自己的代码。

The rule is to override Equals() whenever you create a value type, and to override Equals() on reference types when you do not want your reference type to obey reference semantics, as defined by System.Object. When you write your own Equals(), follow the implementation just outlined. Overriding Equals() means that you should write an override for GetHashCode(). See Item 10 for details.

规则就是:无论何时当你创建一个值类型的时候,重写Equals()方法;当你不想让你的引用类型遵循引用语义的时候(就像在System.Object中定义的那样)那就在引用类型上重写Equals()方法。当你编写自己的Equals()时候,遵从刚刚描述的实现大纲。重写Equals()意味着你同时应该重写GetHashCode()。详见Item10

Three down, one to go: operator==(). Anytime you create a value type, redefine operator==(). The reason is exactly the same as with the instance Equals() function. The default version uses reflection to compare the contents of two value types. That's far less efficient than any implementation that you would write, so write your own. Follow the recommendations in Item 17 to avoid boxing when you compare value types.

已经搞定了3个,我们来看下一个:==()操作符。任何时候,当你创建一个值类型的时候,都要重定义==()操作符。理由和实例上的Equals()方法是完全一样的。默认的版本使用反射来比较2个值类型的内容。这和任何你自己编写的实现比较起来都是远远低效率的,因此,自己编写吧。遵从Item 17里面的建议,当比较值类型的时候,避免装箱操作。

Notice that I didn't say that you should write operator==() whenever you override instance Equals(). I said to write operator==() when you create value types. You should rarely override operator==()when you create reference types. The .NET Framework classes expect operator==() to follow reference semantics for all reference types.

注意,我并没有说:无论何时当你重写实例的Equals()方法时,都需要编写==()操作符。我说得是:当你创建值类型是,编写==()操作符。当你创建引用类型时,几乎不用重写==()操作符。.Net框架类期望,所有的引用类型的==()操作符都遵循引用语义。

C# gives you four ways to test equality, but you need to consider providing your own definitions for only two of them. You never override the static Object.ReferenceEquals() and static Object.Equals() because they provide the correct tests, regardless of the runtime type. You always override instance Equals() and operator==() for value types to provide better performance. You override instance Equals() for reference types when you want equality to mean something other than object identity. Simple, right?

C#给你提供了4种方法来检测相等性,但是你需要考虑仅仅为其中的2个提供自己的定义。从不应该重写static Object.ReferenceEquals()static Object.Equals(),因为它们提供可正确的检测,忽略运行时类型。总是要为值类型重写实例的Equals()==()操作符,来提供更好的性能。对于引用类型,当你希望相等性意味着其他意思而不是对象标识时,重写实例的Equals()。很简单,对吧?

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值