Ejemplo de covarianza y contravarianza del mundo real

162

Tengo algunos problemas para entender cómo usaría la covarianza y la contravarianza en el mundo real.

Hasta ahora, los únicos ejemplos que he visto han sido el mismo ejemplo de matriz anterior.

object[] objectArray = new string[] { "string 1", "string 2" };

Sería bueno ver un ejemplo que me permitiera usarlo durante mi desarrollo si pudiera verlo en otro lugar.

Maquinilla de afeitar
fuente
1
Exploro la covarianza en esta respuesta a (mi propia pregunta): tipos de covarianza: por ejemplo . Creo que lo encontrará interesante y, con suerte, instructivo.
Cristian Diaconescu

Respuestas:

109

Digamos que tiene una clase Persona y una clase que se deriva de ella, Maestro. Tienes algunas operaciones que toman IEnumerable<Person>como argumento. En su clase de escuela tiene un método que devuelve un IEnumerable<Teacher>. La covarianza le permite usar directamente ese resultado para los métodos que toman un IEnumerable<Person>, sustituyendo un tipo más derivado por un tipo menos derivado (más genérico). La contravarianza, contra intuitivamente, le permite usar un tipo más genérico, donde se especifica un tipo más derivado.

Consulte también Covarianza y contravarianza en genéricos en MSDN .

clases :

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

Uso :

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());
tvanfosson
fuente
14
@FilipBartuzi: si, como yo cuando escribí esta respuesta, trabajaste en una universidad que es un gran ejemplo del mundo real.
tvanfosson
55
¿Cómo se puede marcar la respuesta cuando no responde la pregunta y no da ningún ejemplo del uso de la varianza co / contra en c #?
barakcaf
@barakcaf agregó un ejemplo de contravarianza. No estoy seguro de por qué no estaba viendo el ejemplo de covarianza, tal vez necesitaba desplazar el código hacia abajo, pero agregué algunos comentarios al respecto.
tvanfosson
@tvanfosson el código usa co / contra, indica que no muestra cómo declararlo. El ejemplo no muestra el uso de entrada / salida en la declaración genérica, mientras que la otra respuesta sí.
barakcaf
Entonces, si lo hago bien, la covarianza es lo que permite el principio de sustitución de Liskov en C #, ¿es correcto?
Miguel Veloso
136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

Por completitud…

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??
Marcelo Cantos
fuente
138
Me gusta este ejemplo realista. Estaba escribiendo un código de burro la semana pasada y estaba muy contento de que ahora tengamos covarianza. :-)
Eric Lippert
44
¡Este comentario anterior con @javadba diciéndole a THE EricLippert qué es covarianza y contravarianza es un ejemplo covariante realista de mí diciéndole a mi abuela cómo chupar huevos! : p
iAteABug_And_iLiked_it
1
La pregunta no preguntaba qué contravarianza y covarianza pueden hacer , sino por qué necesitarías usarla . Su ejemplo está lejos de ser práctico porque tampoco requiere. Puedo crear un QuadrupedGobbler y tratarlo como a sí mismo (asignarlo a IGobbler <Quadruped>) y aún puede engullir Burros (puedo pasar un Burro al método Gobble que requiere un Quadruped). No se necesita contravarianza. Eso está bien que nosotros podemos tratar una QuadrupedGobbler como DonkeyGobbler, pero ¿por qué tenemos que, en este caso, si un QuadrupedGobbler ya se puede engullir burros?
wired_in
1
@wired_in Porque cuando solo te importan los burros, ser más general puede interferir. Por ejemplo, si tiene una granja que suministra burros para ser engullida, puede expresar esto como void feed(IGobbler<Donkey> dg). Si tomaste un IGobbler <Quadruped> como parámetro, no podrías pasar un dragón que solo come burros.
Marcelo Cantos
1
Waaay tarde a la fiesta, pero este es el mejor ejemplo escrito que he visto sobre SO. Tiene sentido completo mientras es ridículo. Voy a tener que mejorar mi juego con respuestas ...
Jesse Williams
120

