Effective C# Item9:理解幾個相等判斷之間的關係

iDotNetSpace發表於2010-01-11
   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 perator==( 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建議所有引用型別上應用==操作時,都遵循“引用相等”。

    
作者:李勝攀
    
出處:http://wing011203.cnblogs.com/

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-624812/,如需轉載,請註明出處,否則將追究法律責任。

相關文章