程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則9:明白幾個相等運算之間的關系

Effective C#原則9:明白幾個相等運算之間的關系

編輯:關於C#

明白ReferenceEquals(), static Equals(), instance Equals(), 和運算行 符==之間的關系。

當你創建你自己的類型時(不管是類還是結構),你要 定義類型在什麼情況下是相等的。C#提供了4個不同的方法來斷定兩個對象是否 是相等的:

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 );

這種語言讓你可以為上面所有的4種方法創建自己的版 本。But just because you can doesn't mean that you should.你或許從 來不用重新定義前面兩個方法。你經常遇到的是創建你自己實例的Equals()方法 ,來為你的類型定義語義;或者你偶而重載運==運算符,但這只是為了考慮值類 型的性能。幸運的是,這4個方法的關系,當你改變其中一個時,會影響到其它 的幾個。是的,須要4個方法來完整的測試對象是否完全相等。但你不用擔心, 你可以簡單的搞定它們。

和C#裡其它大多數復雜元素一樣,這個(對相等 的比較運算)也遵守這樣的一個事實:C#充許你同時創建值類型和引用類型。兩 個引用類型的變量在引用同一個對象時,它們是相等的,就像引用到對象的ID一 樣。兩個值類型的變量在它們的類型和內容都是相同時,它們應該是相等的。這 就是為什麼相等測試要這麼多方法了。

我們先從兩個你可能從來不會修 改的方法開始。Object.ReferenceEquals()在兩個變量引用到同一個對象時返回 true,也就是兩個變量具有相同的對象ID。不管比較的類型是引用類型還是值類 型的,這個方法總是檢測對象ID,而不是對象內容。是的,這就是說當你測試兩 個值類型是否相等時,ReferenceEquals()總會返回false,即使你是比較同一個 值類型對象,它也會返回false。這裡有兩個裝箱,會在原則16中討論。(譯注: 因為參數要求兩個引用對象,所以用兩個值類型來調用該方法,會先使兩個參數 都裝箱,這樣一來,兩個引用 對象自然就不相等了。)

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(),這是因為它已經確實實現了它自己的功能:檢測兩 個變量的對象ID(是否相同)。

第二個可能從來不會重新定義的方法是靜 態的Object.Equals()。這個方法在你不清楚兩個參數的運行類型時什麼時,檢 測它們是否相等。記住: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);
}

這個示例代碼展示的兩個方法是我還沒有討論 的:操作符==()和實例的Equals()方法。我會詳細的解釋這兩個,但我還沒有准 備結束對靜態的Equals()的討論。現在,我希望你明白,靜態的Equals()是使用 左邊參數實例的Equals()方法來斷定兩個對象是否相等。

與 ReferenceEquals()一樣,你或許從來不會重新定義靜態的Object.Equals()方法 ,因為它已經確實的完成了它應該完成的事:在你不知道兩個對象的確切類型時 斷定它們是否是一樣的。因為靜態的Equals()方法把比較委托給左邊參數實例的 Equals(),它就是用這一原則來處理另一個類型的。

現在你應該明白為 什麼你從來不必重新定義靜態的ReferenceEquals()以及靜態的Equals()方法了 吧。現在來討論你須要重載的方法。但首先,讓我們先來討論一下這樣的一個與 相等相關的數學性質。你必須確保你重新定義的方法的實現要與其它程序員所期 望的實現是一致的。這就是說你必須確保這樣的一個數學相等性質:相等的自反 性,對稱性和傳遞性。自反性就是說一個對象是等於它自己的,不管對於什麼類 型,a==a總應該返回true;對稱就是說,如果有a==b為真,那麼b==a也必須為真 ;傳遞性就是說,如果a==b為真,且b==c也為真,那麼a==c也必須為真,這就是 傳遞性。

現在是時候來討論實例的Object.Equals()函數了,包括你應該 在什麼時候來重載它。當默認的行為與你的類型不一致時,你應該創建你自己的 實例版本。Object.Equals()方法使用對象的ID來斷定兩個變量是否相等。這個 默認的Object.Equals()函數的行為與Object.ReferenceEquals()確實是一樣的 。但是請注意,值類型是不一樣的。System.ValueType並沒有重載 Object.Equals(),記住,System.ValueType是所有你所創建的值類型(使用關鍵 字struct創建)的基類。兩個值類型的變量相等,如果它們的類型和內容都是一 樣的。ValueType.Equals()實現了這一行為。不幸的是,ValueType.Equals()並 不是一個高效的實現。ValueType.Equals()是所有值類型的基類(譯注:這裡是 說這個方法在基類上進行比較)。為了提供正確的行為,它必須比較派生類的所 有成員變量,而且是在不知道派生類的類型的情況下。在C#裡,這就意味著要使 用反射。正如你將會在原則44裡看到的,對反射而言它們有太多的不利之處,特 別是在以性能為目標的時候。

相等是在應用中經常調用的基礎結構之一 ,因此性能應該是值得考慮的目標。在大多數情況下,你可以為你的任何值類型 重載一個快得多的Equals()。簡單的推薦一下:在你創建一個值類型時,總是重 載ValueType.Equals()。

