c# - ¿I<D> vuelve a implementar I<B> si I<D> es convertible a I<B> por conversión de varianza?
c#-4.0 covariance (4)
Creo que es porque llamar:
ICloneable<Base> cb = d;
sin variación, entonces cb
solo puede representar ICloneable<Base>
. Pero con la varianza, también puede representar ICloneable<Derived>
, que obviamente es más cercano y mejor en el reparto de d
que el casting a ICloneable<Base>
.
interface ICloneable<out T>
{
T Clone();
}
class Base : ICloneable<Base>
{
public Base Clone() { return new Base(); }
}
class Derived : Base, ICloneable<Derived>
{
new public Derived Clone() { return new Derived(); }
}
Dadas estas declaraciones de tipo, ¿qué parte de la especificación de C # explica por qué la última línea del siguiente fragmento de código se imprime "Verdadero"? ¿Pueden los desarrolladores confiar en este comportamiento?
Derived d = new Derived();
Base b = d;
ICloneable<Base> cb = d;
Console.WriteLine(b.Clone() is Derived); // "False": Base.Clone() is called
Console.WriteLine(cb.Clone() is Derived); // "True": Derived.Clone() is called
Tenga en cuenta que si el parámetro de tipo T
en ICloneable
no se declara, ambas líneas se imprimirán "Falso".
Es complicado.
La llamada a b.Clone claramente debe invocar BC. No hay ninguna interfaz involucrada aquí en absoluto! El método para llamar está determinado completamente por el análisis en tiempo de compilación. Por lo tanto debe devolver una instancia de Base. Este no es muy interesante.
La llamada a cb.Clone por el contrario es extremadamente interesante.
Hay dos cosas que tenemos que establecer para explicar el comportamiento. Primero: ¿qué "ranura" se invoca? Segundo: ¿qué método hay en esa ranura?
Una instancia de Derived debe tener dos ranuras, ya que hay dos métodos que deben implementarse: ICloneable<Derived>.Clone
y ICloneable<Base>.Clone
. Llamemos a esas ranuras ICDC e ICBC.
Claramente, la ranura invocada por cb.Clone debe ser la ranura ICBC; no hay ninguna razón para que el compilador sepa que la ranura ICDC existe incluso en cb, que es de tipo ICloneable<Base>
.
¿Qué método va en esa ranura? Hay dos métodos, Base.Clone y Derived.Clone. Llamemos a esos BC y DC. Como ha descubierto, el contenido de esa ranura en una instancia de Derived es DC.
Esto parece extraño. Claramente, el contenido de la ranura ICDC debe ser DC, pero ¿por qué el contenido de la ranura ICBC también debe ser DC? ¿Hay algo en la especificación de C # que justifique este comportamiento?
Lo más cercano que obtenemos es la sección 13.4.6, que trata de la "reimplementación de la interfaz". Brevemente, cuando dices:
class B : IFoo
{
...
}
class D : B, IFoo
{
...
}
luego, en lo que respecta a los métodos de IFoo, comenzamos desde cero en D. Todo lo que B tiene que decir sobre qué métodos de B se asignan a los métodos de IFoo se descarta; D podría elegir las mismas asignaciones que hizo B, o podría elegir otras completamente diferentes. Este comportamiento puede llevar a algunas situaciones no anticipadas; Puedes leer más sobre ellos aquí:
http://blogs.msdn.com/b/ericlippert/archive/2011/12/08/so-many-interfaces-part-two.aspx
Pero: ¿es una implementación de ICloneable<Derived>
una reimplementación de ICloneable<Base>
? No está del todo claro que deba serlo. ¡La reimplementación de la interfaz de IFoo es una reimplementación de todas las interfaces base de IFoo, pero ICloneable<Base>
no es una interfaz base de ICloneable<Derived>
!
Decir que esta es una reimplementación de la interfaz sería sin duda un tramo; La especificación no lo justifica.
Entonces, ¿qué está pasando aquí?
Lo que está sucediendo aquí es que el tiempo de ejecución debe completar la ranura ICBC. (Como ya hemos dicho, la ranura ICDC claramente debe obtener el método DC). El tiempo de ejecución cree que se trata de una reimplementación de la interfaz, por lo que lo hace mediante la búsqueda de Derivado a Base, y realiza una primera coincidencia. DC es un partido gracias a la varianza, por lo que gana sobre BC.
Ahora bien, puede preguntar dónde se especifica ese comportamiento en la especificación de la CLI, y la respuesta es "en ninguna parte". De hecho, la situación es considerablemente peor que eso; Una lectura cuidadosa de la especificación de CLI muestra, de hecho, que se especifica el comportamiento opuesto . Técnicamente, el CLR no cumple con sus propias especificaciones aquí.
Sin embargo, considere el caso exacto que describe aquí. ¡Es razonable suponer que alguien que llama a ICloneable<Base>.Clone()
en una instancia de Derived quiere volver a obtener un Derived!
Cuando agregamos la varianza a C #, por supuesto, probamos el escenario que mencionas aquí y finalmente descubrimos que el comportamiento era injustificado y deseable. Luego siguió un período de alguna negociación con los encargados de la especificación de la CLI sobre si deberíamos editar o no la especificación de modo que este comportamiento deseable se justifique por la especificación. No recuerdo cuál fue el resultado de esa negociación; Yo no estaba personalmente involucrado en ello.
Entonces, resumiendo:
- De hecho , el CLR realiza una búsqueda de coincidencia de primer ajuste de derivada a base, como si se tratara de una reimplementación de la interfaz.
- De jure , eso no está justificado ni por la especificación C # ni por la especificación CLI.
- No podemos cambiar la implementación sin romper a la gente.
- La implementación de interfaces que unifican las conversiones por desviación es peligrosa y confusa; Intenta evitarlo.
Para otro ejemplo de dónde la unificación de interfaz de variante expone un comportamiento no justificado, dependiente de la implementación en la implementación de "primer ajuste" de CLR, consulte:
Y para un ejemplo en el que la unificación genérica no variante de los métodos de interfaz expone un comportamiento injustificado, dependiente de la implementación en la implementación de "primer ajuste" de CLR, consulte:
http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx
En ese caso, puede causar un cambio en el comportamiento del programa reordenando el texto de un programa, que es realmente extraño en C #.
Me parece que la parte relevante de la especificación sería la que controla cuál de las dos conversiones de referencia implícitas posibles está en juego para la asignación ICloneable<Base> cb = d;
. Las dos opciones, tomadas de la sección 6.1.6, "conversiones de referencia implícitas", son:
- Desde cualquier tipo de clase S a cualquier tipo de interfaz T, siempre que S implemente T.
(En este caso, Derived
implementa ICloneable<Base>
, según la sección 13.4, porque "cuando una clase C implementa directamente una interfaz, todas las clases derivadas de C también implementan implícitamente la interfaz", y Base
implementa directamente ICloneable<Base>
, así que Derived
implementa implícitamente.)
- Desde cualquier tipo de referencia a una interfaz o tipo de delegado T si tiene una identidad implícita o una conversión de referencia a una interfaz o tipo de delegado T0 y T0 es convertible en varianza (§13.1.3.2) a T.
(Aquí, Derived
es implícitamente convertible a ICloneable<Derived>
porque lo implementa directamente, e ICloneable<Derived>
es convertible en varianza a ICloneable<Base>
).
Pero no puedo encontrar ninguna parte de la especificación que se ocupa de desambiguar conversiones de referencia implícitas.
Solo puede tener un significado: el método new public Derived Clone()
implementa tanto ICloneable<Base>
como ICloneable<Derived>
. Solo una llamada explícita a Base.Clone()
llama al método oculto.