c# - EqualityComparer<T>.Default vs. T.Equals
.net generics (5)
Supongamos que tengo una MyClass<T> genérica que necesita comparar dos objetos de tipo <T> . Usualmente yo haría algo como ...
void DoSomething(T o1, T o2)
{
if(o1.Equals(o2))
{
...
}
}
Ahora suponga que mi MyClass<T> tiene un constructor que admite pasar un IEqualityComparer<T> , similar al Dictionary<T> . En ese caso tendría que hacer ...
private IEqualityComparer<T> _comparer;
public MyClass() {}
public MyClass(IEqualityComparer<T> comparer)
{
_comparer = comparer;
}
void DoSomething(T o1, T o2)
{
if((_comparer != null && _comparer.Equals(o1, o2)) || (o1.Equals(o2)))
{
...
}
}
Para eliminar esta declaración if larga, sería bueno si pudiera tener _comparer predeterminado en un ''comparador predeterminado'' si se usa el constructor normal. Busqué algo como typeof(T).GetDefaultComparer() pero no pude encontrar nada igual.
Encontré EqualityComparer<T>.Default , ¿podría usar eso? Y luego este fragmento ...
public MyClass()
{
_comparer = EqualityComparer<T>.Default;
}
void DoSomething(T o1, T o2)
{
if(_comparer.Equals(o1, o2))
{
...
}
}
... proporciona los mismos resultados que usando o1.Equals(o2) para todos los casos posibles?
(Como nota al margen, ¿esto significaría que también necesitaría usar alguna restricción genérica especial para <T> ?)
¿Podrías usar el operador coaelescense nulo? para acortar el si si realmente importa
if ((_comparer ?? EqualityComparer<T>.Default).Equals(o1, o2))
{
}
De forma predeterminada, hasta que se invalide en una clase, Object.Equals(a,b) / a.Equals(b) realiza una comparación por referencia.
El comparador será devuelto por EqualityComparer<T>.Default depende de T Por ejemplo, si T : IEquatable<> se T : IEquatable<> el EqualityComparer<T> apropiado.
Debería ser el mismo, pero no está garantizado, ya que depende de los detalles de implementación del tipo T
Explicación:
Sin una restricción a T , o1.Equals (o2) llamará a Object.Equals , incluso si T implementa IEquatable<T> .
EqualityComparer<T>.Default embargo, utilizará Object.Equals solamente, si T no implementa IEquatable<T> . Si implementa esa interfaz, utiliza IEquatable<T>.Equals .
Siempre y cuando la implementación de Object.Equals de Object.Equals solo llame a IEquatable<T>.Equals el resultado es el mismo. Pero en el siguiente ejemplo, el resultado no es el mismo:
public class MyObject : IEquatable<MyObject>
{
public int ID {get;set;}
public string Name {get;set;}
public override bool Equals(object o)
{
var other = o as MyObject;
return other == null ? false : other.ID == ID;
}
public bool Equals(MyObject o)
{
return o.Name == Name;
}
}
Ahora, no tiene ningún sentido implementar una clase como esta. Pero tendrá el mismo problema, si el implementador de MyObject simplemente olvidó anular Object.Equals .
Conclusión:
Usar EqualityComparer<T>.Default es una buena forma de EqualityComparer<T>.Default , ¡porque no es necesario que soporte objetos con errores!
Eso es exactamente lo que hace Dictionary<> y otras colecciones genéricas en el BCL si no especifica un comparador al construir el objeto. La ventaja de esto es que EqualityComparer<T>.Default devolverá el comparador correcto para los IEquatable<T> , tipos anulables y enumeraciones de IEquatable. Si T no es ninguno de ellos, hará una comparación simple de Igual a como lo está haciendo el código anterior.
Sí, creo que sería prudente usar EqualityComparer<T>.Default , porque usa la implementación de IEquatable<T> si el tipo T implementa, o la anulación de Object.Equals contrario. Podrías hacerlo de la siguiente manera:
private IEqualityComparer<T> _comparer;
public IEqualityComparer<T> Comparer
{
get { return _comparer?? EqualityComparer<T>.Default;}
set { _comparer=value;}
}
public MyClass(IEqualityComparer<T> comparer)
{
_comparer = comparer;
}
void DoSomething(T o1, T o2)
{
if(Comparer.Equals(o1, o2))
{
...
}
}