c# .net interface covariance contravariance

Comprender las interfaces covariantes y contravariantes en C#



.net covariance (2)

Con <out T> , puede tratar la referencia de interfaz como uno hacia arriba en la jerarquía.

Con <in T> , puede tratar la referencia de la interfaz como uno hacia abajo en la jerarquía.

Déjame intentar explicarlo en más términos ingleses.

Digamos que está recuperando una lista de animales de su zoológico y tiene la intención de procesarlos. Todos los animales (en su zoológico) tienen un nombre y una identificación única. Algunos animales son mamíferos, algunos son reptiles, algunos son anfibios, otros son peces, etc. pero todos son animales.

Entonces, con su lista de animales (que contiene animales de diferentes tipos), puede decir que todos los animales tienen un nombre, por lo que obviamente sería seguro obtener el nombre de todos los animales.

Sin embargo, ¿qué pasa si solo tienes una lista de peces, pero necesitas tratarlos como animales? ¿Funciona? Intuitivamente, debería funcionar, pero en C # 3.0 y antes, este fragmento de código no se compilará:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

La razón de esto es que el compilador no "sabe" lo que pretende o puede hacer con la colección de animales después de que la haya recuperado. Por lo que sabe, podría haber un camino a través de IEnumerable<T> para volver a poner un objeto en la lista, y eso potencialmente le permitiría poner un animal que no sea un pez, en una colección que se supone que contiene solo pescado.

En otras palabras, el compilador no puede garantizar que esto no esté permitido:

animals.Add(new Mammal("Zebra"));

Entonces el compilador simplemente se niega a compilar su código. Esto es covarianza.

Veamos la contravariancia.

Dado que nuestro zoológico puede manejar todos los animales, sin duda puede manejar peces, así que intentemos agregar algunos peces a nuestro zoológico.

En C # 3.0 y anteriores, esto no se compila:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> fishes.Add(new Fish("Guppy"));

Aquí, el compilador podría permitir esta pieza de código, aunque el método devuelve List<Animal> simplemente porque todos los peces son animales, así que si cambiamos los tipos a este:

List<Animal> fishes = GetAccessToFishes(); fishes.Add(new Fish("Guppy"));

Entonces funcionaría, pero el compilador no puede determinar que no estás tratando de hacer esto:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> Fish firstFist = fishes[0];

Como la lista es en realidad una lista de animales, esto no está permitido.

Entonces, contra y co-varianza es la forma en que trata las referencias de objeto y lo que se le permite hacer con ellas.

Las palabras clave de out y out en C # 4.0 marcan específicamente la interfaz como una u otra. Con in , puede colocar el tipo genérico (normalmente T) en las entradas de entrada , lo que significa argumentos de método y propiedades de solo escritura.

Sin out , puede colocar el tipo genérico en las posiciones de salida , que son los valores de retorno del método, las propiedades de solo lectura y los parámetros del método de salida.

Esto le permitirá hacer lo que se pretende hacer con el código:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish> // since we can only get animals *out* of the collection, every fish is an animal // so this is safe

List<T> tiene direcciones de entrada y de salida en T, por lo que no es covariante ni contravariante, sino una interfaz que le permitió agregar objetos, como este:

interface IWriteOnlyList<in T> { void Add(T value); }

te permitiría hacer esto:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns IWriteOnlyList<Animal> fishes.Add(new Fish("Guppy")); <-- this is now safe

Aquí hay algunos videos que muestran los conceptos:

Aquí hay un ejemplo:

namespace SO2719954 { class Base { } class Descendant : Base { } interface IBibbleOut<out T> { } interface IBibbleIn<in T> { } class Program { static void Main(string[] args) { // We can do this since every Descendant is also a Base // and there is no chance we can put Base objects into // the returned object, since T is "out" // We can not, however, put Base objects into b, since all // Base objects might not be Descendant. IBibbleOut<Base> b = GetOutDescendant(); // We can do this since every Descendant is also a Base // and we can now put Descendant objects into Base // We can not, however, retrieve Descendant objects out // of d, since all Base objects might not be Descendant IBibbleIn<Descendant> d = GetInBase(); } static IBibbleOut<Descendant> GetOutDescendant() { return null; } static IBibbleIn<Base> GetInBase() { return null; } } }

Sin estas marcas, lo siguiente podría compilarse:

public List<Descendant> GetDescendants() ... List<Base> bases = GetDescendants(); bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

o esto:

public List<Base> GetBases() ... List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases as Descendants

Me he topado con estos en un libro de texto que estoy leyendo en C #, pero estoy teniendo dificultades para entenderlos, probablemente debido a la falta de contexto.

¿Hay una buena explicación concisa de lo que son y para qué son útiles?

Editar para aclarar:

Interfaz covariante:

interface IBibble<out T> . .

Interfaz contravariante:

interface IBibble<in T> . .


Esta publicación es la mejor que he leído sobre el tema

En resumen, la covarianza / contravariancia / invariancia se refiere a la conversión automática de tipos (de base a derivada y viceversa). Esos lanzamientos de tipos son posibles solo si se respetan algunas garantías en términos de acciones de lectura / escritura realizadas en los objetos lanzados. Lee la publicación para más detalles.