Equals, == y GetHashCode en C# (1)

Cada vez que necesitaba implementar la igualdad tenía que consultar a San Google, y aunque acabé creando un snippet para hacerlo rápido (y Resharper ahora también genera código para ello), era hora de que resumiese los distintos conceptos y normas que hay tras la comparación de igualdad en .NET con C#.

photo credit: Mouse via photopin cc

photo credit: Mouse cc

El objetivo de implementar la igualdad es cambiar el comportamiento por defecto de esta en las clases, la cual siempre es por referencia: dos instancias serán iguales si ambas instancias son las mismas. Sin embargo en ocasiones querremos que la igualdad se base en lo que representa la instancia. Un ejemplo de esto son las cadenas: dos cadenas son iguales si son la misma cadena, aunque sean instancias distintas. Otro ejemplo serán los DataRowView, cuyas instancias son iguales si referencian a la misma fila.

Las estructuras se comportan de modo distinto; por defecto dos instancias de una estructura son iguales si todas sus propiedades valen lo mismos. No será habitual que queramos cambiar dicho comportamiento, pero como veremos sobreescribir la igualdad tiene mejor rendimiento.

El método a sobreescrbir será public static bool Equals(object objA, object objB), que compara dos objetos y nos indica si son iguales. Querremos que el sentido de esta igualdad sea semántico: dos instancias son iguales si representan el mismo objeto real.

El comportamiento del método cambia según se trate de tipos por referencia y tipos por valor. Estos últimos heredan de ValueType, que provee una implementación por defecto que compara los valores de todas las propiedades. La implementación que heredan de Object los objetos realiza una comparación de referencias. Las diferencias de comportamiento se comprueban con el siguiente código:

public class ByReferece {
    public int Property { get; set; }
}

public struct ByValue {
    public int Property { get; set; }
}

ByReferece ref1 = new ByReferece() { Property=1};
ByReferece ref2 = new ByReferece() {Property = 1};
ByValue val1 = new ByValue() { Property = 1 };
ByValue val2 = new ByValue() { Property = 1 };

Console.WriteLine("Las instancias por referencia son iguales? " + ref1.Equals(ref2));
Console.WriteLine("Las instancias por valor son iguales? " + val1.Equals(val2));

// resultado:
// Las instancias por referencia son iguales? False
// Las instancias por valor son iguales? True

ValueType compara las instancias usando la reflexión para comparar todas las propiedad. Puesto que la reflexión es algo lenta, si implementamos nuestro propio Equals obtendremos un mejor desempeño. El siguiente código muestra una implementación sencilla de Equals, y de paso probamos su eficiencia en tipos por valor y por referencia:

public struct SlowProduct {
	public int Price { get; set; }
	public string Description { get; set; }
	public decimal DefaultPrice { get; set; }
}
public struct FastProduct {
	public int Price { get; set; }
	public string Description { get; set; }
	public decimal DefaultPrice { get; set; }
	public override bool Equals(object obj) {
		FastProduct typedObj = (FastProduct)obj;
		if (this.DefaultPrice != typedObj.DefaultPrice) return false;
		if (this.Description != typedObj.Description) return false;
		if (this.Price != typedObj.Price) return false;
		return true;

	}
}

List SlowProducts = new List();
List FastProducts = new List();
for (int i = 0; i < 15000; i++) {
	SlowProducts.Add(new SlowProduct() { DefaultPrice = DateTime.Now.Millisecond });
	FastProducts.Add(new FastProduct() { DefaultPrice = DateTime.Now.Millisecond });
}
Stopwatch sw = new Stopwatch();
sw.Start();
var slows = SlowProducts.Intersect(SlowProducts).ToList();
sw.Stop();

Console.WriteLine("SlowProducts tardó " + sw.ElapsedMilliseconds);
sw.Restart();
var fast = FastProducts.Intersect(FastProducts).ToList();
sw.Stop();
Console.WriteLine("FastProducts tardó " + sw.ElapsedMilliseconds);
Console.ReadKey();

// resultado
// SlowProducts tardó 1250
// FastProducts tardó 171

[Nota: En el Famework 4.5 no usa siempre la reflexión: “Si ninguno de los campos de la instancia actual y obj son tipos de referencia, el método Equals realiza una comparación byte a byte de los dos objetos en memoria. De lo contrario, usa la reflexión para comparar los campos obj correspondientes y de esta instancia.”]

photo credit: Max Nathan via photopin cc

photo credit: Max Nathan via photopin cc

