C# 相等性比較

Tip

本文屬於 基礎部分, 適合已經至少懂一種程式語言並正在學習 C# 的開發者。 如果你是程式新手,建議先從 入門 教學開始。

來自另一種語言? 在 Java 中,對物件使用 ==,以及在 JavaScript 中對物件使用 ===,測試的是識別是否相同,而不是內容。 C# 類別預設運作方式相同。 在 Python 中,==預設會呼叫 __eq__,並依內容進行測試,類似於 C# records 的比較方式。 C# 結構 體在呼叫 Equals時也會依值做比較。

C# 區分兩種平等。 價值相等 是指當兩個實例的資料相符時,兩者相等。 參考相等 性表示兩個變數只有在記憶體中指向同一物件時才相等。 這個條件也稱為 同一性。 價值類型通常比較資料,參考類型則比較身份。 型別作者可以變更這些預設值,但這種思維模型可以防止一些細微的錯誤,例如兩個看起來完全相同的物件卻不被視為相等,或是透過其中一個變數進行的變更悄悄改變了另一個變數所見的內容。

值型態、參考型態與等式預設值

C# 中的每個型態要麼是 值型態 ,要麼是 參考型態值型態直接將資料儲存在變數中。 參考型別會保留對物件的參考。 當你將一個參考型變數指派給另一個變數時,兩個變數都會指向同一個物件。 欲了解更多關於值型別與參考型別的資訊,請參閱 型別系統概述

預設的等式行為通常遵循以下類型:

  • 內建的數值型別和 枚舉 是值型別。 當兩個 int 變數的數值相符時,它們就是相等的。
  • 結構是值類型。 一般的 struct 在你呼叫 Equals 時會使用值相等比較。
  • 元組 是值型態。 當兩個元組的所有元素值都相符時,它們就是相等的。
  • 類別 是參考型別。 一個純類別使用參考等號,因此 ==Equals 會測試兩個變數是否指向同一物件。

類別使用參考相等性。 兩個擁有相同資料的獨立物件不等於,但兩個指向同一物件的變數則相等:

var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");

Console.WriteLine(order1 == order2);               // => False
Console.WriteLine(order1.Equals(order2));          // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False

Order order3 = order1;
Console.WriteLine(order1 == order3);               // => True

struct 透過 Equals 顯示值相等。 當兩個結構體實例的欄位相符時,它們相等:

var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

Console.WriteLine(pt1.Equals(pt2)); // => True

純結構體不會有預設的 == 運算子。 在純結構上寫入 p1 == p2 時,只有當該結構宣告自己的 operator ==時才會編譯。 如果你需要為結構體提供比較運算子,請將 ==!= 定義為一對,並使其與 EqualsGetHashCode 保持一致。

元組也是值類型。 當每個元素的值都相符時,兩個元組相等。 命名元組中的元素名稱是編譯時的便利,且在比較時不會被考慮。 只有立場和價值觀才重要:

var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

Console.WriteLine(t1 == t2); // => True

欲了解更多關於元組語法與解構的資訊,請參見 元組與解構

使用 Object.ReferenceEquals 直接測試身分識別

ReferenceEquals 無論型別如何覆寫 Equals 或重載 ==,都必須測試同一性。 當你需要確認兩個變數是否指向完全相同的物件時,可以用它作為身份診斷:

var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;

Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True

常見的用途是在 Equals 覆寫中直接略過完整比較:當兩個引數是同一個參考時,就一定相等,無須檢查各個欄位。

Note

當變數被指定為 介面時, == 會檢查介面變數是否指向同一物件。 對 Equals 的呼叫仍會執行底層物件的實作。

Note

ReferenceEquals 在比較值型別時,即使兩個參數包含相同的值,也會回傳 false 。 此行為發生是因為每個值型態參數在傳遞給 ReferenceEquals時會獨立被框定為獨立的堆積物件。

類型可以定義不同的等式語意

類型 可以 定義與預設行為不同的等式語意。 最常見的原因是實現價值平等。 如果你建立一個代表資料的類型,例如銀行帳戶、庫存中的產品或系統中的使用者,請將值相同的實例視為相等。 選擇記錄 類型 以實現價值等號,編譯器會自動產生所有必要的等號成員。

Note

字串是類別,但 ==Equals 比較的是字串內容,而不是物件身分。

  • ==:相等運算子。 大多數類型都使用此運算子作為主要的等式檢查。 其行為取決於該型別是否有內建運算子或使用者定義 == 的運算子。
  • !=:不等式算子。 當一個型別定義了使用者定義 == 的運算子時,它也必須定義 !=
  • Equals:一個被所有類型繼承的虛擬方法。 你可以覆寫它,以變更某個型別的相等性語意。
  • GetHashCode:一種由雜湊式集合使用的虛擬方法。 當兩個值相等時,它們的雜湊碼也必須相等。
  • ReferenceEquals:一種靜態方法,總是測試身份。

利用紀錄達成價值平等

使用 record 修飾符來給予以資料為中心的型別值等值,當該型別可以是記錄時。 編譯器會產生 EqualsGetHashCode 以及 ==/!= 成員,用來比較每個已宣告的屬性值。

A record class 仍是參考型別,但它比較的是值而非單位:

var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");

Console.WriteLine(person1 == person2);               // => True
Console.WriteLine(person1.Equals(person2));          // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False

ReferenceEquals 確認 person1person2 是記憶體中不同的物件,而 ==Equals 則會回傳 True ,因為編譯器產生的等式會比較屬性值。

相同的編譯器產生方式也適用於 record struct 以下類型:

var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

Console.WriteLine(dim1 == dim2);      // => True
Console.WriteLine(dim1.Equals(dim2)); // => True

記錄型別會產生整個等號集來對應自己的型別。 record classrecord struct 類型都會覆寫 EqualsGetHashCode。 它們也會產生 ==!= 運算子,以及適用於記錄類型的具型別 Equals 方法。 與平面 struct不同,因此 a record struct 自動支撐 ==!= 。 欲了解更多關於記錄類型及其等同語意的資訊,請參閱記錄。

具有參考類型成員的紀錄

記錄的相等性會使用各成員自身的相等性語意。 每個屬性或欄位都會用自己的 Equals 方法比較。 對於大多數純量值,如 intstringDateTime,此方法比較記錄成員的值。 這種微妙之處出現在常見的可變集合,如 List<T>T[]:這些類型會透過參考來比較,因此兩個包含 相同內容不同清單物件 的記錄實例,在合成記錄等式 中並不 被視為相等。

var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True

playlist1playlist2 是兩個獨立 List<string> 的實例。 即使兩者內容相符,Equals 仍會傳回 false

當你需要讓記錄相等性反映集合內容時,你有幾個選項:

  • 在 record 上實作 IEquatable<T>,並覆寫 Equals,讓集合成員使用 Enumerable.SequenceEqual
  • 使用值相等的集合類型 ——例如自訂 IEqualityComparer<T> 型態或其類型 Equals 會比較元素。
  • 以身份為核心設計:如果紀錄代表一個實體而非純值,則其集合成員的參考平等可能是刻意的。

Important

在 C# 中,如今手動實作相等性已相當罕見。 紀錄會自動處理價值平等的常見情境。 如果你需要手動實作相等性——例如,因為你的型別必須衍生自非記錄的基底類別——請參閱語言參考中的「當型別無法是記錄時,自行實作相等性」。

另請參閱