Comprensión de las interfaces covariantes y contravariantes en C #

86

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

¿Existe una buena explicación concisa de qué son y para qué sirven?

Edite para aclarar:

Interfaz covariante:

interface IBibble<out T>
.
.

Interfaz contravariante:

interface IBibble<in T>
.
.
NibblyPig
fuente
3
En mi humilde opinión
digEmAll
Puede ser útil:
Entrada de
Hmm, es bueno, pero no explica por qué, que es lo que realmente me desconcierta.
NibblyPig

Respuestas:

144

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

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

Voy a intentar explicarlo en términos más ingleses.

Supongamos 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, algunos 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é sucede si solo tiene una lista de peces, pero necesita 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 haberla recuperado. Por lo que sabe, podría haber una forma IEnumerable<T>de volver a poner un objeto en la lista, y eso podría permitirle poner un animal que no sea un pez, en una colección que se supone que contiene solo peces.

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

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

Así que el compilador simplemente se niega a compilar su código. Esta es la covarianza.

Veamos la contravarianza.

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

En C # 3.0 y antes, 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 este fragmento de código, aunque el método devuelve List<Animal>simplemente porque todos los peces son animales, así que si cambiamos los tipos a esto:

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

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

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

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

Entonces, la contravarianza y la covarianza es cómo se tratan las referencias de objetos y qué se le permite hacer con ellas.

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

Con out, se le permite colocar el tipo genérico en posiciones de salida , que son valores de retorno de método, propiedades de solo lectura y parámetros de método de salida.

Esto le permitirá hacer lo que pretendía 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 salida en T, por lo que no es ni covariante ni contravariante, sino una interfaz que le permite 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:

He aquí 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, se podría compilar lo siguiente:

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
Lasse V. Karlsen
fuente
Hmm, ¿podrías explicar el objetivo de la covarianza y la contravarianza? Podría ayudarme a entenderlo más.
NibblyPig
1
Vea el último bit, que es lo que el compilador evitó antes, el propósito de in y out es decir qué puede hacer con las interfaces (o tipos) que son seguras, para que el compilador no le impida hacer cosas seguras .
Lasse V. Karlsen
Excelente respuesta, vi los videos, fueron muy útiles y, combinados con su ejemplo, lo tengo ordenado ahora. Solo queda una pregunta, y es por qué se requieren 'fuera' e 'dentro', ¿por qué Visual Studio no sabe automáticamente lo que está tratando de hacer (o cuál es la razón detrás de esto)?
NibblyPig
Automagic "Veo lo que estás tratando de hacer allí" generalmente está mal visto cuando se trata de declarar cosas como clases, es mejor que el programador marque explícitamente los tipos. Puede intentar agregar "in" a una clase que tenga métodos que devuelvan T y el compilador se quejará. Imagínese lo que sucedería si eliminara silenciosamente la "entrada" que previamente había agregado automáticamente.
Lasse V. Karlsen
1
Si una palabra clave necesita esta larga explicación, es evidente que algo está mal. En mi opinión, C # está tratando de ser demasiado inteligente en este caso particular. No obstante, gracias por una explicación genial.
rr-
7

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

En resumen, la covarianza / contravarianza / invariancia se ocupa de la conversión automática de tipos (de base a derivada y viceversa). Estos tipos de conversiones solo son posibles si se respetan algunas garantías en términos de acciones de lectura / escritura realizadas en los objetos transmitidos. Lea la publicación para obtener más detalles.

Ben G
fuente
5
Link parece muerto. Aquí hay una versión archivada: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618
1
Me
volkit