C#の等価比較

Tip

この記事は、少なくとも 1 つのプログラミング言語を既に知っており、C# を学習している開発者向けの 基礎 セクションの一部です。 プログラミングを初めて使用する場合は、最初に「 はじめ に」チュートリアルから始めてください。

別の言語から来ていますか? Javaでは、オブジェクトに==し、オブジェクトに対する JavaScript ===は、コンテンツではなく ID をテストします。 C# クラスは、既定で同じように動作します。 Pythonでは、C# レコードの比較と同様に、==は既定で__eq__を呼び出し、コンテンツをテストします。 C# 構造体は、Equalsを呼び出すときにも値によって比較されます。

C# では、2 種類の等価性が区別されます。 値の等価性 は、データが一致したときに 2 つのインスタンスが等しいことを意味します。 参照等価は 、2 つの変数がメモリ内の同じオブジェクトを指している場合にのみ等しいことを意味します。 この条件は ID とも呼ばれます。 値型は通常、データを比較し、参照型は通常、ID を比較します。 型の作成者はこれらの既定値を変更できますが、そのメンタル モデルでは、同じように見える 2 つのオブジェクトが等しいと見なされない、または 1 つの変数を介した変更によって、別の変数に表示される内容が自動的に変更されるという微妙なバグを防ぐことができます。

値型、参照型、等価性の既定値

C# のすべての型は、 値型 または 参照型です値型は、そのデータを変数に直接保持します。 参照型は、オブジェクトへの参照を保持します。 参照型変数を別の変数に割り当てると、両方の変数が同じオブジェクトを参照します。 値型と参照型の詳細については、「 型システムの概要」を参照してください。

通常、既定の等値動作は型の種類に従います。

  • 組み込みの数値型と 列挙型 は値型です。 数値が一致すると、2 つの int 変数が等しくなります。
  • 構造体は 値型です。 プレーン struct では、 Equalsを呼び出すときに値の等価性が使用されます。
  • タプルは 値型です。 すべての要素値が一致すると、2 つのタプルが等しくなります。
  • クラス は参照型です。 プレーン クラスでは参照の等価性が使用されるため、 ==Equals 、2 つの変数が同じオブジェクトを指しているかどうかをテストします。

クラスは参照等価を使用します。 同じデータを持つ 2 つの個別のオブジェクトは等しくないが、同じオブジェクトを参照する 2 つの変数は等しい:

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を通じて値の等価性を示します。 フィールドが一致すると、2 つの構造体インスタンスが等しくなります。

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

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

プレーン構造体では、定義済みの == 演算子は取得されません。 プレーン構造体に p1 == p2 を記述すると、構造体が独自の operator ==を宣言する場合にのみコンパイルされます。 構造体の演算子の比較が必要な場合は、 ==!= をペアとして定義し、 EqualsGetHashCodeとの一貫性を保ちます。

タプルも値型です。 すべての要素値が一致すると、2 つのタプルが等しくなります。 名前付きタプル内の要素名はコンパイル時の利便性であり、比較時には考慮されません。 位置と値のみが重要です。

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

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

タプルの構文と分解の詳細については、「 タプルと分解」を参照してください。

Object.ReferenceEqualsを使用して ID を直接テストする

ReferenceEquals 型が Equals をオーバーライドする方法や ==オーバーロードする方法に関係なく、常に ID をテストします。 2 つの変数がまったく同じオブジェクトを指しているかどうかを確認する必要がある場合は、ID 診断として使用します。

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 は、ID ではなく文字列の内容を比較します。

  • ==: 等値演算子。 ほとんどの型では、この演算子をプライマリ等値チェックとして使用します。 その動作は、型に組み込み演算子とユーザー定義 == 演算子のどちらが含まれているかによって異なります。
  • !=: 不等号演算子。 型でユーザー定義の == 演算子を定義する場合は、 !=も定義する必要があります。
  • Equals: すべての型によって継承される仮想メソッド。 これをオーバーライドして、型の等価セマンティクスを変更できます。
  • GetHashCode: ハッシュ ベースのコレクションで使用される仮想メソッド。 2 つの値が等しい場合は、ハッシュ コードも等しい必要があります。
  • ReferenceEquals: 常に ID をテストする静的メソッド。

値の等価性にはレコードを使用する

record 修飾子を使用すると、型がレコードになり得る場合に、データ中心の型に値の等価性を持たせることができます。 コンパイラは、宣言されたすべてのプロパティ値を比較する EqualsGetHashCode、および ==/!= メンバーを生成します。

record classはまだ参照型ですが、ID ではなく値を比較します。

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 がメモリ内の異なるオブジェクトであることを確認しますが、コンパイラによって生成された等値はプロパティ値を比較するため、 ==EqualsTrue を返します。

同じコンパイラ生成が 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 class型とrecord struct型の両方が、EqualsGetHashCodeをオーバーライドします。 また、 == 演算子と != 演算子に加えて、レコード型の型指定された Equals メソッドも生成されます。 プレーンな structとは異なり、 record struct は自動的に ==!= をサポートします。 レコードの種類とその等価セマンティクスの詳細については、「 レコード」を参照してください。

参照型メンバーを持つレコード

レコードの等価性では、各メンバー自身の等価性セマンティクスが使用されます。 各プロパティまたはフィールドは、独自の Equals メソッドを使用して比較されます。 intstringDateTimeなど、ほとんどのスカラー値では、レコード メンバーの値が比較されます。 この微妙な点は、 List<T>T[]などの一般的な変更可能なコレクションで発生します。これらの型は参照によって比較されるため、 同じコンテンツを持つ異なるリスト オブジェクト を含む 2 つのレコード インスタンスは、合成されたレコードの等価性によって等しいとは見な されません

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> インスタンスです。 内容が一致していても、 Equalsfalseを返します。

コレクション の内容を反映するためにレコードの等価性が必要な場合は、いくつかのオプションがあります。

  • レコードでIEquatable<T>を実装し、コレクション メンバーにEqualsを使用するようにEnumerable.SequenceEqualをオーバーライドします。
  • 値が等しいコレクション型 (カスタム IEqualityComparer<T> 、独自の Equals が要素を比較する型など) を使用します。
  • ID に関する設計: レコードが純粋な値ではなくエンティティを表している場合、コレクション メンバーの参照等価性が意図的である可能性があります。

Important

C# では、現在、等価性の手動実装はまれです。 レコードは、値の等価性に関する一般的なシナリオを自動的に処理します。 たとえば、型が非レコード基底クラスから派生する必要があるために、等価性を手動で実装する必要がある場合は、「型を言語参照の レコードにできない場合に等値を自分で実装 する」を参照してください。

こちらも参照ください