2010-02-02 18 views

Respuesta

234

La pregunta es "¿cuál es la diferencia entre covarianza y contravarianza?"

Covarianza y contravarianza son propiedades de una función de asignación 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 C#.Primero:

{ Animal, 
    Tiger, 
    Fruit, 
    Banana }. 

Y en segundo lugar, este conjunto claramente relacionados:

{ IEnumerable<Animal>, 
    IEnumerable<Tiger>, 
    IEnumerable<Fruit>, 
    IEnumerable<Banana> } 

Hay un mapeo operación desde el primer set para el segundo set. Es decir, para cada T en el primer conjunto, el tipo correspondiente en el segundo conjunto es IEnumerable<T>. O, en forma abreviada, 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 puede asignar un valor de tipo Tiger a una variable del tipo Animal, por lo que se dice que estos tipos son "compatibles con la asignación". Escribamos "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 grasa".

Así que 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 soporta la compatibilidad de asignaciones covariante de ciertas interfaces, existe una relación de compatibilidad de asignaciones 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> 

en cuenta que el mapeo T → IE<T>preserva la existencia y la dirección de la compatibilidad asignación. Es decir, si X ⇒ Y, entonces también es cierto que 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 la 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: una secuencia de Tigres se puede usar cuando se necesita una secuencia de Animales, pero lo contrario no es cierto. Una secuencia de animales no necesariamente se puede usar cuando 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 de la primera serie a la tercera serie 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 de T → IC<T> ha conservado la existencia pero invertido la dirección de compatibilidad asignación. Es decir, si X ⇒ Y, entonces IC<X> ⇐ IC<Y>.

Un mapeo que conservas pero invierte una relación se llama un contravariant de mapeo.

Nuevamente, 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 puede comparar necesariamente dos animales.

Así que esa es la diferencia entre la covarianza y la contravarianza en C# 4. La covarianza preserva la dirección de asignación. Contravarianza invierte it.

+2

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

+0

Gracias Eric, parece lo mismo que de Java y . – Bargitta

+0

@Bargitta: Es muy similar. La diferencia es que C# usa * varianza de sitio definida * y Java usa * varianza de sitio de llamada *. Entonces la forma en que las cosas varían es la misma, pero cuando el desarrollador dice "Necesito que esto sea una variante" es diferente. Incidentalmente, ¡la función en ambos idiomas fue en parte diseñada por la misma persona! –

94

Probablemente sea más fácil dar ejemplos: así es como los recuerdo.

covarianza

ejemplos canónicos: IEnumerable<out T>, Func<out T>

Puede convertir de IEnumerable<string> a IEnumerable<object> o Func<string> a Func<object>. Los valores solo vienen desde 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ónicos: IComparer<in T>, Action<in T>

Puede convertir de IComparer<object> a IComparer<string> o Action<object> a Action<string>; los valores solo van a 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> se puede covariante en T (es decir, declarar como IFoo<out T> si T sólo se utiliza en una posición de salida (por ejemplo, un tipo de retorno) dentro de la interfaz. Se . se pueden contravariant en T (es decir IFoo<in T>) si T sólo se utiliza en una posición de entrada (por ejemplo, un tipo de parámetro)

se pone potencialmente confuso porque "posición de salida" no es tan simple como suena - un parámetro de tipo Action<T> sigue utilizando solo T en una posición de salida; la contravariancia de Action<T> la hace girar, si ve lo que quiero decir. Es un "resultado" en el que 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 puede. Por lo general, este tipo de cosas no aparecen, afortunadamente :)

+1

Para alguien como yo, que habría sido mejor para agregar ejemplos que muestran lo que no está covariante y contravariante lo que no es y lo que no es tanto. – bjan

+1

@Jon Skeet Buen ejemplo, solo que no entiendo * "un parámetro de tipo' Acción 'solo sigue usando' T' en una posición de salida "*. 'Acción ' el tipo de retorno es nulo, ¿cómo puede usar 'T' como salida? ¿O es eso lo que significa, porque no devuelve nada, puedes ver que nunca puede violar la regla? –

+1

Para mi yo futuro, que está volviendo a esta excelente respuesta ** nuevamente para volver a aprender la diferencia, esta es la línea que desea: _ "[Covarianza] funciona porque si solo está sacando valores de la API, y va a devolver algo específico (como cadena), puede tratar ese valor devuelto como un tipo más general (como objeto). "_ –

