等値演算子 - 2 つのオブジェクトが等しいかどうかをテストします

== (等価)!= (非等値) 演算子は、そのオペランドが等しいかどうを確認します。 内容が等しい場合、値の型は等しくなります。 2 つの変数が同じストレージを参照している場合、参照型は等しくなります。

C# 言語リファレンスには、C# 言語の最新リリース バージョンが記載されています。 また、今後の言語リリースのパブリック プレビューの機能に関する初期ドキュメントも含まれています。

このドキュメントでは、言語の最後の 3 つのバージョンまたは現在のパブリック プレビューで最初に導入された機能を特定します。

ヒント

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.NaNまたはSingle.NaN) でない場合、演算の結果はfalse。 この条件は、NaN値が、doubleを含む他のfloat (またはNaN) 値より大きい、より小さい、または等しくないことを意味します。 詳細およびサンプルについては、Double.NaN または Single.NaN の参照記事をご覧ください。

同じ列挙型の 2 つのオペランドは、基になる整数型の対応する値が等しい場合は等しくなります。

既定ではユーザー定義 struct 型は == 演算子をサポートしていません。 == 演算子をサポートするには、ユーザー定義 struct でそれをオーバーロードする必要があります。

C# タプルには、==演算子と!=演算子が組み込まれています。 詳細については、タプル型に関する記事のタプルの等価性に関するセクションを参照してください。

参照型の等価性

既定では、レコードを除く参照型オペランドは、同じオブジェクトを参照する場合は等しくなります。

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

前の例に示すように、ユーザー定義参照型は既定で == 演算子をサポートしています。 ただし、参照型は == 演算子をオーバーロードできます。 参照型が == 演算子をオーバーロードする場合、その型の 2 つの参照が同じオブジェクトを参照しているかどうかを調べるには Object.ReferenceEquals メソッドを使用します。

レコードの型の等価性

レコード型では、既定で値の等価性セマンティクスを提供する == 演算子と != 演算子がサポートされています。 つまり、両方がすべてのフィールドの null または対応する値であり、自動的に実装されるプロパティが等しい場合、2 つのレコード オペランドが等しくなります。

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

前の例に示すように、参照型メンバーの等価性は、その特定の等値実装を使用して比較されます。

文字列の等価性

2 つの string オペランドは、その両方が null であるか、両方の文字列インスタンスの長さが同じで、それぞれの文字列の位置に同じ文字が含まれている場合に等しくなります。

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

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

文字列の等価比較は、大文字と小文字が区別される序数比較です。 文字列の比較に関する詳細については、「C# で文字列を比較する方法」を参照してください。

デリゲートの等価性

同じランタイム型の 2 つの デリゲート オペランドは、両方が 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

Important

呼び出しリスト内の等しい項目には、受信側を含め、呼び出し内のすべての固定パラメーターが含まれます。 受信側は、エントリが呼び出されたときに 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# 言語仕様の「Delegate equality operators (デリゲートの等値演算子)」セクションをご覧ください。

次の例に示すように、意味的に同じ ラムダ式の 評価に由来するデリゲートは等しくない。

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

等しくない演算子 !=

等しくない演算子 != は、オペランドが等しくない場合は true を返し、それ以外の場合は false 返します。 組み込み型のオペランドの場合、式 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 を優先します。

Important

可能な限り 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);
}

実装では、3 つの必須メンバーが提供されます。コア比較として Equals(T?) 、オブジェクト レベルの呼び出しの override Equals(object?) 、ハッシュ ベースのコレクションが正しく動作するように override GetHashCode()HashCode.Combine は、 Equalsで使用されるのと同じ値から 1 つのハッシュを構築するライブラリ ヘルパーです。 IEquatable<T> (Equals(T?) オーバーロード) の実装は省略可能ですが、呼び出し元が既に具象型を持っている場合はボックス化を回避できます。

また、 ==!=を定義する場合、言語にはペアとして言語が必要です。 警告 CS0660CS0661 では、4 つのメンバーすべてに一貫性を保つように注意してください。

上記の 3 つのメンバーを配置すると、Equalsは値の等価性を反映しますが、==演算子がまだ宣言されていないため、==は ID をテストします。

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 が null 以外であると仮定します)。

  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. null 動作: x.Equals(null)falseを返します。null 以外のxで呼び出されたときにx.Equals(y)はスローしないでください。

封印されていないクラス階層の値の等価性は、対称規則と推移的ルールを満たすために、シールされたクラスよりも注意が必要です。 危険は、 IEquatable<T>.Equals(T? other) ディスパッチは、ランタイム型ではなく、 変数の宣言された型 (変数宣言で記述された型) に従うということです。 Shapeが非virtualEquals(Shape? other)を宣言する場合、実行時にCircleを保持するShapeとして型指定された変数はShape.Equalsを呼び出し、Circle固有のフィールドをサイレントに無視します。 Shape変数を介してアクセスすると、半径が異なる 2 つのCircle オブジェクトが等しいと比較できます。

正しいパターンには、型指定された Equals メソッドを virtual し、各派生クラスが比較を拡張できるようにし、基底クラスの実装に GetType() == other.GetType() ガードを追加して、異なるランタイム型のオブジェクトが等しいとは見なされないようにする、2 つの連携する要件が必要です。

基本クラスの実装

// 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 型指定された Equals: 各派生クラスはこのメソッドをオーバーライドして、独自のフィールドとの比較を強化します。
  • GetType() ガード: GetType() == other.GetType() は、 Circle が同じ色の Shape に等しくならないようにし、異なる派生型のオブジェクトが互いに等しくなるようにします。
  • GetHashCodeにはGetType()が含まれます。2 つのオブジェクトはランタイム型が一致する場合にのみ等しいため、GetHashCodeはランタイム型とデータ フィールドをハッシュする必要があります。 ここで GetType() を省略すると、 Dictionary<TKey,TValue>HashSet<T>で正しくない動作が発生します。
  • == デリゲートを Equals: 演算子とメソッドの等価性の一貫性を維持します。

派生クラスの実装

フィールドを追加する派生クラスは、型指定された Equalsをオーバーライドし、独自の型にキャストし、 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() ガードを適用し、共有フィールドをチェックします。 引数が他の派生型のShapeである場合、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>パターンは、シールされたクラスに対して正しく、完全です。

演算子のオーバーロード可/不可

ユーザー定義型の演算子と==演算子を!=できます。 これら 2 つの演算子のいずれかをオーバーロードする場合は、もう一方の演算子もオーバーロードする必要があります。

レコード型の == 演算子と != 演算子を明示的にオーバーロードすることはできません。 レコード型==!=演算子とT演算子の動作を変更するには、次のシグネチャを使用して IEquatable<T>.Equals メソッドを実装します。

public virtual bool Equals(T? other);

C# 言語仕様

詳細については、C# 言語仕様に関するページの「関係演算子と型検査演算子」のセクションを参照してください。

レコード型の等価性の詳細については、C# 言語仕様等値メンバーのセクションを参照してください。

関連項目