你應該重載實例的Equals()函數,僅當你想改 變一個引用類型所定義的(Equals()的)語義時。.Net結構類庫中大量的類是使用 值類型的語義來代替引用類型的語義。兩個字符中對象相等,如果它們包含相同 的內容。兩個DataRowViewc對象相等,如果它們引用到同一個DataRow。關鍵就 是,如果你的類型須要遵從值類型的語義(比較內容)而不是引用類型的語義(比 較對象ID)時,你應該自己重載實例的Object.Equals()方法。

好了,現 在你知道什麼時候應該重載你自己的Object.Equals(),你應該明白怎樣來實現 它。值類型的比較關系有很多裝箱的實現,裝箱在原則17中討論。對於用戶類型 ,你的實例方法須要遵從原先定義行為(譯注:前面的數學相等性質),從而避免 你的用戶在使用你的類時發生一些意想不到的行為。這有一個標准的模式:

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 );
 }
}

首先,Equals()決不應該拋出異常,這感覺不大好。兩個變 量要麼相等,要麼不等;沒有其它失敗的余地。直接為所有的失敗返回false, 例如null引用或者錯誤參數。現在,讓我們來深入的討論這個方法的細節,這樣 你會明白為什麼每個檢測為什麼會在那裡,以及那些方法可以省略。第一個檢測 斷定右邊的對象是否為null,這樣的引用上沒有方法檢測,在C#裡,這決不可能 為null。在你調用任何一個引用到null的實例的方法之前,CLR可能拋出異常。 下一步的檢測來斷定兩個對象的引用是否是一樣的,檢測對象ID就行了。這是一 個高效的檢測,並且相等的對象ID來保證相同的內容。

接下來的檢測來 斷定兩個對象是否是同樣的數據類型。這個步驟是很重要的,首先,應該注意到 它並不一定是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" );

在任何可能的情況下,你都希望要麼看到兩個Equals或者兩個 Not Equal。因為一些錯誤,這並不是先前代碼的情形。這裡的第二個比較決不 會返回true。這裡的基類,類型B,決不可能轉化為D。然而,第一個比較可能返 回true。派生類,類型D,可以隱式的轉化為類型B。如果右邊參數以B類型展示 的成員與左邊參數以B類型展示的成員是同等的,B.Equals()就認為兩個對象是 相等的。你將破壞相等的對稱性。這一架構被破壞是因為自動實現了在繼承關系 中隱式的上下轉化。

當你這樣寫時,類型D被隱式的轉化為B類型:

baseObject.Equals( derived )

如果 baseObject.Equals()在它自己所定義的成員相等時,就斷定兩個對象是相等的 。另一方面,當你這樣寫時,類型B不能轉化為D類型,

derivedObject.Equals( base )

B對象不能轉化為 D對象,derivedObject.Equals()方法總是返回false。如果你不確切的檢測對象 的類型,你可能一不小心就陷入這樣的窘境,比較對象的順序成為一個問題。

當你重載Equals()時,這裡還有另外一個可行的方法。你應該調用基類 的System.Object或者System.ValueType的比較方法,除非基類沒有實現它。前 面的代碼提供了一個示例。類型D調用基類,類型B,定義的Equals()方法,然而 ,類B沒有調用baseObject.Equals()。它調用了Systme.Object裡定義的那個版 本,就是當兩個參數引用到同一個對象時它返回true。這並不是你想要的,或者 你是還沒有在第一個類裡的寫你自己的方法。

原則是不管什麼時候,在 創建一個值類型時重載Equals()方法,並且你不想讓引用類型遵從默認引用類型 的語義時也重載Equals(),就像System.Object定義的那樣。當你寫你自己的 Equals()時,遵從要點裡實現的內容。重載Equals()就意味著你應該重寫 GetHashCode(),詳情參見原則10。

解決了三個,最後一個:操作符==() ,任何時候你創建一個值類型,重新定義操作符==()。原因和實例的Equals()是 完全一樣的。默認的版本使用的是引用的比較來比較兩個值類型。效率遠不及你 自己任意實現的一個,所以,你自己寫。當你比較兩個值類型時,遵從原則17裡 的建議來避免裝箱。

注意,我並不是說不管你是否重載了實例的Equals (),都還要必須重載操作符==()。我是說在你創建值類型時才重載操作符==()。 .Net框架裡的類還是期望引用類型的==操作符還是保留引用類型的語義。

C#給了你4種方法來檢測相等性,但你只須要考慮為其中兩個提供你自 己的方法。你決不應該重載靜態的Object.ReferenceEquals()和靜態的 Object.Equals(),因為它們提供了正確的檢測,忽略運行時類型。你應該為了 更好的性能而總是為值類型實例提供重載的Equals()方法和操作符==()。當你希 望引用類型的相等與對象ID的相等不同時,你應該重載引用類型實例的Equals() 。簡單,不是嗎?

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved