Как определить равенство значений для класса или структуры (руководство по программированию на C#)
Записи автоматически реализуют равенство значений. Попробуйте определить record
вместо class
, когда ваш тип моделирует данные и должен реализовать равенство значений.
При определении класса или структуры необходимо решить, имеет ли смысл создавать пользовательское определение равенства значений (или эквивалентности) для этого типа. Обычно равенство значений реализуется, если объекты этого типа будут добавляться в коллекции или если они предназначены в первую очередь для хранения набора полей или свойств. В основу определения равенства значений можно положить сравнение всех полей и свойств в типе или только их части.
В любом случае, реализация как для классов, так и для структур, должна соответствовать следующим пяти принципам обеспечения эквивалентности (в следующих правилах предполагается, что 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.Equals(y)
вызывает исключение, еслиx
имеет значение NULL. Это нарушает правила 1 или 2 в зависимости от аргумента дляEquals
.
Любая определяемая вами структура имеет заданную по умолчанию реализацию равенства значений, которая наследуется от переопределения System.ValueType метода Object.Equals(Object). Эта реализация использует отражение для проверки всех полей и свойств в типе. Хотя эта реализация возвращает верный результат, она отличается невысокой скоростью по сравнению с пользовательской реализацией, которую можно написать специально для конкретного типа.
Детали реализации равенства значений для классов и структур различаются. Однако для реализации равенства как для классов, так и для структур, необходимо выполнить одни и те же базовые действия.
Переопределите виртуальный метод Object.Equals(Object). В большинстве случаев пользовательская реализация
bool Equals( object obj )
должна вызывать относящийся к конкретному типу методEquals
, который является реализацией интерфейса System.IEquatable<T>. (См. шаг 2.)Реализуйте интерфейс System.IEquatable<T>, предоставив метод
Equals
для конкретного типа. Именно на этом этапе происходит фактическое сравнение значений. Например, функцию равенства можно определить путем сравнения только одного из двух полей в типе. Не создавайте исключения вEquals
. Для классов, которые связаны наследованием, должно соблюдаться следующее.этот метод должен проверять только те поля, которые объявлены в классе. Он должен вызывать метод
base.Equals
для проверки полей в базовом классе. (Не вызывайтеbase.Equals
, если тип наследует напрямую от Object, так как реализация Object для Object.Equals(Object) выполняет проверку равенства ссылок.)Две переменные должны считаться равными, только если совпадают их типы времени выполнения. Также убедитесь, что реализация
IEquatable
методаEquals
для типов времени выполнения используется, если типы времени выполнения и времени компиляции переменной отличаются. Одна из стратегий обеспечения правильного сравнения типов времени выполнения — реализацияIEquatable
только в классахsealed
. Дополнительные сведения см. в примере класса ниже в этой статье.
Рекомендуется (хотя это и не обязательно) перегрузить операторы == и !=.
Переопределите Object.GetHashCode таким образом, чтобы два объекта с равными значениями создавали одинаковый хэш-код.
Необязательно. Для поддержки определений для "больше" или "меньше", реализуйте IComparable<T> интерфейс для вашего типа, а также перегрузите <операторы = и >= .
Примечание.
Записи можно использовать для получения семантики равенства значений без ненужных стандартных кодов.
Пример класса
В следующем примере показана реализация равенства значений в классе (ссылочный тип).
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
Этот код сообщает, что p1
и p2
равны несмотря на разницу в значениях z
. Эта разница игнорируется, так как компилятор выбирает реализацию TwoDPoint
для IEquatable
на основе типа времени компиляции.
Встроенный механизм обеспечения равенства значений типов 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
метод в структуре, цель состоит в том, чтобы обеспечить более эффективное средство проверки равенства значений и при необходимости на основе сравнения на некоторых подмножествах полей или свойств структуры.
Операторы == и != нельзя применять к структурам, если только они явным образом не перегружены для структуры.