Tengo problemas para comprender la diferencia entre covarianza y contravarianza.
Tengo problemas para comprender la diferencia entre covarianza y contravarianza.
Respuestas:
La pregunta es "¿cuál es la diferencia entre covarianza y contravarianza?"
La covarianza y la contravarianza son propiedades de una función de mapeo que asocia un miembro de un conjunto con otro . Más específicamente, un mapeo puede ser covariante o contravariante con respecto a una relación en ese conjunto.
Considere los siguientes dos subconjuntos del conjunto de todos los tipos de C #. Primero:
{ Animal,
Tiger,
Fruit,
Banana }.
Y segundo, este conjunto claramente relacionado:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
Hay una operación de mapeo del primer conjunto al segundo conjunto. Es decir, para cada T en el primer conjunto, el tipo correspondiente en el segundo conjunto es IEnumerable<T>
. O, en forma corta, el mapeo es T → IE<T>
. Tenga en cuenta que esta es una "flecha delgada".
Conmigo hasta ahora?
Ahora consideremos una relación . Hay una relación de compatibilidad de asignación entre pares de tipos en el primer conjunto. Se Tiger
puede asignar un valor de tipo a una variable de tipo Animal
, por lo que se dice que estos tipos son "compatibles con la asignación". Vamos a escribir "un valor de tipo X
se puede asignar a una variable de tipo Y
" en una forma más corta: X ⇒ Y
. Tenga en cuenta que esta es una "flecha gorda".
Entonces, en nuestro primer subconjunto, aquí están todas las relaciones de compatibilidad de asignación:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
En C # 4, que admite la compatibilidad de asignación covariante de ciertas interfaces, existe una relación de compatibilidad de asignación entre pares de tipos en el segundo conjunto:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
Observe que la asignación T → IE<T>
conserva la existencia y la dirección de la compatibilidad de la asignación . Es decir, si X ⇒ Y
, entonces también es cierto eso IE<X> ⇒ IE<Y>
.
Si tenemos dos cosas a cada lado de una flecha gruesa, entonces podemos reemplazar ambos lados con algo en el lado derecho de una flecha delgada correspondiente.
Una asignación que tiene esta propiedad con respecto a una relación particular se denomina "asignación covariante". Esto debería tener sentido: se puede usar una secuencia de Tigres donde se necesita una secuencia de Animales, pero lo contrario no es cierto. No se puede usar necesariamente una secuencia de animales donde se necesita una secuencia de tigres.
Eso es covarianza. Ahora considere este subconjunto del conjunto de todos los tipos:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
ahora tenemos el mapeo del primer conjunto al tercer conjunto T → IC<T>
.
En C # 4:
IC<Tiger> ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger> Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit> ⇒ IC<Banana> Backwards!
IC<Fruit> ⇒ IC<Fruit>
Es decir, el mapeo T → IC<T>
ha preservado la existencia pero invirtió la dirección de compatibilidad de asignación. Es decir, si X ⇒ Y
, entonces IC<X> ⇐ IC<Y>
.
Un mapeo que conserva pero invierte una relación se llama mapeo contravariante .
De nuevo, esto debería ser claramente correcto. Un dispositivo que puede comparar dos animales también puede comparar dos tigres, pero un dispositivo que puede comparar dos tigres no necesariamente puede comparar dos animales.
Entonces esa es la diferencia entre covarianza y contravarianza en C # 4. La covarianza preserva la dirección de la asignabilidad. La contravarianza lo revierte .
IEnumerable<Tiger>
de IEnumerable<Animal>
forma segura? Porque no hay forma de ingresar una jirafa IEnumerable<Animal>
. ¿Por qué podemos convertir un IComparable<Animal>
a IComparable<Tiger>
? Porque no hay forma de sacar una jirafa de un IComparable<Animal>
. ¿Tener sentido?
Probablemente sea más fácil dar ejemplos, así es como los recuerdo.
Covarianza
Ejemplos canónicas: IEnumerable<out T>
,Func<out T>
Puede convertir de IEnumerable<string>
a IEnumerable<object>
, o Func<string>
a Func<object>
. Los valores solo salen de estos objetos.
Funciona porque si solo está sacando valores de la API, y va a devolver algo específico (como string
), puede tratar ese valor devuelto como un tipo más general (como object
).
Contravarianza
Ejemplos canónicas: IComparer<in T>
,Action<in T>
Puede convertir de IComparer<object>
a IComparer<string>
, o Action<object>
para Action<string>
; los valores solo entran en estos objetos.
Esta vez funciona porque si la API espera algo general (como object
) puede darle algo más específico (como string
).
Más generalmente
Si tiene una interfaz IFoo<T>
, puede ser covariante en T
(es decir, declararla como IFoo<out T>
si T
solo se usara en una posición de salida (por ejemplo, un tipo de retorno) dentro de la interfaz. Puede ser contravariante en T
(es decir IFoo<in T>
) si T
solo se usa en una posición de entrada ( por ejemplo, un tipo de parámetro).
Se vuelve potencialmente confuso porque la "posición de salida" no es tan simple como parece: un parámetro de tipo Action<T>
todavía solo se usa T
en una posición de salida; la contravarianza de lo Action<T>
cambia, si entiendes lo que quiero decir. Es una "salida" porque los valores pueden pasar desde la implementación del método hacia el código de la persona que llama, al igual que un valor de retorno. Por lo general, este tipo de cosas no surgen, afortunadamente :)
Action<T>
todavía solo se usa T
en una posición de salida" . Action<T>
El tipo de retorno es nulo, ¿cómo se puede usar T
como salida? ¿O es eso lo que significa, porque no devuelve nada que pueda ver que nunca puede violar la regla?
Espero que mi publicación ayude a obtener una visión del tema independiente del idioma.
Para nuestras capacitaciones internas, he trabajado con el maravilloso libro "Smalltalk, Objects and Design (Chamond Liu)" y reformulé los siguientes ejemplos.
¿Qué significa "consistencia"? La idea es diseñar jerarquías de tipos de tipo seguro con tipos altamente sustituibles. La clave para obtener esta coherencia es la conformidad basada en subtipos, si trabaja en un lenguaje de tipo estático. (Discutiremos el Principio de sustitución de Liskov (LSP) en un alto nivel aquí).
Ejemplos prácticos (pseudocódigo / inválido en C #):
Covarianza: supongamos que las aves que ponen huevos "consistentemente" con escritura estática: si el tipo Bird pone un huevo, ¿el subtipo de Bird no pondría un subtipo de huevo? Por ejemplo, el tipo Duck pone un DuckEgg, luego se le da la consistencia. ¿Por qué es esto consistente? Porque en esa expresión: Egg anEgg = aBird.Lay();
la referencia aBird podría ser legalmente sustituida por un Bird o por una instancia de Duck. Decimos que el tipo de retorno es covariante para el tipo, en el que se define Lay (). La anulación de un subtipo puede devolver un tipo más especializado. => "Entregan más".
Contravarianza: supongamos que los pianos que los pianistas pueden tocar "consistentemente" con la escritura estática: si un pianista toca el piano, ¿podría tocar un piano de cola? ¿No preferiría un Virtuoso jugar un GrandPiano? (¡Ten cuidado, hay un giro!) ¡Esto es inconsistente! Porque en esa expresión: ¡ aPiano.Play(aPianist);
aPiano no podía ser legalmente sustituido por un Piano o por una instancia de GrandPiano! Un GrandPiano solo puede ser jugado por un Virtuoso, ¡los pianistas son demasiado generales! Los GrandPianos deben ser jugables por tipos más generales, entonces el juego es consistente. Decimos que el tipo de parámetro es contravariante al tipo, en el que se define Play (). La anulación de un subtipo puede aceptar un tipo más generalizado. => "Requieren menos".
Volver a C #:
debido a que C # es básicamente un lenguaje de tipo estático, las "ubicaciones" de la interfaz de un tipo que deben ser co o contravariantes (por ejemplo, parámetros y tipos de retorno), deben marcarse explícitamente para garantizar un uso / desarrollo consistente de ese tipo , para que el LSP funcione bien. En los lenguajes tipados dinámicamente, la consistencia de LSP no suele ser un problema, en otras palabras, puede deshacerse por completo del "marcado" co y contravariante en las interfaces y delegados .Net, si solo utiliza el tipo dinámico en sus tipos. - Pero esta no es la mejor solución en C # (no debe usar la dinámica en las interfaces públicas).
Volver a la teoría:
La conformidad descrita (tipos de retorno covariante / tipos de parámetros contravariantes) es el ideal teórico (respaldado por los lenguajes Emerald y POOL-1). Algunos lenguajes oop (por ejemplo, Eiffel) decidieron aplicar otro tipo de consistencia, especialmente. También tipos de parámetros covariantes, porque describe mejor la realidad que el ideal teórico. En lenguajes tipados estáticamente, la consistencia deseada a menudo se debe lograr mediante la aplicación de patrones de diseño como "doble despacho" y "visitante". Otros lenguajes proporcionan los llamados "despacho múltiple" o métodos múltiples (básicamente se trata de seleccionar sobrecargas de funciones en tiempo de ejecución , por ejemplo, con CLOS) u obtener el efecto deseado mediante el tipeo dinámico.
Bird
define public abstract BirdEgg Lay();
, Duck : Bird
DEBE implementarse. public override BirdEgg Lay(){}
Por lo tanto, su afirmación que BirdEgg anEgg = aBird.Lay();
tiene algún tipo de variación es simplemente falsa. Siendo la premisa del punto de la explicación, todo el punto ya no existe. En su lugar, ¿ diría que existe la covarianza dentro de la implementación donde un DuckEgg se convierte implícitamente en el tipo BirdEgg out / return? De cualquier manera, por favor aclara mi confusión.
DuckEgg Lay()
no es una anulación válida para Egg Lay()
en C # , y ese es el quid. C # no admite tipos de retorno covariantes, pero Java y C ++ sí. Más bien describí el ideal teórico usando una sintaxis similar a C #. En C #, debe dejar que Bird y Duck implementen una interfaz común, en la que Lay se define para tener un tipo de retorno covariante (es decir, fuera de especificación), ¡entonces las cosas encajan!
extends
, Consumidor super
".
El delegado convertidor me ayuda a comprender la diferencia.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
representa la covarianza donde un método devuelve un tipo más específico .
TInput
representa la contravarianza donde se pasa un método de un tipo menos específico .
public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }
public static Poodle ConvertDogToPoodle(Dog dog)
{
return new Poodle() { Name = dog.Name };
}
List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
La variación de Co y Contra son cosas bastante lógicas. El sistema de tipo de lenguaje nos obliga a apoyar la lógica de la vida real. Es fácil de entender con el ejemplo.
Por ejemplo, desea comprar una flor y tiene dos florería en su ciudad: la rosa y la margarita.
Si le preguntas a alguien "¿dónde está la tienda de flores?" y alguien te dice dónde está la tienda de rosas, ¿estaría bien? Sí, porque la rosa es una flor, si quieres comprar una flor puedes comprar una rosa. Lo mismo se aplica si alguien le respondió con la dirección de la tienda de margaritas.
Esto es ejemplo de covarianza : se le permite fundido A<C>
a A<B>
, donde C
es una subclase de B
, si A
produce valores genéricos (devuelve como resultado de la función). La covarianza se trata de productores, por eso C # usa la palabra clave out
para covarianza.
Tipos:
class Flower { }
class Rose: Flower { }
class Daisy: Flower { }
interface FlowerShop<out T> where T: Flower {
T getFlower();
}
class RoseShop: FlowerShop<Rose> {
public Rose getFlower() {
return new Rose();
}
}
class DaisyShop: FlowerShop<Daisy> {
public Daisy getFlower() {
return new Daisy();
}
}
La pregunta es "¿dónde está la florería?", La respuesta es "la tienda de rosas allí":
static FlowerShop<Flower> tellMeShopAddress() {
return new RoseShop();
}
Por ejemplo, desea regalar una flor a su novia y a su novia le gustan las flores. ¿Puedes considerarla como una persona que ama las rosas, o como una persona que ama a las margaritas? Sí, porque si ama cualquier flor, le encantaría tanto la rosa como la margarita.
Este es un ejemplo de la contravarianza : se le permite fundido A<B>
a A<C>
, donde C
es subclase de B
, si A
consume valor genérico. La contravarianza se trata de consumidores, por eso C # usa la palabra clave in
para contravarianza.
Tipos:
interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
void takeGift(TFavoriteFlower flower);
}
class AnyFlowerLover: PrettyGirl<Flower> {
public void takeGift(Flower flower) {
Console.WriteLine("I like all flowers!");
}
}
Estás considerando a tu novia que ama cualquier flor como alguien que ama las rosas y le estás dando una rosa:
PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());