Esto es lo que armé para ayudarme a entender la diferencia.

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant
CSharper
fuente
10
Esto es lo mejor que he visto hasta ahora que es claro y conciso. ¡Gran ejemplo!
Rob L
66
¿Cómo se puede convertir la fruta en manzana (en el Contravarianceejemplo) cuando Fruites el padre Apple?
Tobias Marschall
@TobiasMarschall eso significa que tienes que estudiar más sobre "polimorfismo"
snr
56

Las palabras clave in y out controlan las reglas de conversión del compilador para interfaces y delegados con parámetros genéricos:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class
Jack
fuente
Suponiendo que el pescado es un subtipo de animal. Gran respuesta por cierto.
Rajan Prasad
48

Aquí hay un ejemplo simple usando una jerarquía de herencia.

Dada la jerarquía de clases simple:

ingrese la descripción de la imagen aquí

Y en código:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Invarianza (es decir, parámetros de tipo genérico * no * decorados con ino outpalabras clave)

Aparentemente, un método como este

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... debería aceptar una colección heterogénea: (lo que hace)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

Sin embargo, pasar una colección de un tipo más derivado falla.

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

¿Por qué? Debido a que el parámetro genérico IList<LifeForm>no es covariante, IList<T>es invariante, por lo que IList<LifeForm>solo acepta colecciones (que implementan IList) donde Tdebe estar el tipo parametrizado LifeForm.

Si la implementación del método PrintLifeFormsfue maliciosa (pero tiene la misma firma de método), la razón por la cual el compilador evita que pase List<Giraffe>es obvia:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Dado que IListpermite agregar o eliminar elementos, cualquier subclase de LifeFormpodría así agregarse al parámetro lifeForms, y violaría el tipo de cualquier colección de tipos derivados pasados ​​al método. (Aquí, el método malicioso intentaría agregar un Zebraa var myGiraffes). Afortunadamente, el compilador nos protege de este peligro.

Covarianza (genérico con tipo parametrizado decorado con out)

La covarianza se usa ampliamente con colecciones inmutables (es decir, cuando no se pueden agregar o eliminar elementos nuevos de una colección)

La solución al ejemplo anterior es garantizar que se use un tipo de colección genérico covariante, por ejemplo IEnumerable(definido como IEnumerable<out T>). IEnumerableno tiene métodos para cambiar a la colección, y como resultado de la outcovarianza, cualquier colección con subtipo de LifeFormahora puede pasarse al método:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormsahora se puede llamar con Zebras, Giraffesy cualquiera IEnumerable<>de las subclases deLifeForm

Contravarianza (Genérico con tipo parametrizado decorado con in)

La contravarianza se usa con frecuencia cuando las funciones se pasan como parámetros.

Aquí hay un ejemplo de una función, que toma un Action<Zebra>como parámetro y lo invoca en una instancia conocida de una cebra:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Como se esperaba, esto funciona bien:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Intuitivamente, esto fallará:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

Sin embargo, esto tiene éxito

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

e incluso esto también tiene éxito:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

¿Por qué? Porque Actionse define como Action<in T>, es decir contravariant, significa que para Action<Zebra> myAction, eso myActionpuede ser "a lo sumo" a Action<Zebra>, pero Zebratambién son aceptables las superclases de menos derivadas .

Aunque esto puede no ser intuitivo al principio (p. Ej., ¿Cómo se Action<object>puede pasar como un parámetro que requiere Action<Zebra>?), Si desempaqueta los pasos, notará que la función llamada ( PerformZebraAction) en sí misma es responsable de pasar los datos (en este caso, una Zebrainstancia ) a la función: los datos no provienen del código de llamada.

Debido al enfoque invertido de usar funciones de orden superior de esta manera, para cuando Actionse invoca, es la Zebrainstancia más derivada la que se invoca contra la zebraActionfunción (pasada como parámetro), aunque la función en sí misma usa un tipo menos derivado.

