现在的位置: 首页 > 综合 > 正文

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

2014年01月05日 ⁄ 综合 ⁄ 共 13563字 ⁄ 字号 评论关闭

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 T
rue 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

抱歉!评论已关闭.