軌道
/
C#
C#
/
課程大綱
/
相等性
相等

相等性 在 C#

5 個練習

關於 相等性

這個程式設計練習展示了 C# 中相等的幾個性質:

Object.Equals()

這個程式設計練習涵蓋的主題

  • 簡單型別(字串和基本型別)通常用==和!=來測試是否相等。這比使用這些型別同樣提供的 Equals() 方法更道地。寫 Java 的開發者在處理字串時要留意:== 在 C# 中是依值比較,但在 Java 中卻是依參照比較,等他們回頭寫 Java 時別忘了這點。
  • 參照型別(類別的執行個體)則使用繼承自object的Equals()方法來比較。如果你做相等測試的目的是確認兩個物件是同一個執行個體,那麼直接沿用object的實作就夠了。如果不是,你就需要覆寫object.Equals()。
  • 如果你知道類別的所有執行個體都只在同一個地方建立,譬如某個遊戲或模擬中的角色,那麼參照相等就足夠了。不過,同一個現實世界的實體很可能會建立出多個執行個體(例如來自資料庫、使用者輸入,或網路請求)。這種情況下,就必須測試能唯一識別該實體的各個值是否相等。因此必須覆寫Equals()。
  • 覆寫後的Equals()會包含對簡單型別成員使用==的相等測試,以及對參照型別成員遞迴呼叫Equals()的測試。
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • 靜態方法object.ReferenceEquals()用來比較兩個物件,判斷它們是不是同一個執行個體。這能讓程式更清楚,而且在Equals()和==已被覆寫或重載的地方更是必要。
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

其他相關主題

  • 除了public override bool Equals(object obj)之外,IDE 通常還會產生多載protected bool Equals(FacialFeatures other),供牽涉到繼承時使用。衍生類別可以呼叫基底類別的Equals(),再補上自己的測試。
  • 除非你已經在你的類別中重載了==運算子以及Equals()方法(參見operator-overloading練習),或者你只在意參照是否相等,否則不要使用==。
  • structs 的相等測試會在structs練習中討論。
  • tuples 的相等會在tuples練習中討論。
  • 許多開發者會倚賴 IDE 來提供相等方法的實作,因為 IDE 會處理所有相等的細節。例如,JetBrains 的 RIDER(v 2020.1)會為類別產生下列相等方法:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • 如果你想改良 IDE 產生的程式碼,請小心。那些程式碼一般來說能抵擋 null 和型別寫錯的物件,而且最佳化得還不錯。
  • 委派的相等測試並沒有在練習中特別討論。
  • 陣列和大多數集合都沒有內建的相等測試。LINQ(會在後面的練習中討論)提供了 SequenceEquals(),但沒有 LINQ 時,就只能逐一走訪兩個集合、個別比較項目。
  • 關於如何在你自己寫的類別中使用==和!=,請參見operator-overloading練習。

Object.GetHashCode()

  • object.GetHashCode() 會以 32 位元整數的形式回傳雜湊碼。雜湊碼會被字典和集合類別(例如 Dictionary<T> 和 HashSet<T>)用來有效率地存放和取出物件。以字典來說,雜湊是針對鍵進行的。
  • C# 開發者普遍認為,如果你覆寫了Equals(),也應該覆寫GetHashCode()。Equals()和GetHashCode()之間有一種關係,必須成立才能讓字典、雜湊集合以及其他使用雜湊碼的類別正確運作。你應該好好實作這個方法,才不會為日後可能加入雜湊碼集合的維護者埋下陷阱。
  • 雜湊碼和相等之間的關係是:如果兩個物件相等(Equal() 回傳 true),那麼這兩個物件的GetHashCode()必須回傳相同的值。反過來則不成立,這個關係並不對稱。想像一個查找函式,它先根據雜湊碼找到某個「桶子」,再用相等測試挑出特定的項目。
  • 建立雜湊碼最簡單的方式,就是呼叫 HashCode.Combine(),傳入相等測試所用的那些值(或其一部分)。請記住,你提供給Combine()的資訊越多,雜湊實作的效率通常就越好。
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • 在雜湊集合使用期間,相等測試所用的值必須保持穩定。如果你用一組值把物件加入集合,之後又改變這些值,雜湊碼就不會再指向正確的「桶子」。實際上這表示物件應該是不可變的。其他做法則可能讓維護者踩到陷阱。不可變性會在其他的練習中討論。
  • 你有可能設計出比程式庫常式所產生更好的雜湊碼,但那不是因為你對資料的特性有深入了解,就是因為集合非常單純,值可以直接使用而不需要雜湊。這樣多花的功夫可能不值得。

效能強化

為了稍微提升效能,特別是在物件隸屬於集合的情況下,你可以加入一個多載成員public bool Equals(T other)。

這可以省下參照型別的一些型別檢查,也能為實值型別省下一次裝箱,因為它們不需要為了當作public override bool Equals(object other)的引數而轉換成物件(裝箱)。

如果你在類別中加入IEquatable<T>介面,就會需要實作這個多載。除非你的程式碼含有接收IEquatable<T>型別物件的常式(而且大概會做一些與相等有關、且不管實作類別是什麼的有趣事情),否則實在沒有其他有力的理由要加入這個介面。

IEqualityComparer<T>

如果你的類別可以用兩種不同方式唯一識別,例如一個Person類別同時有 SSID 和唯一的電子郵件地址,那麼 .NET 提供了一種方法,讓兩個不同的集合使用不同的雜湊碼和相等測試。兩者可以各自有IEqualityComparer<T>的不同實作,各帶自己的Equals()和GetHashCode()方法。你可以有一個以 SSID 為鍵的字典,以及另一個以電子郵件地址為鍵的字典。

當IEqualityComparer<T>派上用場時,你通常還是會覆寫項目類別上的Equals()和GetHashCode(),以避免在集合類別之外出問題。

使用IEqualityComparer<T>時要考慮的一點是,私有方法等等將無法供相等測試使用。

如果只會用到一個雜湊集合,那麼避免使用IEqualityComparer<T>、把相等和雜湊碼封裝在物件本身裡,可能會比較好。物件和雜湊集合之間這種關鍵相依性無法由編譯器強制檢查,本身就已經不理想了;但如果雜湊和比較的邏輯又放在別的地方,維護者就更容易在修改類別成員時忽略對集合造成的後果。

關於浮點數相等的注意事項

有個基本型別會讓不小心的程式設計者踢到鐵板,那就是測試浮點數值的相等。這在floating-point-numbers概念的 about.md 文件中有討論。

相等與繼承

這篇文章說明了在相等與繼承方面必須做的一些決定。只有當你同時操作某個類別和衍生類別的執行個體,而且需要測試兩者是否相等時,這些才會與你有關。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 相等性

練習已鎖定

再解鎖 4 個練習,就能練習 相等性