Diferencia entre covarianza y contravarianza

Respuestas:

266

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 Tigerpuede 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 Xse 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 .

Eric Lippert
fuente
44
Para alguien como yo, hubiera sido mejor agregar ejemplos que muestren lo que NO es covariante y lo que NO es contravariante y lo que NO es ambos.
bjan
2
@Bargitta: es muy similar. La diferencia es que C # usa la varianza del sitio definida y Java usa la varianza del sitio de llamada . Entonces, la forma en que las cosas varían es la misma, pero donde el desarrollador dice "Necesito que esto sea variante" es diferente. Por cierto, ¡la función en ambos idiomas fue diseñada en parte por la misma persona!
Eric Lippert
2
@AshishNegi: Lea la flecha como "puede usarse como". "Una cosa que puede comparar animales puede usarse como una cosa que puede comparar tigres". ¿Tiene sentido ahora?
Eric Lippert
1
@AshishNegi: No, eso no está bien. IEnumerable es covariante porque T solo aparece en los retornos de los métodos de IEnumerable. E IComparable es contravariante porque T solo aparece como parámetros formales de los métodos de IComparable .
Eric Lippert
2
@AshishNegi: Quiere pensar en las razones lógicas que subyacen a estas relaciones. ¿Por qué podemos convertir 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?
Eric Lippert
111

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 Tsolo 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 Tsolo 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 Ten 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 :)

Jon Skeet
fuente
1
Para alguien como yo, hubiera sido mejor agregar ejemplos que muestren lo que NO es covariante y lo que NO es contravariante y lo que NO es ambos.
bjan
1
@Jon Skeet Buen ejemplo, solo no entiendo "un parámetro de tipo Action<T>todavía solo se usa Ten una posición de salida" . Action<T>El tipo de retorno es nulo, ¿cómo se puede usar Tcomo salida? ¿O es eso lo que significa, porque no devuelve nada que pueda ver que nunca puede violar la regla?
Alexander Derck
2
Para mi yo futuro, que vuelve a este excelente respuesta de nuevo para volver a aprender la diferencia, esto es la línea que desea: "[covarianza] funciona porque si sólo está tomando valores de la API, y que va a devolver algo específico (como cadena), puede tratar ese valor devuelto como un tipo más general (como objeto) ".
Matt Klein
La parte más confusa de todo esto es que, ya sea para la covarianza o contravarianza, si ignoras la dirección (hacia adentro o hacia afuera), de todos modos, obtienes una conversión más específica a más genérica. Quiero decir: "puede tratar ese valor devuelto como un tipo más general (como objeto)" para covarianza y: "API está esperando algo general (como objeto) puede darle algo más específico (como cadena)" para contravarianza . ¡Para mí esto suena un poco igual!
XMight
@AlexanderDerck: No estoy seguro de por qué no te respondí antes; Estoy de acuerdo en que no está claro, e intentaré aclararlo.
Jon Skeet
16

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.

Nico
fuente
Usted dice que la anulación de un subtipo puede devolver un tipo más especializado . Pero eso es completamente falso. Si se Birddefine 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.
Suamere
1
Para abreviar: tienes razón! Perdón por la confusion. 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!
Nico
1
Como análogo al comentario de Matt-Klein sobre la respuesta de @ Jon-Skeet, "a mi futuro yo": la mejor conclusión para mí aquí es "Entregan más" (específico) y "Requieren menos" (específico). ¡"Requerir menos y entregar más" es una excelente mnemotecnia! Es análogo a un trabajo donde espero requerir instrucciones menos específicas (solicitudes generales) y, sin embargo, entregar algo más específico (un producto de trabajo real). De cualquier manera, el orden de los subtipos (LSP) no está roto.
karfus
@karfus: Gracias, pero, según recuerdo, parafraseé la idea "Requerir menos y entregar más" de otra fuente. Podría ser que fue el libro de Liu al que me refiero arriba ... o incluso una charla de Rock .NET. Por cierto. en Java, la gente redujo el mnemotécnico a "PECS", que se relaciona directamente con la forma sintáctica de declarar variaciones, PECS es para "Productor extends, Consumidor super".
Nico
5

El delegado convertidor me ayuda a comprender la diferencia.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputrepresenta la covarianza donde un método devuelve un tipo más específico .

TInputrepresenta 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();
woggles
fuente
0

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.

Covarianza

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 Ces una subclase de B, si Aproduce valores genéricos (devuelve como resultado de la función). La covarianza se trata de productores, por eso C # usa la palabra clave outpara 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();
}

Contravarianza

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 Ces subclase de B, si Aconsume valor genérico. La contravarianza se trata de consumidores, por eso C # usa la palabra clave inpara 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());

Enlaces

VadzimV
fuente