c# c#-4.0 covariance contravariance

c# - Diferencia entre covarianza y contra-varianza



c#-4.0 covariance (6)

El conversor delegado me ayuda a entender la diferencia.

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

TOutput representa covarianza cuando un método devuelve un tipo más específico .

TInput representa la contravariancia cuando un método pasa 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();

Tengo problemas para entender la diferencia entre covarianza y contravarianza.


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

Para nuestros entrenamientos internos 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 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 / inválido en C #):

  • Covarianza: supongamos que las aves que ponen huevos "consistentemente" con el tipado estático: si el tipo de ave pone un huevo, ¿no sería el subtipo de pájaro un subtipo de huevo? Por ejemplo, el tipo Duck establece 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 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 tal expresión: aPiano.Play(aPianist); ¡APiano 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 #:
Como C # es básicamente un lenguaje estáticamente tipado, las "ubicaciones" de la interfaz de un tipo que deben ser covariantes o contravariantes (por ejemplo, parámetros y tipos de devolución), deben marcarse explícitamente para garantizar un uso / desarrollo constante de ese tipo, para hacer que LSP funciona 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 (respaldado por los idiomas Emerald y POOL-1). Algunos lenguajes de oop (por ejemplo, Eiffel) decidieron 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 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 el llamado "envío 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 tipado dinámico.


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 a 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 desde el primer conjunto hasta el segundo conjunto. Es decir, para cada T en el primer conjunto, el tipo correspondiente en el segundo conjunto es IEnumerable<T> . O, en una forma breve, el mapeo es T → IE<T> . Tenga en cuenta que esta es una "flecha delgada".

Conmigo hasta ahora?

Ahora consideremos una relación . Existe una relación de compatibilidad de asignación entre pares de tipos en el primer conjunto. Un valor de tipo Tiger se puede asignar a una variable de 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".

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 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 el mapeo T → IE<T> preserva la existencia y la dirección de la compatibilidad de asignación . Es decir, si X ⇒ Y , 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.

Un mapeo que tiene esta propiedad con respecto a una relación particular se denomina "mapeo 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 desde el primer conjunto hasta el 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 ha invertido la dirección de compatibilidad de asignación. Es decir, si X ⇒ Y , entonces IC<X> ⇐ IC<Y> .

Un mapeo que conserva pero revierte una relación se denomina 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 puede comparar necesariamente dos animales.

Entonces esa es la diferencia entre covarianza y contravarianza en C # 4. La covarianza conserva la dirección de asignabilidad. La contradicción lo revierte .


Mejor si explico por qué la covarianza de tipo de retorno y la contravariancia del parámetro de entrada no están permitidas en C #.

Surgeon, BabySitter y Plumber derivan de la clase Person

public class Surgeon : Person { } public class Plumber : Person { } public class BabySitter : Person { } public class Person { } // We want to restrict performing surgeries only to doctors. public void PerformSurgery(Surgeon doctor) { // If we pass in a Person to PerformSurgery he/she might be a plumber // and we don''t want plumbers performing surgeries doctor.Operate(); } // This also makes sense for fixing pipeleaks // If we pass in a Person to FixPipeLeak he/she might be a babysitter public void FixPipeLeak(Plumber plumber) { plumber.FixPlumbing(); }

PerformSurgery(new Person()) y FixPipeLeak(new Person()) generan un error de compilación. Entonces no queremos contravariancia en nuestros parámetros de entrada

Lo que nosotros queremos

public Person FindNewFriend() { return new Surgeon(); //or return new Plumber(); // or return new BabySitter(); }

No nos importa si la persona es un médico o un fontanero o una niñera, solo queremos tomar una cerveza y pasar un buen rato. Por eso queremos apoyar la contravarianza en nuestros tipos de devolución. Esto compila con éxito

Lo que no queremos

public BabySitter FindSomeoneToBabySit() { // We don''t want just anybody to watch our child // We want an actual babysitter return new Person(); // This generates a compile error // What we want return new BabySitter(); }

Entonces no queremos covarianza de tipo de retorno


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 salen de estos objetos.

Funciona porque si solo está sacando valores de la API y va a devolver algo específico (como una string ), puede tratar ese valor devuelto como un tipo más general (como un object ).

Contravariancia

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 estos objetos.

Esta vez funciona porque si la API espera algo general (como un object ), puede darle algo más específico (como una string ).

Más generalmente

Si tiene una interfaz IFoo<T> puede ser covariante en T (es decir, declararlo como IFoo<out T> si T solo se usa 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 suena: un parámetro de tipo Action<T> todavía solo usa T en una posición de salida; la contravariancia de Action<T> girar, si ver a qué me refiero Es un "resultado" en el sentido de que los valores pueden pasar de la implementación del método al código de la persona que llama, al igual que un valor de retorno puede hacerlo. Por lo general, este tipo de cosas no aparecen, afortunadamente :)