13

Espero que mi publicación ayude a obtener una visión independiente del tema.

Para nuestros entrenamientos internos he trabajado con el maravilloso libro "Smalltalk, Objetos y Diseño (Chamond Liu)" y reformulé los siguientes ejemplos.

¿Qué significa "consistencia"? La idea es diseñar jerarquías tipo seguro con tipos altamente sustituibles. La clave para obtener esta coherencia es la conformidad basada en el subtipo, si trabaja en un lenguaje estáticamente tipado. (Discutiremos el principio de sustitución de liskov (LSP) en un alto nivel aquí.)

Ejemplos prácticos (pseudo código/no válidos en C#):

  • covarianza: Vamos a suponer que las aves que ponen huevos “ consistentemente "con tipificación estática: si el tipo Pájaro pone un huevo, ¿no sería el subtipo de Bird un subtipo de huevo? P.ej. el tipo Duck pone un DuckEgg, luego se da la consistencia. ¿Por qué es esto consistente? Porque en tal expresión: Egg anEgg = aBird.Lay(); la referencia aBird podría ser legalmente sustituida por una instancia de Bird o por una de Duck. Decimos que el tipo de devolución es covariante al 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 pueden tocar "constantemente" con el tipado estático: si un pianista toca el piano, ¿podría tocar un piano de cola? ¿No preferiría un Virtuoso jugar a GrandPiano? (¡Cuidado, hay un giro!) ¡Esto es inconsistente! Porque en una expresión como esta: aPiano.Play(aPianist); ¡un piano no puede ser sustituido legalmente por un piano o por una instancia de GrandPiano! ¡Un Virtuoso solo puede jugar un GrandPiano, los Pianistas son demasiado generales! Los GrandPianos deben poder reproducirse por tipos más generales, luego la jugada 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 tipos estáticos, los "lugares" de la interfaz de un tipo que deben ser co- o contravariante (por ejemplo, parámetros y retorno tipos), debe 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 LSP no suele ser un problema, en otras palabras, usted podría deshacerse por completo del "marcado" co y contravariante en las interfaces .Net y delegados, si solo utilizó 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 covariantes/tipos de parámetros contravariantes) es el ideal teórico (compatible con los idiomas Emerald y POOL-1). Algunos lenguajes de oop (p.Eiffel) decidió aplicar otro tipo de consistencia, esp. también tipos de parámetros covariantes, porque describe mejor la realidad que el ideal teórico. En los idiomas con tipado estático, la consistencia deseada a menudo debe lograrse mediante la aplicación de patrones de diseño como "envío doble" y "visitante". Otros lenguajes proporcionan el llamado "despacho múltiple" o métodos múltiples (esto es básicamente seleccionar sobrecargas de funciones en tiempo de ejecución, por ejemplo, con CLOS) u obtener el efecto deseado mediante el uso de tipeo dinámico.

+0

Dices * La anulación de un subtipo puede devolver un tipo más especializado *. Pero eso es completamente falso. Si 'Bird' define' public abstract BirdEgg Lay(); ', entonces' Duck: Bird' * DEBE * implementar 'public override BirdEgg Lay() {}' Entonces su afirmación de que 'BirdEgg anEgg = aBird.Lay();' tiene algún tipo de variación en absoluto es simplemente falso. Siendo la premisa del punto de la explicación, todo el punto se ha ido. ¿Diría usted * en su lugar * que la covarianza existe dentro de la implementación en la que un DuckEgg se convierte implícitamente en el tipo BirdEgg out/return? De cualquier forma, aclara mi confusión. – Suamere

+1

Para abreviar: ¡tienes razón! Perdón por la confusion. 'DuckEgg Lay()' no es una invalidación válida para 'Egg Lay()' _in C# _, y esa es la clave. C# no admite tipos de retorno covariantes, pero Java así como C++ sí lo hacen. Prefiero describir el ideal teórico utilizando una sintaxis similar a C#. En C#, necesita permitir 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 juntas! – Nico

1

El conversor delegado me ayuda a entender la diferencia.

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

TOutput representa covarianza, donde un método devuelve un tipo más específica.

TInput representa contravarianza, donde se pasa un método 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(); 
Cuestiones relacionadas