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))
{
...
}
}