Equals(object) no es el único método relacionado con la igualdad. Object también expone el método public static bool ReferenceEquals (object left, object right) que determina si dos instancias de object son la misma instancia. Dado que el método no tiene conocimiento suficiente sobre los objetos para establecer su igualdad, llama directamente al Equals(Object) que habremos sobreescrito anteriormente.

Un detalle a tener en cuenta sobre ReferenceEquals es su aplicación a tipos por valor. Dado que los dos parámetros son object, si pasamos dos valores se convertirán automáticamente a object. Si pasamos dos veces el mismo valor, se convertirá dos veces, por lo que nos dirá que no se tratan de la misma referencia (lo cual es completamente cierto).

float i = 5.5f;
float j = 5.5f;
 if (ReferenceEquals(i, j))
	Console.WriteLine("Son la misma referencia.");
else
	Console.WriteLine("Son referencias distintas");
// Resultado
// Son referencias distintas

Al implementar Equals() es importante tener en cuenta es qué condiciones deberá tener nuestra función de comparación para cumplir las expectativas que quien use nuestro código. Pueden parecer bastante obvias, pero deberemos tenerlas en cuenta siempre:

  • x.Equals(x) devuelve true. (x=x)
  • x.Equals(y) devuelve el mismo valor que y.Equals(x). (x=y debe valer lo mismo que y=x)
  • (x.Equals(y) & y.Equals(z)) devuelve el valor true si y solo si x.Equals(z) devuelve el valor true. (si x=y e y=z, entonces x=z)

Las invocaciones sucesivas de x.Equals(y) devuelven el mismo valor siempre que no se modifiquen los objetos a los que se hace referencia mediante x e y.

Además de estas propiedades el contrato de Equals también impone que:

  • x.Equals(null) devuelve el valor false.
  • No debería lanzar una excepción en la implementación de un método Equals. En vez de esto, se devuelve un valor false para un argumento nulo.

Un posible ejemplo de implementación más elaborado para un tipo por valor podría se este:

public class Foo
{
public int Property { get; set;}

public override bool Equals(Object right)
{
	if (Object.ReferenceEquals(right, null))
		return false;
	if (Object.ReferenceEquals(this, right))
		return true;
	if (this.GetType() != right.GetType())
	return false;

	return this.Propery==((Foo)right).Property;
}

Esta implementación comprueba, por este orden, si el parámetro es nulo, si es la misma referencia que this, si es exactamente del mismo tipo que this (no sólo que se pueda convertir), y por último el valor de la propiedad que define la igualdad.

photo credit: Leo Reynolds via photopin cc

photo credit: Leo Reynolds via photopin cc

Si nuestra clase tiene que realizar múltiples comprobaciones para determinar la igualdad, podríamos crear un método que contenga dicha lógica. Si dicho método lo llamamos Equals pero el parámetro es de tipo Foo y no Object estaremos implementando IEquatable , y así matamos dos pájaros de un tiro. IEquatable no es un interfaz cualquiera; se usa en varios métodos de varias colecciones genéricas, así que implementándolas podremos agilizar la ejecución. Tangencialmente, señalar que si implementamos IEquatable y tiene sentido querer ordenar instancias de la clase deberemos implementar IComparable.

public class Foo : IEquatable
{
public int Property { get; set;}

public override bool Equals(object right)
{
	// check null:
	// this pointer is never null in C# methods.
	if (object.ReferenceEquals(right, null))
		return false;
	if (object.ReferenceEquals(this, right))
		return true;

	return this.Equals(right as Foo);
}
public bool Equals(Foo other)
{
	return this.Propery==right.Property;
}
}

En cuanto al herencia, “cuando el método Equals de una clase base proporciona igualdad de valores, un reemplazo del método Equals de una clase derivada deberá llamar a la implementación heredada de Equals”. Esto es cierto siempre y cuando nuestro objeto no herede directamente de Object (en cuyo caso estaríamos usando la igualdad por referencia). El ejemplo de la documentación es muy ilustrativo:

class Point3D: Point
{
   int z;

   public Point3D(int xValue, int yValue, int zValue) : base(xValue, yValue)
   {
        z = zValue;
   }
   public override bool Equals(Object obj)
   {
      return base.Equals(obj) && z == ((Point3D)obj).z;
   }
}

Por último, el operador == debería comportarse del mismo modo que Equals(), así que al implementar == deberíamos implementar Equals() y usarlo para calcular si dos objetos son el mismo.

Anuncios
Tagged with: , ,
Publicado en Programacion
One comment on “Equals, == y GetHashCode en C# (1)
  1. […] primera parte puede leerse aquí)El método GetHashCode es un método peculiar cuando no se conoce. La primera vez que lo vi pensé […]

Deixa a túa opinión

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: