相等運算子 - 測試兩個物件相等或不相等

== (相等)!= (不等) 運算子可檢查其運算元是否相等。 實值型別在內容相等時相等。 兩個變數參考相同的儲存體時,參考型別會相等。

C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。

文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。

小提示

欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。

當您針對is進行測試時,可以使用==模式比對運算符做為測試的替代方案。 運算子 is 會針對所有值和參考型別使用預設相等語意。

等號比較運算子 ==

等號比較運算子 == 會在其運算元相等時傳回 true,否則傳回 false

實值型別相等

若他們的值相等時,內建實值型別的運算元就會相等:

int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b);  // output: True

char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2);  // output: False
Console.WriteLine(c1 == char.ToLower(c2));  // output: True

注意

==對於 <><=>= 運算子,如果任一作數不是數位 (Double.NaNSingle.NaN),則運算的結果為 false。 該條件表示該 NaN 值不高於、不低於或等於任何其他 double (或 float)值,包括 NaN。 如需詳細資訊和範例,請參閱 Double.NaNSingle.NaN 參考文章。

若基礎整數型別的對應值相等時,相同列舉類型的兩個運算元就會相等。

使用者定義結構型別預設不支援 == 運算子。 若要支援 == 運算子,使用者定義結構必須多載它。

C# Tuple 具有 和 == 運算子的內建支援!=。 如需詳細資訊,請參閱 Tuple 型別一文中的 Tuple 相等一節。

參考型別相等

根據預設,參考類型作數,不包括記錄,如果它們參考相同的物件,則相等:

public class ReferenceTypesEquality
{
    public class MyClass
    {
        private int id;

        public MyClass(int id) => this.id = id;
    }

    public static void Main()
    {
        var a = new MyClass(1);
        var b = new MyClass(1);
        var c = a;
        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a == c);  // output: True
    }
}

如前例所示,使用者自訂的參考類型預設支援該 == 運算元。 不過,參考型別可以多載 == 運算子。 若參考型別多載 == 運算子,請使用 Object.ReferenceEquals 方法檢查兩個該類型的參考是否參考相同的物件。

記錄型別相等

記錄型別預設支援 ==!= 運算子,以提供值相等語意。 也就是說,當兩個記錄操作數都是 null 或所有欄位的對應值,且自動實作的屬性相等時,兩個記錄操作數都會相等。

public class RecordTypesEquality
{
    public record Point(int X, int Y, string Name);
    public record TaggedNumber(int Number, List<string> Tags);

    public static void Main()
    {
        var p1 = new Point(2, 3, "A");
        var p2 = new Point(1, 3, "B");
        var p3 = new Point(2, 3, "A");

        Console.WriteLine(p1 == p2);  // output: False
        Console.WriteLine(p1 == p3);  // output: True

        var n1 = new TaggedNumber(2, new List<string>() { "A" });
        var n2 = new TaggedNumber(2, new List<string>() { "A" });
        Console.WriteLine(n1 == n2);  // output: False
    }
}

如前例所示,參考型成員的相等性是透過其特定的等式實作來比較的。

字串相等

當兩個 string 運算元皆為 null,或兩個 string 執行個體的長度相同且在各字元位置擁有完全相同的字元,兩者就會相等:

string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower());  // output: True

string s3 = "Hello!";
Console.WriteLine(s1 == s3);  // output: False

字串相等比較是區分大小寫的序數比較。 如需有關字串比較的詳細資訊,請參閱如何在 C# 中比較字串

委派等號

兩個null相同執行時間類型時,或其調用清單長度相同且每個位置的項目相等,則兩者相等:

Action a = () => Console.WriteLine("a");

Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
Console.WriteLine(b == c);  // output: True

這很重要

調用清單中的相等專案包含調用中的所有固定參數,包括接收者。 接收者是叫用專案時所表示 this 之 對象的實例。

var o1 = new object();
var o2 = new object();
var d1 = o1.ToString;
var d2 = o2.ToString;
Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
Console.WriteLine(d1 == d2);  // output: False (different receivers)

如需詳細資訊,請參閱 C# 語言規格委派相等運算子一節。

來自語意相同 lambda 表達 式的代理並不相等,以下範例顯示:

Action a = () => Console.WriteLine("a");
Action b = () => Console.WriteLine("a");

Console.WriteLine(a == b);  // output: False
Console.WriteLine(a + b == a + b);  // output: True
Console.WriteLine(b + a == a + b);  // output: False

不等比較運算符 !=

若運算元不相等!=,則不等式運算truefalse子返回,否則。 對於內建類型的運算元,運算式 x != y 會產生與運算式 !(x == y) 相同的結果。 如需有關型別等號的詳細資訊,請參閱等號比較運算子一節。

以下範例示範如何使用運算 != 子:

int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b);  // output: True

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2);  // output: False

object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2);  // output: True

階級階層中的平等