StuartLC
fuente
77
Esta es una gran explicación para las diferentes opciones de variación, ya que muestra el ejemplo y también aclara por qué el compilador restringe o permite sin las palabras clave de entrada / salida
Vikhram
¿Dónde se inusa la palabra clave para la contravarianza ?
Java
@javadba en lo anterior, Action<in T>y Func<in T, out TResult>son contravariantes en el tipo de entrada. (Mis ejemplos utilizan los tipos existentes invariante (Lista), covariante (IEnumerable) y contravariante (Acción, Func))
StuartLC
Ok, no lo hago C#, no lo sabría.
Java
Es bastante similar en Scala, solo una sintaxis diferente: [+ T] sería covariante en T, [-T] sería contravariante en T, Scala también puede aplicar la restricción 'entre' y la subclase promiscua 'Nada', que C # no tiene
StuartLC
32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

Básicamente, cuando tenía una función que toma un Enumerable de un tipo, no podía pasar un Enumerable de un tipo derivado sin emitirlo explícitamente.

Sin embargo, solo para advertirte sobre una trampa:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

Ese es un código horrible de todos modos, pero existe y el comportamiento cambiante en C # 4 podría introducir errores sutiles y difíciles de encontrar si usa una construcción como esta.

Michael Stum
fuente
Por lo tanto, esto afecta a las colecciones más que nada, porque en c # 3 podría pasar un tipo más derivado a un método de un tipo menos derivado.
Navaja
3
Sí, el gran cambio es que IEnumerable ahora lo admite, mientras que antes no.
Michael Stum
4

De MSDN

El siguiente ejemplo de código muestra el soporte de covarianza y contravarianza para grupos de métodos

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}
Kamran Bigdely
fuente
4

Contravarianza

En el mundo real, siempre puedes usar un refugio para animales en lugar de un refugio para conejos porque cada vez que un refugio de animales alberga un conejo es un animal. Sin embargo, si usa un refugio para conejos en lugar de un refugio para animales, su personal puede ser comido por un tigre.

En el código, esto significa que si usted tiene un IShelter<Animal> animalssimplemente hay que escribir IShelter<Rabbit> rabbits = animals si usted promete y su uso Ten el IShelter<T>sólo como parámetros del método de esta manera:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

y reemplazar un ítem por uno más genérico, es decir, reducir la varianza o introducir contra varianza.

Covarianza

En el mundo real, siempre puedes usar un proveedor de conejos en lugar de un proveedor de animales porque cada vez que un proveedor de conejos te da un conejo, es un animal. Sin embargo, si usa un proveedor de animales en lugar de un proveedor de conejos, puede ser comido por un tigre.

En el código, esto significa que si tiene un ISupply<Rabbit> rabbitsarchivo, simplemente puede escribir ISupply<Animal> animals = rabbits si promete y usar Tel ISupply<T>único método como devolver valores como este:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

y reemplazar un elemento con un más derivada uno, es decir, aumentar la varianza o introducir co varianza.

Con todo, esto es solo una promesa comprobable en tiempo de compilación de usted de que trataría un tipo genérico de cierta manera para mantener la seguridad del tipo y no hacer que coman a nadie.

Es posible que desee leer esto para darle doble vuelta a esto.

Ivan Rybalko
fuente
puede ser comido por un tigre que valía un upvote
javadba
Tu comentario contravariancees interesante. Lo estoy leyendo como indicando un requisito operativo : que el tipo más general debe admitir los casos de uso de todos los tipos derivados de él. Entonces, en este caso, el refugio de animales debe ser capaz de soportar la protección de todo tipo de animales. ¡En ese caso, agregar una nueva subclase podría romper la superclase! Es decir, si agregamos un subtipo Tyrannosaurus Rex , podría destruir nuestro refugio de animales existente .
Java
(Continuado). Eso difiere mucho de la covarianza que se describe claramente estructuralmente : todos los subtipos más específicos admiten las operaciones definidas en el supertipo, pero no necesariamente de la misma manera.
Java
3

El delegado convertidor me ayuda a visualizar ambos conceptos trabajando juntos:

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