Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Sugerencia
Considere la posibilidad de usar primero los registros . Los registros implementan automáticamente la igualdad de valores con código mínimo, lo que los convierte en el enfoque recomendado para la mayoría de los tipos centrados en los datos. Si necesita lógica de igualdad de valores personalizada o no puede usar registros, continúe con los pasos de implementación manuales que se indican a continuación.
Cuando defina una clase o un struct, debe decidir si tiene sentido crear una definición personalizada de igualdad (o equivalencia) de valores para el tipo. Normalmente, la igualdad de valores se implementa cuando se espera agregar objetos del tipo a una colección, o cuando su objetivo principal es almacenar un conjunto de campos o propiedades. Puede basar la definición de la igualdad de valores en una comparación de todos los campos y propiedades del tipo, o bien puede basarla en un subconjunto.
En cualquier caso, tanto en las clases como en las estructuras, la implementación debe cumplir las cinco garantías de equivalencia (en las siguientes reglas, se da por hecho que x, y y z no son NULL):
La propiedad reflexiva
x.Equals(x)devuelvetrue.La propiedad simétrica
x.Equals(y)devuelve el mismo valor quey.Equals(x).La propiedad transitiva: si
(x.Equals(y) && y.Equals(z))devuelvetrue,x.Equals(z)devuelvetrue.Las invocaciones sucesivas de
x.Equals(y)devuelven el mismo valor siempre y cuando los objetos a los que x e y hacen referencia no se modifiquen.Cualquier valor distinto de NULL no es igual a NULL. Sin embargo,
x.Equals(y)produce una excepción cuandoxes NULL. Esto rompe las reglas 1 o 2, en función del argumento deEquals.
Cualquier struct que defina ya tiene una implementación predeterminada de igualdad de valor que hereda de la invalidación System.ValueType del método Object.Equals(Object). Esta implementación usa la reflexión para examinar todos los campos y propiedades del tipo. Aunque esta implementación genera resultados correctos, es relativamente lenta en comparación con una implementación personalizada escrita específicamente para el tipo.
Los detalles de implementación para la igualdad de valores son diferentes para las clases y los structs. A pesar de ello, tanto las clases como los structs requieren los mismos pasos básicos para implementar la igualdad:
Invalide el método virtualObject.Equals(Object). Esto proporciona un comportamiento de igualdad polimórfico, lo que permite que los objetos se comparen correctamente cuando se tratan como
objectreferencias. Garantiza un comportamiento adecuado en colecciones y cuando se usa polimorfismo. En la mayoría de los casos, la implementación debool Equals( object obj )debería llamar solamente al métodoEqualsespecífico del tipo que es la implementación de la interfaz System.IEquatable<T>. (Vea el paso 2).Implemente la interfaz System.IEquatable<T> proporcionando un método
Equalsespecífico del tipo. Esto proporciona una comprobación de igualdad segura de tipos sin conversión boxing, lo que da lugar a un mejor rendimiento. También evita la conversión innecesaria y habilita la comprobación de tipos en tiempo de compilación. Aquí es donde se realiza la comparación de equivalencias propiamente dicha. Por ejemplo, podría decidir que, para definir la igualdad, solo se comparen uno o dos campos del tipo. No genere excepciones desdeEquals. Para las clases que están relacionadas por herencia:este método debe examinar únicamente los campos que se declaran en la clase. Debe llamar a
base.Equalspara examinar los campos que están en la clase base. (No llame abase.Equalssi el tipo hereda directamente de Object, porque la implementación Object de Object.Equals(Object) realiza una comprobación de igualdad de referencia).Dos variables deben considerarse iguales solo si los tipos en tiempo de ejecución de las variables que se van a comparar son los mismos. Además, asegúrese de que se utiliza la implementación
IEquatabledel métodoEqualspara el tipo en tiempo de ejecución si los tipos en tiempo de ejecución y en tiempo de compilación de una variable son diferentes. Una estrategia para asegurarse de que los tipos en tiempo de ejecución siempre se comparan correctamente es implementarIEquatablesolo en clasessealed. Para obtener más información, vea el ejemplo de clases más adelante en este artículo.
Opcional, pero recomendado: Sobrecargue los operadores == y !=. Esto proporciona una sintaxis coherente e intuitiva para comparaciones de igualdad, lo que coincide con las expectativas del usuario de los tipos integrados. Garantiza que
obj1 == obj2yobj1.Equals(obj2)se comporten de la misma manera.Invalide Object.GetHashCode de manera que dos objetos que tengan igualdad de valor produzcan el mismo código hash. Esto es necesario para un comportamiento correcto en colecciones basadas en hash, como
Dictionary<TKey,TValue>yHashSet<T>. Los objetos iguales deben tener códigos hash iguales o estas colecciones no funcionarán correctamente.Opcional: Para admitir definiciones para "mayor que" o "menor que", implemente la interfaz IComparable<T> para el tipo y sobrecargue los operadores <= y >=. Esto habilita las operaciones de ordenación y proporciona una relación de ordenación completa para el tipo, útil al agregar objetos a colecciones ordenadas o al ordenar matrices o listas.
Ejemplo de registro
En el ejemplo siguiente se muestra cómo los registros implementan automáticamente la igualdad de valores con código mínimo. El primer registro TwoDPoint es un tipo de registro simple que implementa automáticamente la igualdad de valores. El segundo registro ThreeDPoint muestra que los registros pueden derivarse de otros registros y mantener un comportamiento adecuado de igualdad de valores:
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 }
*/
Los registros proporcionan varias ventajas para la igualdad de valores:
-
Implementación automática: los registros implementan System.IEquatable<T> e invalidan Object.Equalsautomáticamente , Object.GetHashCodey los
==/!=operadores . -
Comportamiento de herencia correcto: los registros implementan
IEquatable<T>el uso de métodos virtuales que comprueban el tipo de tiempo de ejecución de ambos operandos, lo que garantiza un comportamiento correcto en jerarquías de herencia y escenarios polimórficos. - Inmutabilidad de forma predeterminada: los registros fomentan el diseño inmutable, que funciona bien con la semántica de igualdad de valores.
- Sintaxis concisa: los parámetros posicionales proporcionan una manera compacta de definir tipos de datos.
- Mejor rendimiento: la implementación de igualdad generada por el compilador está optimizada y no usa reflexión como la implementación de estructura predeterminada.
Use registros cuando el objetivo principal es almacenar datos y necesita semántica de igualdad de valores.
Registros con miembros que usan igualdad de referencia
Cuando los registros contienen miembros que usan igualdad de referencia, el comportamiento automático de igualdad de valores de los registros no funciona según lo previsto. Esto se aplica a colecciones como System.Collections.Generic.List<T>, matrices y otros tipos de referencia que no implementan la igualdad basada en valores (con la excepción notable de , que implementa la igualdad de System.Stringvalores).
Importante
Aunque los registros proporcionan una igualdad de valores excelente para los tipos de datos básicos, no resuelven automáticamente la igualdad de valores para los miembros que usan la igualdad de referencia. Si un registro contiene un System.Collections.Generic.List<T>, System.Arrayu otros tipos de referencia que no implementan la igualdad de valores, dos instancias de registro con contenido idéntico en esos miembros seguirán sin ser iguales porque los miembros usan igualdad de referencia.
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!
Esto se debe a que los registros usan el Object.Equals método de cada miembro y los tipos de colección suelen usar la igualdad de referencia en lugar de comparar su contenido.
A continuación se muestra el problema:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Así es como se comporta al ejecutar el código:
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();
Soluciones para registros con miembros de igualdad de referencia
Implementación personalizadaSystem.IEquatable<T>: reemplace la igualdad generada por el compilador por una versión codificada a mano que proporciona una comparación basada en contenido para los miembros de igualdad de referencia. En el caso de las colecciones, implemente la comparación de elementos por elemento mediante Enumerable.SequenceEqual o métodos similares.
Use tipos de valor siempre que sea posible: considere si los datos se pueden representar con tipos de valor o estructuras inmutables que admiten naturalmente la igualdad de valores, como System.Numerics.Vector<T> o Plane.
Usar tipos con igualdad basada en valores: en el caso de las colecciones, considere la posibilidad de usar tipos que implementan la igualdad basada en valores o implementan tipos de recopilación personalizados que invalidan Object.Equals para proporcionar una comparación basada en contenido, como System.Collections.Immutable.ImmutableArray<T> o System.Collections.Immutable.ImmutableList<T>.
Diseño con igualdad de referencia en mente: acepte que algunos miembros usarán la igualdad de referencia y diseñarán la lógica de la aplicación en consecuencia, asegurándose de reutilizar las mismas instancias cuando la igualdad sea importante.
Este es un ejemplo de implementación de la igualdad personalizada para los registros con colecciones:
// 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();
}
}
Esta implementación personalizada funciona correctamente:
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();
El mismo problema afecta a matrices y otros tipos de colección:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Las matrices también usan la igualdad de referencia, lo que genera los mismos resultados inesperados:
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();
Incluso las colecciones de solo lectura muestran este comportamiento de igualdad de referencia:
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();
La información clave es que los registros resuelven el problema de igualdad estructural , pero no cambian el comportamiento de igualdad semántica de los tipos que contienen.
Ejemplo de clase
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en una clase (tipo de referencia). Este enfoque manual es necesario cuando no se pueden usar registros o se necesita lógica de igualdad personalizada:
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
*/
En las clases (tipos de referencia), la implementación predeterminada de ambos métodos Object.Equals(Object) realiza una comparación de igualdad de referencia, no una comprobación de igualdad de valores. Cuando un implementador invalida el método virtual, lo hace para asignarle semántica de igualdad de valores.
Los operadores == y != pueden usarse con clases, incluso si la clase no los sobrecarga, pero el comportamiento predeterminado consiste en realizar una comprobación de igualdad de referencia. En una clase, si sobrecarga el método Equals, debería sobrecargar los operadores == y !=, pero no es obligatorio.
Importante
Es posible que el código de ejemplo anterior no controle cada escenario de herencia de la manera esperada. Observe el código siguiente:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Este código notifica que p1 es igual a p2 pesar de la diferencia en los valores z. La diferencia se omite porque el compilador elige la implementación TwoDPoint de IEquatable basándose en el tipo en tiempo de compilación. Se trata de un problema fundamental con la igualdad polimórfica en las jerarquías de herencia.
Igualdad polimórfica
Al implementar la igualdad de valores en jerarquías de herencia con clases, el enfoque estándar que se muestra en el ejemplo de clase puede provocar un comportamiento incorrecto cuando los objetos se usan polimórficamente. El problema se produce porque System.IEquatable<T> las implementaciones se eligen en función del tipo en tiempo de compilación, no del tipo en tiempo de ejecución.
El problema con las implementaciones estándar
Considere este escenario problemático:
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!
La comparación devuelve True porque el compilador selecciona TwoDPoint.Equals(TwoDPoint) en función del tipo declarado, ignorando las Z diferencias de coordenadas.
La clave para corregir la igualdad polimórfica es garantizar que todas las comparaciones de igualdad usen el método virtual Object.Equals , que puede comprobar los tipos en tiempo de ejecución y controlar correctamente la herencia. Esto se puede lograr mediante la implementación explícita de la interfaz para System.IEquatable<T> que delegue al método virtual:
La clase base muestra los patrones clave:
// 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);
}
La clase derivada amplía correctamente la lógica de igualdad:
// 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);
}
Así es como esta implementación controla los escenarios polimórficos problemáticos:
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();
La implementación también controla correctamente las comparaciones de tipos directos:
// 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();
La implementación de igualdad también funciona correctamente con colecciones:
// 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
El código anterior muestra los elementos clave para implementar la igualdad basada en valores:
-
Invalidación virtual
Equals(object?): la lógica de igualdad principal se produce en el método virtualObject.Equals, al que se llama independientemente del tipo en tiempo de compilación. -
Comprobación de tipos en tiempo de ejecución: el uso
this.GetType() != p.GetType()garantiza que los objetos de distintos tipos nunca se consideran iguales. - Implementación de interfaz explícita: la System.IEquatable<T> implementación delega al método virtual, lo que impide problemas de selección de tipos en tiempo de compilación.
-
Método auxiliar virtual protegido: el
protected virtual Equals(TwoDPoint? p)método permite que las clases derivadas invaliden la lógica de igualdad al tiempo que se mantiene la seguridad de tipos.
Use este patrón en los siguientes supuestos:
- Tiene jerarquías de herencia en las que la igualdad de valores es importante
- Los objetos se pueden usar polimórficamente (declarados como tipo base, creados como tipos derivados)
- Necesita tipos de referencia con semántica de igualdad de valores
El enfoque preferido es usar record tipos para implementar la igualdad basada en valores. Este enfoque requiere una implementación más compleja que el enfoque estándar y requiere pruebas exhaustivas de escenarios polimórficos para garantizar la corrección.
Ejemplo de estructura
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en una estructura (tipo de valor). Aunque las estructuras tienen igualdad de valor predeterminada, una implementación personalizada puede mejorar el rendimiento:
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
*/
}
Para los structs, la implementación predeterminada de Object.Equals(Object) (que es la versión invalidada de System.ValueType) realiza una comprobación de igualdad de valor con la reflexión para comparar valores de cada campo en el tipo. Aunque esta implementación genera resultados correctos, es relativamente lenta en comparación con una implementación personalizada escrita específicamente para el tipo.
Al invalidar el método virtual Equals en una estructura, el propósito es proporcionar un medio más eficaz de realizar la comprobación de igualdad de valores y, opcionalmente, basar la comparación en algún subconjunto de los campos o propiedades de la estructura.
Los operadores == y != no pueden funcionar en un struct a menos que el struct los sobrecargue explícitamente.