Заметка
Доступ к этой странице требует авторизации. Вы можете попробовать войти в систему или изменить каталог.
Доступ к этой странице требует авторизации. Вы можете попробовать сменить директорию.
Подсказка
Сначала рассмотрите возможность использования записей. Записи автоматически реализуют равенство значений с минимальным кодом, что делает их рекомендуемым подходом для большинства типов, ориентированных на данные. Если вам нужна логика равенства пользовательских значений или не удается использовать записи, перейдите к приведенным ниже инструкциям по реализации вручную.
При определении класса или структуры необходимо решить, имеет ли смысл создавать пользовательское определение равенства значений (или эквивалентности) для этого типа. Обычно равенство значений реализуется, если объекты этого типа будут добавляться в коллекции или если они предназначены в первую очередь для хранения набора полей или свойств. В основу определения равенства значений можно положить сравнение всех полей и свойств в типе или только их части.
В любом случае, реализация как для классов, так и для структур, должна соответствовать следующим пяти принципам обеспечения эквивалентности (в следующих правилах предполагается, что 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). Это обеспечивает полиморфное поведение равенства, что позволяет объектам правильно сравниваться при обработке ссылок
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. Дополнительные сведения см. в примере класса ниже в этой статье.
Рекомендуется (хотя это и не обязательно) перегрузить операторы == и !=. Это обеспечивает единый и понятный синтаксис для сравнения равенства, соответствующий ожиданиям пользователей от встроенных типов данных. Это гарантирует, что
obj1 == obj2иobj1.Equals(obj2)ведет себя так же.Переопределите Object.GetHashCode таким образом, чтобы два объекта с равными значениями создавали одинаковый хэш-код. Это необходимо для правильного поведения в хэш-коллекциях, таких как
Dictionary<TKey,TValue>иHashSet<T>. Объекты, равные, должны иметь одинаковые хэш-коды, или эти коллекции не будут работать правильно.Необязательно. Для поддержки определений для "больше" или "меньше", реализуйте IComparable<T> интерфейс для вашего типа, а также перегрузите <операторы = и >= . Это позволяет выполнять сортировку и обеспечивает полное упорядочение для типа, полезное при добавлении объектов в отсортированные коллекции или при сортировке массивов или списков.
Пример записи
В следующем примере показано, как записи автоматически реализуют равенство значений с минимальным кодом. Первая запись — это простой тип записи TwoDPoint , который автоматически реализует равенство значений. Вторая запись ThreeDPoint демонстрирует, что записи могут быть производными от других записей и по-прежнему поддерживать правильное поведение равенства значений:
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
Записи предоставляют несколько преимуществ для равенства значений:
-
Автоматическая реализация: записи автоматически реализуют System.IEquatable<T> и переопределяют Object.Equals, Object.GetHashCode, и операторы
==/!=. -
Правильное поведение наследования. Записи реализуются
IEquatable<T>с помощью виртуальных методов, которые проверяют тип среды выполнения обоих операндов, обеспечивая правильное поведение в иерархиях наследования и полиморфных сценариях. - Неизменяемость по умолчанию: записи поощряют неизменяемый дизайн, который хорошо работает с семантикой равенства значений.
- Краткий синтаксис: позиционные параметры предоставляют компактный способ определения типов данных.
- Улучшенная производительность. Реализация равенства, созданная компилятором, оптимизирована и не использует отражение, например реализацию структуры по умолчанию.
Используйте записи, когда основной целью является хранение данных и требуется семантика равенства значений.
Записи с элементами, используюющими равенство ссылок
Если записи содержат элементы, использующие равенство ссылок, автоматическое поведение равенства значений записей не работает должным образом. Это относится к коллекциям, таким как System.Collections.Generic.List<T>массивы и другие ссылочные типы, которые не реализуют равенство на основе значений (с заметным исключением, которое реализует равенство значений System.String).
Внимание
Хотя записи обеспечивают отличное равенство значений для основных типов данных, они не решают равенство значений для членов, использующих эталонное равенство. Если запись содержит System.Collections.Generic.List<T>System.Arrayили другие ссылочные типы, которые не реализуют равенство значений, два экземпляра записей с идентичным содержимым в этих элементах по-прежнему не будут равны, так как члены используют равенство ссылок.
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
Это связано с тем, что записи используют Object.Equals метод каждого элемента, а типы коллекций обычно используют равенство ссылок, а не сравнивают их содержимое.
Ниже показана проблема:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Вот как это происходит при запуске кода:
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
Решения для записей с элементами ссылочного равенства
Настраиваемая System.IEquatable<T> реализация: замените равенство, созданное компилятором, с помощью закодированной вручную версии, которая обеспечивает сравнение на основе содержимого для элементов ссылочного равенства. Для коллекций реализуйте сравнение элементов по элементам с помощью Enumerable.SequenceEqual или аналогичных методов.
Используйте типы значений, где это возможно: рассмотрите возможность представления данных с помощью типов значений или неизменяемых структур, которые, естественно System.Numerics.Vector<T> , поддерживают равенство значений, например или Plane.
Используйте типы с равенством на основе значений: для коллекций рекомендуется использовать типы, реализующие равенство на основе значений или реализующие пользовательские типы коллекций, которые переопределяются Object.Equals для сравнения на основе контента, например System.Collections.Immutable.ImmutableArray<T> или System.Collections.Immutable.ImmutableList<T>.
Проектирование с учетом равенства ссылок: примите, что некоторые члены будут использовать равенство ссылок и разрабатывать логику приложения соответствующим образом, обеспечивая повторное использование тех же экземпляров, когда равенство важно.
Ниже приведен пример реализации пользовательского равенства для записей с коллекциями:
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
Эта настраиваемая реализация работает правильно:
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
Эта же проблема влияет на массивы и другие типы коллекций:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Массивы также используют равенство ссылок, создавая те же непредвиденные результаты:
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
Даже коллекции чтения демонстрируют такое поведение равенства ссылок:
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
Ключевое представление заключается в том, что записи решают проблему структурного равенства, но не изменяют поведение семантического равенства содержащихся типов.
Пример класса
В следующем примере показана реализация равенства значений в классе (ссылочный тип). Этот ручной подход необходим в случаях, когда вы не можете использовать записи или требуется кастомная логика сравнения.
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) = {pointA.Equals(pointB)}");
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}");
pointE = new TwoDPoint(3, 4);
Console.WriteLine($"(pointE == pointA) = {pointE == pointA}");
Console.WriteLine($"(pointA == pointE) = {pointA == pointE}");
Console.WriteLine($"(pointA != pointE) = {pointA != pointE}");
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine($"pointE.Equals(list[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 на основе типа времени компиляции. Это основная проблема с полиморфным равенством в иерархиях наследования.
Полиморфное равенство
При реализации равенства значений в иерархиях наследования с классами стандартный подход, показанный в примере класса, может привести к неправильному поведению при использовании объектов полиморфно. Проблема возникает, так как System.IEquatable<T> реализации выбираются на основе типа времени компиляции, а не типа среды выполнения.
Проблема со стандартными реализациями
Рассмотрим этот проблемный сценарий:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
Сравнение возвращается True , так как компилятор выбирает TwoDPoint.Equals(TwoDPoint) на основе объявленного типа, игнорируя Z различия координат.
Ключ к исправлению полиморфного равенства заключается в том, чтобы все сравнения равенства использовали виртуальный Object.Equals метод, который может проверить типы среды выполнения и правильно обрабатывать наследование. Это можно сделать с помощью явной реализации интерфейса для System.IEquatable<T> делегатов виртуального метода:
Базовый класс демонстрирует ключевые шаблоны:
// Safe polymorphic equality implementation using explicit interface implementation
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) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual 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) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
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);
}
Вот как эта реализация обрабатывает проблемные полиморфные сценарии:
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
Реализация также правильно обрабатывает прямые сравнения типов:
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
Реализация равенства также работает правильно с коллекциями:
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
Приведенный выше код демонстрирует ключевые элементы для реализации равенства на основе значений:
-
Виртуальная
Equals(object?)переопределение: основная логика равенства происходит в виртуальном Object.Equals методе, который вызывается независимо от типа времени компиляции. -
Проверка типов среды выполнения. Использование
this.GetType() != p.GetType()гарантирует, что объекты разных типов никогда не считаются равными. - Явная реализация интерфейса: System.IEquatable<T> реализация делегирует виртуальный метод, предотвращая проблемы выбора типов во время компиляции.
-
Защищенный вспомогательный метод виртуального помощника:
protected virtual Equals(TwoDPoint? p)метод позволяет производным классам переопределять логику равенства при сохранении безопасности типов.
Используйте этот шаблон, когда:
- У вас есть иерархии наследования, где важно равенство значений
- Объекты могут использоваться полиморфно (объявленный как базовый тип, экземпляр которого создается как производный тип)
- Вам нужны ссылочные типы с семантикой равенства значений
Предпочтительный подход — использовать 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) = {pointA.Equals(pointB)}");
// True:
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
// True:
Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
// False:
Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
// False:
Console.WriteLine($"(pointA == null) = {pointA == null}");
// True:
Console.WriteLine($"(pointA != null) = {pointA != null}");
// False:
Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
// CS0019:
// Console.WriteLine($"pointA == i = {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]): {pointA.Equals(list[0])}");
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}");
// True:
Console.WriteLine($"pointC == pointD = {pointC == pointD}");
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}");
pointD = temp;
// True:
Console.WriteLine($"pointD == (pointC = 3,4) = {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 метода в структуре цель заключается в предоставлении более эффективного способа проверки равенства значений и, при необходимости, основывания сравнения на определенных полях или свойствах структуры.
Операторы == и != нельзя применять к структурам, если структура явно не перегружает их.