Cómo: Definir la igualdad de valores para un tipo (Guía de programación de C#)
Cuando se define una clase o struct, debe decidirse si tiene sentido crear una definición personalizada de la igualdad (o equivalencia) de valores para el tipo. Normalmente, una igualdad de valores se implementa cuando se espera que se agreguen a una colección determinada objetos del tipo concreto o cuando el propósito principal es almacenar un conjunto de campos o propiedades. La definición de igualdad de valores se puede basar en una comparación de todos los campos y propiedades del tipo o bien en un subconjunto. No obstante, en cualquiera de los casos y tanto en las clases como los structs, la implementación debe seguir las cinco garantías de equivalencia:
x.Equals(x) devuelve true. Esto se denomina propiedad reflexiva.
x.Equals(y) devuelve el mismo valor que y.Equals(x). Esto se denomina propiedad simétrica.
si (x.Equals(y) && y.Equals(z)) devuelve true, x.Equals(z) devuelve true. Esto se denomina propiedad transitiva.
Las invocaciones sucesivas de x.Equals(y) devuelven el mismo valor siempre que no se modifiquen los objetos a los que hacen referencia x e y.
x.Equals(null) devuelve false. Sin embargo, null.Equals (null) produce una excepción; no obedece la regla número dos anterior.
Cualquier struct que se defina ya tiene una implementación predeterminada de igualdad de valores que hereda de la invalidación ValueType del método Object.Equals(Object). Esta implementación usa la reflexión para examinar todas las propiedades y los campos públicos y no públicos del tipo. Aunque esta implementación genera resultados correctos, es relativamente lenta en comparación con una implementación personalizada escrita por el usuario específicamente para el tipo.
Los detalles de implementación de la igualdad de valores son distintos para las clases y los structs. Sin embargo, tanto las clases como los structs requieren los mismos pasos básicos para implementar la igualdad:
Invalide el método Object.Equals(Object) virtual. En la mayoría de los casos, la implementación de bool Equals( object obj ) debería llamar simplemente al método Equals específico del tipo que es la implementación de la interfaz IEquatable. (Ver el paso 2).
Implemente la interfaz IEquatable proporcionando un método Equals específico del tipo. Aquí es donde se realiza la comparación de equivalencias en sí. Por ejemplo, puede que decida definir la igualdad mediante la comparación de uno o dos campos del tipo solamente. No inicie excepciones desde Equals. Para las clases solamente: este método solo debe examinar los campos declarados en la clase. Debe llamar a base.Equals para examinar los campos que están en la clase base. (No lleve a cabo esta acción si el tipo hereda directamente de Object, ya que la implementación Object de Object.Equals(Object) realiza una comprobación de igualdad de referencias).
Opcional pero recomendado: sobrecargue los operadores == y !=.
Invalide Object.GetHashCode para que dos objetos que tengan igualdad de valores produzcan el mismo código hash.
Opcional: para admitir las definiciones de "mayor que" o "menor que", implemente la interfaz IComparable para su tipo y sobrecargue también los operadores <= y >=.
El primer ejemplo que sigue las presentaciones una implementación de clase. El segundo ejemplo muestra una implementación de struct.
Ejemplo
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en una clase (tipo de referencia).
namespace ValueEquality
{
using System;
class TwoDPoint : IEquatable<TwoDPoint>
{
// Readonly auto-implemented properties.
public int X { get; private set; }
public int Y { get; private set; }
// Set the properties in the constructor.
public TwoDPoint(int x, int y)
{
if ((x < 1) || (x > 2000) || (y < 1) || (y > 2000))
throw new System.ArgumentException("Point must be in range 1 - 2000");
this.X = x;
this.Y = y;
}
public override bool Equals(object obj)
{
return this.Equals(obj as TwoDPoint);
}
public bool Equals(TwoDPoint p)
{
// If parameter is null, return false.
if (Object.ReferenceEquals(p, 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()
{
return X * 0x00010000 + Y;
}
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
// Check for null on left side.
if (Object.ReferenceEquals(lhs, null))
{
if (Object.ReferenceEquals(rhs, null))
{
// null == null = true.
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)
{
return !(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 System.ArgumentException("Point must be in range 1 - 2000");
this.Z = z;
}
public override bool Equals(object obj)
{
return this.Equals(obj as ThreeDPoint);
}
public bool Equals(ThreeDPoint p)
{
// If parameter is null, return false.
if (Object.ReferenceEquals(p, 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()
{
return (X * 0x100000) + (Y * 0x1000) + Z;
}
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
// Check for null.
if (Object.ReferenceEquals(lhs, null))
{
if (Object.ReferenceEquals(rhs, 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)
{
return !(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.
System.Console.WriteLine("Press any key to exit.");
System.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 referencias, no una comprobación de la igualdad de valores. Cuando un implementador invalida el método virtual, el propósito es asignarle semántica de igualdad de valores.
Los operadores == y != se pueden usar con clases aun en el caso de que la clase no los sobrecargue. Sin embargo, el comportamiento predeterminado consiste en realizar una comprobación de la igualdad de referencias. En una clase, si se sobrecarga el método Equals, se deberían sobrecargar los operadores == y !=, pero no es estrictamente necesario.
En el ejemplo siguiente se muestra cómo implementar la igualdad de valores en un struct (tipo de valor):
struct TwoDPoint : IEquatable<TwoDPoint>
{
// Read/write auto-implemented properties.
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
X = x;
Y = x;
}
public override bool Equals(object obj)
{
if (obj is TwoDPoint)
{
return this.Equals((TwoDPoint)obj);
}
return false;
}
public bool Equals(TwoDPoint p)
{
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode()
{
return X ^ Y;
}
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
{
return !(lhs.Equals(rhs));
}
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// Compare using virtual Equals, static Equals, and == and != operators.
// 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("pointE.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);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.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
*/
}
En el caso de los structs, la implementación predeterminada de Object.Equals(Object) (que es la versión invalidada en ValueType) realiza una comprobación de la igualdad de valores en la que usa la reflexión para comparar los valores de cada campo del tipo. Cuando un implementador invalida el método Equals virtual en un struct, el propósito es proporcionar una forma más eficaz de realizar la comprobación de la igualdad de valores y, opcionalmente, basar la comparación en algún subconjunto del campo o las propiedades del struct.
Los operadores == y != no pueden funcionar en un struct a menos que este los sobrecargue explícitamente.