c# - el tipo t debe ser un tipo de referencia
¿Se debe implementar IEquatable<T>, IComparable<T> en clases no selladas? (3)
¿Alguien tiene alguna opinión sobre si IEquatable<T>
o IComparable<T>
generalmente requieren que T
esté sealed
(si es una class
)?
Esta pregunta se me ocurrió porque estoy escribiendo un conjunto de clases base destinadas a ayudar en la implementación de clases inmutables. Parte de la funcionalidad que la clase base pretende proporcionar es la implementación automática de comparaciones de igualdad (usando los campos de la clase junto con los atributos que se pueden aplicar a los campos para controlar las comparaciones de igualdad). Debería ser bastante agradable cuando termine: estoy usando árboles de expresión para crear dinámicamente una función de comparación compilada para cada T
, por lo que la función de comparación debe estar muy cerca del rendimiento de una función de comparación de igualdad regular. (Estoy usando un diccionario inmutable codificado en System.Type
y control doble de bloqueo para almacenar las funciones de comparación generadas de una manera razonablemente efectiva)
Una cosa que ha surgido, sin embargo, es qué funciones usar para verificar la igualdad de los campos miembros. Mi intención inicial era verificar si el tipo de campo de cada miembro (que llamaré X
) implementa IEquatable<X>
. Sin embargo, después de pensarlo un poco, no creo que esto sea seguro de usar a menos que X
esté sealed
. La razón es que si X
no está sealed
, no puedo estar seguro de si X
está delegando adecuadamente las comprobaciones de igualdad en un método virtual en X
, lo que permite que un subtipo anule la comparación de igualdad.
Esto genera una pregunta más general: si un tipo no está sellado, ¿realmente debería implementar estas interfaces? Yo diría que no, ya que argumentaría que el contrato de interfaces es para comparar dos tipos de X
, no dos tipos que pueden ser o no X
(aunque deben ser, por supuesto, X
o un subtipo).
¿Qué piensan ustedes? ¿ IEquatable<T>
deberían IEquatable<T>
e IComparable<T>
para las clases no selladas? (También me pregunto si hay una regla fxcop para esto)
Mi pensamiento actual es hacer que mi función de comparación generada solo use IEquatable<T>
en los campos miembros cuyo T
esté sealed
, y en su lugar usar el Object.Equals(Object obj)
virtual Object.Equals(Object obj)
si T
se IEquatable<T>
aunque T
implemente IEquatable<T>
, dado que el campo podría potencialmente almacenar subtipos de T
y dudo que la mayoría de las implementaciones de IEquatable<T>
estén diseñadas apropiadamente para la herencia.
En general, recomendaría no implementar IEquatable <T> en ninguna clase no sellada, ni implementar IComparable no genérico en la mayoría, pero no se puede decir lo mismo de IComparable <T>. Dos razones:
- Ya existe un medio para comparar objetos que pueden ser o no del mismo tipo: Object.Equals. Como IEtabletable <T> no incluye GetHashCode, su comportamiento esencialmente tiene que coincidir con Object.Equals. La única razón para implementar IEquatable <T> además de Object.Equals es el rendimiento. IEquatable <T> ofrece una pequeña mejora de rendimiento frente a Object.Equals cuando se aplica a tipos de clase sellados, y una gran mejora cuando se aplica a los tipos de estructura. La única forma en que la implementación de tipo abierto de IEquatable <T> .Equals puede garantizar que su comportamiento coincida con el de un Object.Equals posiblemente anulado, sin embargo, es llamar a Object.Equals. Si IEtabletable <T> .Equals tiene que llamar Object.Equals, cualquier posible ventaja de rendimiento desaparece.
- A veces es posible, significativo y útil que una clase base tenga un orden natural definido que involucre solo propiedades de clase base, que será consistente a través de todas las subclases. Al verificar la igualdad de dos objetos, el resultado no debe depender de si uno considera los objetos como un tipo base o un tipo derivado. Sin embargo, al clasificar los objetos, el resultado a menudo debe depender del tipo que se utilice como base para la comparación. Los objetos de clase derivada deben implementar IComparable <TheirOwnType> pero no deben anular el método de comparación del tipo de base. Es completamente razonable que dos objetos de clase derivada se puedan comparar como "sin clasificar" cuando se comparan con el tipo padre, pero que uno se compare por encima del otro cuando se compara con el tipo derivado.
La implementación de IComparable no genérico en clases heredables es quizás más cuestionable que la implementación de IComparable <T>. Probablemente lo mejor que se puede hacer es permitir que una clase base lo implemente si no se espera que cualquier clase secundaria necesite algún otro orden, sino que las clases secundarias no reimplementen o anulen las implementaciones de la clase padre.
He estado pensando en esta cuestión por un tiempo y después de considerarla un poco, estoy de acuerdo con que la implementación de IEquatable<T>
e IComparable<T>
solo debería hacerse en tipos sellados.
Estuve yendo y viniendo por un tiempo, pero luego pensé en la siguiente prueba. ¿Bajo qué circunstancias debería volverse falso lo siguiente? En mi humilde opinión, 2 objetos son iguales o no lo son.
public void EqualitySanityCheck<T>(T left, T right) where T : IEquatable<T> {
var equals1 = left.Equals(right);
var equals2 = ((IEqutable<T>)left).Equals(right);
Assert.AreEqual(equals1,equals2);
}
El resultado de IEquatable<T>
en un objeto dado debe tener el mismo comportamiento que Object.Equals
suponiendo que el comparador es del tipo equivalente. Implementar IEquatable<T>
dos veces en una jerarquía de objetos permite, e implica, que hay dos formas diferentes de expresar la igualdad en su sistema. Es fácil idear cualquier cantidad de escenarios en los que IEquatable<T>
y Object.Equals
difieran ya que hay múltiples IEquatable<T>
pero solo un Object.Equals
. Por lo tanto, lo anterior fallaría y crearía un poco de confusión en su código.
Algunas personas pueden argumentar que la implementación de IEquatable<T>
en un punto más alto en la jerarquía de objetos es válida porque desea comparar un subconjunto de las propiedades de los objetos. En ese caso, debe favorecer un IEqualityComparer<T>
que está específicamente diseñado para comparar esas propiedades.
La mayoría de las implementaciones Equals
que he visto comprueban los tipos de objetos que se comparan, si no son iguales, el método devuelve falso.
Esto evita claramente el problema de que un subtipo se compare con su tipo padre, lo que anula la necesidad de sellar una clase.
Un ejemplo obvio de esto sería tratar de comparar un punto 2D (A) con un punto 3D (B): para un 2D, los valores x e y de un punto 3D pueden ser iguales, pero para un punto 3D, el valor z será muy probablemente sea diferente.
Esto significa que A == B
sería verdadero, pero B == A
sería falso. A la mayoría de las personas les gusta que los operadores de Equals
sean conmutativos, a los tipos de comprobación es claramente una buena idea en este caso.
¿Pero qué pasa si subclases y no agrega ninguna propiedad nueva? Bueno, eso es un poco más difícil de responder, y posiblemente dependa de tu situación.