クラスまたは構造体の値の等価性を定義する方法 (C# プログラミング ガイド)
レコードを使用すると、値の等価性が自動的に実装されます。 ご利用の型によって、データをモデル化する場合や値の同等性を実装する必要がある場合は、class
ではなく record
を定義することを検討してください。
クラスまたは構造体を定義する場合は、型に値の等価性 (同値) のカスタム定義を作成することが有用かどうかを判断します。 通常、値の等価性を実装するのは、その型のオブジェクトをコレクションに追加する予定である場合、またはそれらの主な目的が一連のフィールドまたはプロパティを格納することである場合です。 値の等価性は、型のすべてのフィールドおよびプロパティの比較に基づいて定義できます。また、サブセットに基づいて定義することもできます。
いずれの場合も、クラスおよび構造体の両方について、等価性を保証する 5 つの条件に従って実装する必要があります (次のルールの場合、x
、y
、z
が null ではないと想定しています)。
再帰プロパティ:
x.Equals(x)
からtrue
が返されます。対称プロパティ:
x.Equals(y)
から、y.Equals(x)
と同じ値が返されます。推移性プロパティ:
(x.Equals(y) && y.Equals(z))
からtrue
が返される場合は、x.Equals(z)
からはtrue
が返されます。x.Equals(y)
が連続して呼び出された場合は、x と y によって参照されるオブジェクトが変更されていない限り、同じ値を返します。非 null 値は null と等しくありません。 そのため、
x
が null である場合は、x.Equals(y)
から例外がスローされます。 それにより、Equals
の引数に基づき、ルール 1 または 2 が破られます。
構造体を定義すると、Object.Equals(Object) メソッドの System.ValueType オーバーライドから継承された値の等価性が既定で実装されます。 この実装では、リフレクションを使用して、型のフィールドとプロパティをすべて調べます。 この実装によって正しい結果が生成されますが、その型専用に記述したカスタム実装と比較すると、処理にかなり時間がかかります。
値の等価性に関する実装の詳細は、クラスと構造体で異なりますが、 等価性を実装するための基本的な手順については、両方とも同じです。
仮想 Object.Equals(Object) メソッドをオーバーライドします。 ほとんどの場合、
bool Equals( object obj )
の実装には、System.IEquatable<T> インターフェイスの実装である型固有のEquals
メソッドを呼び出すだけで済みます (手順 2 を参照)。型固有の
Equals
メソッドを指定して、System.IEquatable<T> インターフェイスを実装します。 ここで実際の等価性の比較を実行します。 たとえば、型のフィールドを 1 ~ 2 個だけ比較することで等価性を定義できます。Equals
から例外をスローしないでください。 継承によって関連付けられているクラスの場合:このメソッドはクラスで宣言されているフィールドのみを調べます。 基底クラスに含まれるフィールドを調べるには、
base.Equals
を呼び出す必要があります (型が Object から直接継承されている場合は、base.Equals
を呼び出さないでください。Object.Equals(Object) の Object 実装によって、参照の等価性チェックが実行されるためです)。比較対象の変数の実行時の型が同じである場合にのみ、2 つの変数は等しいと見なされます。 また、変数の実行時とコンパイル時の型が異なる場合は、実行時の型の
Equals
メソッドのIEquatable
実装を必ず使用してください。 実行時の型が常に正しく比較されるようにするための方法の 1 つは、sealed
クラスにのみIEquatable
を実装することです。 詳細については、この記事で後述する「クラスの例」を参照してください。
値の等価性を持つ 2 つのオブジェクトによって同じハッシュ コードが生成されるように、Object.GetHashCode をオーバーライドします。
省略可能: "大なり" または "小なり" の定義をサポートするには、型に対して IComparable<T> インターフェイスを実装したうえで、<= 演算子および >= 演算子をオーバーロードします。
Note
レコードを使用して、不要な定型コードなしで値の等価性セマンティクスを取得できます。
クラスの例
次の例は、クラス (参照型) で値の等価性を実装する方法を示しています。
namespace ValueEqualityClass;
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);
public bool Equals(TwoDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);
public bool Equals(ThreeDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);
pointE = new TwoDPoint(3, 4);
Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
クラス (参照型) の場合、両方の Object.Equals(Object) メソッドの既定の実装で、参照の等価性の比較は実行されますが、値の等価性のチェックは実行されません。 実装が仮想メソッドをオーバーライドする場合、その目的は、仮想メソッドに値の等価性のセマンティクスを提供することです。
==
演算子と !=
演算子は、オーバーロードされなくてもクラスで使用できます。 ただし、既定の動作として参照の等価性のチェックが実行されます。 クラスで Equals
メソッドをオーバーロードする場合は、==
演算子と !=
演算子をオーバーロードすることをお勧めしますが、必須ではありません。
重要
前のコード例では、すべての継承シナリオが期待どおりに処理されるとは限りません。 次のコードがあるとします。
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
このコードを実行すると、z
の値が異なるにもかかわらず、p1
は p2
に等しいと報告されます。 この違いが無視される理由は、コンパイルではコンパイル時の型に基づいて IEquatable
の TwoDPoint
実装のみが選択されることにあります。
record
型の組み込み値が等価である場合、このようなシナリオは正しく処理されます。 TwoDPoint
と ThreeDPoint
の型が record
であるならば、p1.Equals(p2)
の結果は False
になります。 詳細については、record
型の継承階層の等価性に関するページを参照してください。
構造体の例
次の例は、構造体 (値型) で値の等価性を実装する方法を示しています。
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}
public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);
public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
// True:
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
// True:
Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
// False:
Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
// False:
Console.WriteLine("(pointA == null) = {0}", pointA == null);
// True:
Console.WriteLine("(pointA != null) = {0}", pointA != null);
// False:
Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
// CS0019:
// Console.WriteLine("pointA == i = {0}", pointA == i);
// Compare unboxed to boxed.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
// True:
Console.WriteLine("pointC == pointD = {0}", pointC == pointD);
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);
pointD = temp;
// True:
Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
構造体の場合、Object.Equals(Object) (System.ValueType でオーバーライドされるバージョン) の既定の実装で、リフレクションを使用して値の等価性のチェックが実行され、型のすべてのフィールドの値が比較されます。 実装で構造体の仮想 Equals
メソッドをオーバーライドする場合、その目的は、値の等価性のチェックをより効率的に実行することと、オプションで、構造体のフィールドまたはプロパティの一部のサブセットに基づいて比較を行うことです。
== および != 演算子を使用して構造体で操作することは、その構造体によってそれらの演算子が明示的にオーバーロードされない限り、不可能です。
関連項目
.NET