Comparar objetos es una acción muy común en cualquier programa que estemos escribiendo, de hecho, posiblemente sea una de las acciones que con mas frecuencia realizemos, ya que cada vez que indicamos una expresión if o un swich estamos comparando.
Pero comparar no termina ahí, si estamos filtrando u ordenando también vamos a comparar dos elementos.
Índice
1 - Diferencia entre == y equals en C#
Cuando comparamos lo hacemos o bien con ==
o con el método Equals
de objeto y pese a que parece que ambas opciones van a darnos el mismo resultado no es así.
- Nota: tenemos otras opciones como utilizar
Object.Equals(elemento1, elemento2)
oObject.ReferenceEquals(e1, e2);
.
El resultado que vamos a obtener va a depender si estamos trabajando con tipos por valor o tipos por referencia.
Ademas, recuerda que puedes sobreescribir los operadores, en este ejemplo lo vamos a ver todo sin sobreescribir
1.1 - Comparar tipos por valor
Cuando utilizamos tipos por valor, osea tipos primitivos o structs si utilizamos el operador ==
lo que vamos a hacer es comparar el valor del elemento y nos devolverá verdadero o falso.
int value1 = 1;
int value2 = 1;
bool sonIguales = value1 == value2; //true
Ten en cuenta que si creas un struct
, por defecto no vas a poder compararlos con el operador==
, sino que tienes que sobreescribir dicho operador.
public struct ElementoPorValor
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public static bool operator ==(ElementoPorValor c1, ElementoPorValor c2)
{
return c1.Equals(c2);
}
public static bool operator !=(ElementoPorValor c1, ElementoPorValor c2)
{
return !c1.Equals(c2);
}
}
Ahora ya podemos ejecutar el operador ==
, pero como te has dado cuenta, estamos llamando al método .Equals
que en los tipos por valor, comprobará primero el tipo, y luego comprueba que cada uno de los valores son iguales.
ElementoPorValor elemento1 = new ElementoPorValor()
{
Valor1 = 1,
Valor2 = 2
};
ElementoPorValor elemento2 = new ElementoPorValor()
{
Valor1 = 1,
Valor2 = 2
};
bool sonIguales = elemento1 == elemento2; //true
bool sonIgualesOpt2 = elemento1.Equals(elemento2);//true
1.2 - Tipos por referencia
En los tipos por referencia tanto el operador ==
como el método .Equals
comprueban que la referencia al objeto sea la misa:
ElementoReferencia elemento1 = new ElementoReferencia()
{
Valor1 = 1,
Valor2 = 2
};
ElementoReferencia elemento2 = new ElementoReferencia()
{
Valor1 = 1,
Valor2 = 2
};
ElementoReferencia elemento1Copia = elemento1;
bool dosPrimeros = elemento1 == elemento2; //false;
bool laCopia = elemento1 == elemento1Copia; //true
bool dosPrimeros2 = elemento1.Equals(elemento2); //false;
bool laCopia2 = elemento1.Equals(elemento1Copia); //true
Por supuesto como en el caso anterior también podemos sobreescribir el operador ==
para que funcione como nosotros queramos:
public class ElementoReferencia
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public static bool operator ==(ElementoReferencia c1, ElementoReferencia c2)
{
return c1.Valor1 == c2.Valor1 && c1.Valor2 == c2.Valor2;
}
public static bool operator !=(ElementoReferencia c1, ElementoReferencia c2)
{
return c1.Valor1 != c2.Valor1 || c1.Valor2 != c2.Valor2;
}
}
//Y ahora esta comparación
bool dosPrimeros = elemento1 == elemento2; //anteriormente falso ahora da verdadero
2 - La interfaz IEquatable
Hemos visto las formas más comunes de comparar dos objetos, pero, que pasa si nuestro caso de uso es un poco más extremo. Por ejemplo, nuestro objeto tiene 3 propiedades pero por el motivo que sea solo queremos comprobar 2 de ellas.
public class ElementoEspecial
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public DateTime Fecha { get; set; }
}
Como hemos visto anteriormente podemos sobreescribir el operador ==
, pero personalmente no recomiendo quedarnos únicamente ahí.
Lo que yo personalmente recomiendo es implementar la interfaz IEquatable<T>
la cual es utilizada por el tipo Object
, con lo cual lo único que haremos será sobrescribir el funcionamiento del método Equals
.
Cabe destacar antes de continuar que Equals
actúa sobre recibe object bool Equals(object obj)
y que para añadir el nuestro, lo podemos hacer sin utilizar IEquatable
ya que crearemos bool Equals(T objeto)
pero, por compresión de quien venga después en el código, deberíamos sobreescribir IEquatable
.
De esta manera, modificando tanto el operador ==
como el Equals se nos queda un código claro y si además invocamos .Equals
desde el operador==
lo que haremos será crear una consistencia en la ejecución muy importante, la cual nos puede sacar de más de uno y dos problemas.
public class ElementoIEquatable : IEquatable<ElementoIEquatable>
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public DateTime Fecha { get; set; }
public static bool operator ==(ElementoIEquatable c1, ElementoIEquatable c2)
{
return c1.Equals(c2);
}
public static bool operator !=(ElementoIEquatable c1, ElementoIEquatable c2)
{
return !c1.Equals(c2);
}
//Comparamos los objetos, pero sin tener en cuenta la fecha.
public bool Equals(ElementoIEquatable other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Valor1 == other.Valor1 && Valor2 == other.Valor2;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ElementoIEquatable)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(Valor1, Valor2);
}
}
3 - Las Interfaz IComparer e IComparable
Si lo que buscamos es comparar objetos de una forma en la que un elemento es mayor que otro no lo haremos con IEQuatable
, porque como hemos visto, solo nos sale un resultado verdadero o falso.
Por el contrario lo haremos con la interfaz ICompare
o la interfaz IComparable
, ambas parecen lo mismo pero son un poquitín diferentes.
Primero vamos con las partes comunes, el resultado, ambas van a crear un método el cual va a devolver un entero, y este entero tiene 3 posibilidades:
-1
si el primer elemento, o el actual es “mayor”.- 0 si ambos son iguales
- 1 si el segundo elemento, o el introducido es “mayor”
La definición de "mayor" es la que tu quieras diseñar dentro del método.
Cuando implementamos la interfaz IComparable<T>
vamos a tener que implementar el método CompareTo(T obj)
y cuando implementamos IComparer<T>
vamos a implementar Compare(T x, T y)
.
public class ElementoIComparer : IComparer<ElementoIComparer>, IComparable<ElementoIComparer>
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
//ignoramos la fecha de la comparación
public DateTime Fecha { get; set; }
public int Compare(ElementoIComparer x, ElementoIComparer y)
{
if (ReferenceEquals(x, y)) return 0;
if (ReferenceEquals(null, y)) return 1;
if (ReferenceEquals(null, x)) return -1;
int valor1Comparison = x.Valor1.CompareTo(y.Valor1);
if (valor1Comparison != 0) return valor1Comparison;
return x.Valor2.CompareTo(y.Valor2);
}
public int CompareTo(ElementoIComparer other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
int valor1Comparison = Valor1.CompareTo(other.Valor1);
if (valor1Comparison != 0) return valor1Comparison;
return Valor2.CompareTo(other.Valor2);
}
}
Como puedes ver en ambos casos estamos ignorando la fecha de la comparación
3.1 - Cuándo utilizar IComparable
Implementar icomparable
nos va a permitir que si tenemos una lista de elementos podemos ordenarla utilizando linq, basándonos en la lógica que tenemos dentro del método CompareTo
ElementoIComparar elemento1 = new ElementoIComparar(1, 2, DateTime.UtcNow);
ElementoIComparar elemento2 = new ElementoIComparar(2, 2, DateTime.UtcNow);
ElementoIComparar elemento3 = new ElementoIComparar(1, 2, DateTime.UtcNow);
List<ElementoIComparar>listaComparar = new List<ElementoIComparar>()
{
elemento1, elemento2, elemento3
};
List<ElementoIComparar> listaOrdenada = listaComparar.OrderBy(x => x).ToList();
// elemento2 es el último al tener el primer valor con un 2
Assert.AreEqual(elemento2, listaOrdenada.Last());
Como vemos el elemento2
ha pasado a la última posición.
3.2 - Cuándo Utilizar IComparer
Cuando quieres realizar una comparación, pero no necesariamente con el objeto en el que estás. Me explico, implementar la interfaz IComparer<T>
nos crea el método Compare(T x, T y)
con lo cual puedes introducir Cualquier T
, no estás comparando con el elemento en concreto.
Personalmente pienso que esta comparación debería estar en una clase abstracta, helper etc, porque pese a ser ejecutada en un objeto, no interacciona con el.
Conclusión
- En este post hemos visto cómo comparar objetos en C# utilizando diferentes métodos.
- Cómo modificar el
operador==
y el método equals para poder alterar las comparaciones - Cómo implementar las interfaces
IComparable
yIComparer
.