紀錄能正確處理繼承,無需人工操作。 編譯器產生的等號會檢查執行時型態及所有宣告屬性,因此自動滿足對稱性與傳遞性要求。 當目標是價值平等時,優先 record 選擇手動未封閉的階層。

這很重要

盡可能使用 record ——編譯器會為你產生所有必要的等式成員。 只有當你的型別必須從非記錄類別衍生出,或有其他限制阻止 record時,才需要手動實作。

當某個類型無法成為紀錄時,自己實施平等

以下是一個無法成為紀錄的值型別的最小手動實作:

class Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
    {
        R = r;
        G = g;
        B = b;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }

    public bool Equals(Color? other) =>
        other is not null && R == other.R && G == other.G && B == other.B;

    public override bool Equals(object? obj) => obj is Color other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(R, G, B);
}

實作提供三個必要成員: Equals(T?) 作為核心比較,用於 override Equals(object?) 物件層級呼叫,因此 override GetHashCode() 基於雜湊的集合能正常運作。 HashCode.Combine 是一個函式庫輔助工具,從與 相同的 Equals值建立一個雜湊值。 實作 IEquatable<T>Equals(T?) 超載)是可選的,但可避免在呼叫者已有具體型別時出現盒裝。

當你同時定義 ==!=時,語言要求它們成對;警告 CS0660CS0661 提醒你保持四個成員一致。

當上述三個成員就位時, Equals 反映出值相等,但仍 == 需測試同一性,因為尚未 == 宣告任何運算子:

var red1 = new Color(255, 0, 0);
var red2 = new Color(255, 0, 0);

Console.WriteLine(red1.Equals(red2)); // => True
Console.WriteLine(red1 == red2);      // => False  (no == overload; identity check)

正確的實作還必須滿足 等價契約 (假設 xyz 且 非零):

  1. 反射x.Equals(x)返回true
  2. 對稱x.Equals(y) 回傳與 y.Equals(x)相同的值。
  3. 傳遞性:若 x.Equals(y)y.Equals(z)true為 ,則 x.Equals(z) 必為 true
  4. 一致性:只要兩個物件不變,連續呼叫回 x.Equals(y) 傳相同值。
  5. 空行為x.Equals(null) 回傳 false; x.Equals(y) 在非空 x時不得拋出。

在未封閉類別階層中,價值平等比密封類別更需謹慎,以滿足對稱與傳遞規則。 危險在於 IEquatable<T>.Equals(T? other) dispatch 會遵循該變數 宣告的類型 (變數宣告中寫的類型),而非執行時型別。 若Shape宣告 non-Equals(Shape? other)virtual,則在Shape執行時持有 的Circle變數會Shape.Equals呼叫 —— ,且Circle忽略 -特定的欄位。 Circle兩個半徑不同的物體在透過變Shape數存取時可以視為同等。

正確的模式需要兩個協作需求:將型別化 Equals 的方法 virtual 設計成每個衍生類別都能擴展比較,並在基底類別實作中加入 GetType() == other.GetType() 守衛,使不同執行時類型的物件永遠不會被視為相等。

基底類別實作

// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}

關鍵點:

  • virtual typed Equals:每個衍生類別都會覆寫此方法,以增強其自身欄位的比較。
  • GetType() 守護GetType() == other.GetType() 防止 A Circle 與同色的 A Shape 相等,並防止不同派生類型的物件彼此相等。
  • GetHashCode 包含 GetType():由於兩個物件只有在執行時類型相符時才相等,必須 GetHashCode 同時對執行時類型及資料欄位進行雜湊。 若在此GetType()省略,會導致 和 HashSet<T>Dictionary<TKey,TValue>行為錯誤。
  • == 代表 : Equals保持運算子與方法的相等性一致。

衍生類別實作

一個派生類別加入欄位,會覆寫型別的 Equals,將 cast 為自身型別,呼叫 base.Equals,然後比較自己的欄位:

class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}

base.Equals(c) 執行 GetType() 守衛並檢查共用欄位。 當參數是任何其他導出型態 a Shape 時,cast via other is Circle c 會很快失敗。

透過基底型變數的使用方式

Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)

密封類別比較簡單

你無法子類別 sealed 類別,所以編譯時和執行時型別總是一致。 你不需要 GetType() 警衛或 virtual 調度。 IEquatable<T>「當某類型無法成為紀錄時自行實現等號」中所示的模式,對於密封類別來說是正確且完整的。

運算子是否可多載

你可以用使用者定義的類型 超載==!= 運算子。 如果你讓其中一位超載,另一位也必須超載。

你不能明確在記錄類型中超載 ==!= 運算子。 若要改變記錄型==別中 和 != 運算子的行為T,請實作IEquatable<T>.Equals以下簽名的方法:

public virtual bool Equals(T? other);

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格關係及類型測試運算子一節。

欲了解更多關於記錄類型平等的資訊,請參閱 C# 語言規範中的平等成員部分。

另請參閱