《Effective C#》讀書筆記——條目6:理解幾個等同性判斷之間的關系


  創建自定義的類型時(無論是類還是struct),應為類型定義”同等性“的含義。在C#中為我們提供了四種不同的函數來判斷兩個對象是否”相等“:

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

引用相等和值相等

  C#允許我們創建兩種類型:值類型和引用類型。如果兩個引用類型的變量指向的是同一個對象,它們將被認為是“引用相等”。如果兩個值類型的變量類型相同且包含同樣的內容,它們被認為是“值相等”。這也正是同等性判斷需要如此多方法的原因。

 

為什么不應該重新定義靜態的ReferenceEquals()和Equals()方法

  對於前兩個靜態函數,我們永遠都不應該去重新定義,因為它們已經很好的完成了它們的工作,且判斷與運行時具體類型無關:判斷兩個不同變量的對象標志(object identity)是否相等。無論比較的是值類型還是引用類型靜態的ReferenceEquals方法的判斷依據都是對象標志,所以比較兩個值類型永遠返回false,即使是值類型和它本身比較也是,這是因為裝箱的原因。當我們不知道兩個變量的運行時類型時,可以使用靜態的Equals方法來判斷兩個變量是否相等,同等判斷是以來類型,所以靜態的Equals通過委托其中一個類型來做的判斷的,靜態的Ojbect.Equals()方法實現如下:

 1   public static new bool Equals(object left, object right)
 2         {
 3             //檢查對象引用
 4             if (Object.ReferenceEquals(left, right))
 5                 return true;
 6             //是否為null
 7             if (Object.ReferenceEquals(left, null))
 8                 return false;
 9             //調用實例的Equals()方法
10             return left.Equals(right);
11         }

我們可以看到靜態的Equals()方法將判斷的工作交給left參數的實例Equals()方法執行,所以它會使用left參數的類型中定義的規則來進行等同性判斷。

 

什么情況下需要重寫Equals()實例方法

  當Equals()實例方法的默認行為與我們的類型要求不一致時,自然需要覆寫。該方法默認使用對象標志判斷,即比較兩個對象是否引用相等。

值類型(使用Struct關鍵字創建的類型):System.ValueType(所有值類型的基類)覆寫了Object.Equals()方法:兩個值類型變量類型相同,內容一致,兩個變量才認為相等。由於ValueType是所有值類型的基類,為了提供正確的行為,必須能夠在不知道對象運行時類型的情況下比較其派生類中的所有成員變量,這意味着要使用反射來實現。而反射又是非常損耗性能的。而等同性判斷又是一個非常基礎的功能,所以我們有必要(追求性能時)為自己的值類型提供一個更快的Equals()覆寫版本。

引用類型:只有我們希望更改其預定義的語義時,才應該覆寫Equals()方法。在.NET類庫中許多類都是使用值語義而不是引用語義來做等同判斷的,例如:如果兩個string對象包含相同的內容就被認為相等;若兩個DataRowView對象引用同一個DataRow,那么將被認為相等。

 

如何覆寫Equals()實例方法

   覆寫Equlas()實例方法是需要實現IEquatable<T>接口,該接口包含了一個方法Equals(Tother),實現了IEquatable<T>以為着你的類型支持類型安全的等同性比較。若你認為Equals()僅僅應該在比較的兩邊屬於同一個類型時才返回true,那么IEquatable<T>將會讓編譯器幫你找到可能出現的種種類型相關的不相等情況。

下面是覆寫System.Object.Equals()實例方法的標准實現模式(只是一個示例,具體情況還需根據我們的代碼需求來確定):

 1         public class foo : IEquatable<foo>
 2         {
 3             public override bool Equals(object right)
 4             {
 5                 //是否為null
 6                 if (Object.ReferenceEquals(right, null))
 7                     return false;
 8                 //是否引用相等
 9                 if (Object.ReferenceEquals(this, right))
10                     return true;
11                 //可能是子類,所以需要精確的類型判斷
12                 if (this.GetType() != right.GetType())
13                     return false;
14                 //調用實例的Equals()方法
15                 return this.Equals(right as foo);
16             }
17 
18             //IEquatable<foo> 成員
19             public bool Equals(foo other)
20             {
21                 //略去
22                 return true;
23             }
24         }

我們仔細觀察這個實現:第一個堅持判斷右邊對象是否為null,對於this指針引用則不需要這一步,因為在C#中this指針永遠不會為null。第二個判斷兩個對象是否為同一個引用,如果兩個對象引用相同,則對象內容一定相等。第三個函數用來判斷兩個對象的類型是否相同。這里使用精確的比較是非常重要的。首先,沒有假設this指針的類型為Foo,而是再次調用this.GetType()獲取,因為實際的類型可能繼承自Foo。

 

小節:

對於所有的值類型,都應該覆寫其Equals()方法;對於引用類型,當System.Object提供的引用語義不能滿足我們的需求時,才應該去覆寫Equals()方法。覆寫Equals()方法時也應該同時覆寫GetHashCode()方法(條目7)。對於operator==()則比較簡單,只要創建的是值類型,都必須重新覆寫一個operator==(),理由和覆寫ValueType.Equals()實例函數完全一樣。引用類型應該盡量避免覆寫operator==(),.NET 希望所有引用類型都應用的operator==()都遵循引用語義,因為系統提供的默認版本時通過比較兩個值類型實例的內容,並且是用反射來實現的。(其實如果對於性能不是那么敏感的話可以忽略)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM