Effective C# Item9:理解几个相等判断之间的关系

    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 );
    对于一个判断是否相等的操作,为什么会有四种形式呢,究其原因,还要看C#的数据类型,C#的数据类型分为值类型和引用类型,其中值类型直接存储在堆栈上,而引用类型的值存储在堆上,在栈中保留一个指向堆中地址的引用。这样在判断相等的时候,就会产生两种判断方式:1)判断变量在堆栈上存储的值是否相等,这对于值类型来说,就足够了,但是对于引用类型来说,只是判断了堆栈中保存的引用是否相等,还是不全面的;2)判断变量在堆中存储的值是否相等,这主要用于引用类型。

    关于如何判断“相等”,如果两个引用类型的变量指向同一个对象,那么它们将被认为是“引用相等”;如果两个值类型的变量类型相同,而且包含同样的内容,它们被认为是“值相等”。

    对于上述提供的四种用于判断“相等”的方法,其中前两种都是Object类带有的静态方法,其中Object.ReferenceEquals()方法用于判断两个变量的对象标识是否相同,不论是值类型还是引用类型,都是判断是否“引用相等”,而不是“值相等”,这意味着如果我们对于两个值类型使用该方法,那么总是会返回false。

    上述描述的第二种方式,是Object类型的静态方法Equals(),当我们不知道两个变量的运行时类型时,可以使用这个方法来判断两个变量是否相等,由于刚方法并不知道变量的类型,因此,“相等判断”的操作是依赖于类型的,即它会调用其中一个对象实例的Equals方法。静态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 }

    上述第三种方式,是对象实例的Equals()方法,其中System.Object类作为所有类的基类,本身也定义了Equals()方法,Object实例中的Equals()方法,是判断“引用相等”,其行为和ReferenceEquals()方式完全一样。而System.ValueType作为所有值类型的基类,它重写了Equals()方法,在重写方法中,是按照“值相等”的方式来进行判断的,但是,ValueType重写的Equals()方法效率不高,原因是它使用了反射来得到对象的所有属性,进而判断属性的值是否相同,这样会导致性能很差,因此,当我们定义一个值类型时,应该总是重写Equals()方法。

    一般,我们重写Equals()方法的形式如下。

代码
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
10 if (object.ReferenceEquals( this, right ))
11 return true;
12
13 // Discussed below.
14 if (this.GetType() != right.GetType())
15 return false;
16
17 // Compare this type's contents here:
18 return CompareFooMembers(
19 this, right as Foo );
20 }
21 }
22
23

    我们在重写Equals()方法时,应该遵循以下三个原则:

  1. 自反性,即a=a
  2. 交换性,即如果a=b,那么b=a
  3. 传递性,即如果a=b,b=c,那么a=c

    当我们在一个有类继承层次关系的结构中,为父类和子类都重写Equals()方法, 那么很可能造成非常诡异的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 }
46
47 //Test:
48 B baseObject = new B();
49 D derivedObject = new D();
50
51 // Comparison 1.
52 if (baseObject.Equals(derivedObject))
53 Console.WriteLine( "Equals" );
54 else
55 Console.WriteLine( "Not Equal" );
56
57 // Comparison 2.
58 if (derivedObject.Equals(baseObject))
59 Console.WriteLine( "Equals" );
60 else
61 Console.WriteLine( "Not Equal" );
62
63
    如果你认为上述代码应该返回两个“Equals”或者两个“Not Equals”,那么无可厚非,但实际上,对于上述的两次比较,第二次总是会返回false,而第一次有时会返回true,有时会返回false。原因在于类型转换,子类型是可以默认转换为父类型的,但是父类型不可以转换为子类型。

    因此,当我们重写Equals()方法时,有一个很好的建议:如果基类的Equals()方法不是由System.Object或者System.ValueType提供的话,那么我们也应该在重写子类的Equals()方法时,调用基类的Equals()方法。

 

    关于上述判断“相等”的方式,总结如下:

  1. 永远不要重写Object类的ReferenceEquals()和Equals()两个静态方法。
  2. 对于值类型来说,为了提高效率,我们应该总是重写实例的Equals()方法和==()操作符,对于引用类型,如果我们认为相等的含义并非是对象标识相同的话,那么也需要重写Equals()方法,但是不应该重写==()操作符,.NET建议所有引用类型上应用==操作时,都遵循“引用相等”。
posted @ 2010-01-09 14:13  李潘  阅读(772)  评论(0编辑  收藏  举报