Ejemplo del mundo real de covarianza y contravarianza

Me cuesta un poco 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 antiguo ejemplo de matriz.

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

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

Digamos que tienes una Persona de clase y una clase que deriva de ella, Maestra. Tiene algunas operaciones que toman como argumento IEnumerable . En su clase de la escuela tiene un método que devuelve un IEnumerable . La covarianza le permite usar directamente ese resultado para los métodos que toman un IEnumerable , sustituyendo un tipo más derivado por un tipo menos derivado (más genérico). Contravarianza, de manera intuitiva, le permite usar un tipo más genérico, donde se especifica un tipo más derivado. Ver también https://msdn.microsoft.com/en-us/library/dd799517.aspx

 public class Person { public string Name { get; set; } } public class Teacher : Person { } public class MailingList { public void Add(IEnumerable people) { ... } } public class School { public IEnumerable GetTeachers() { ... } } public class PersonNameComparer : IComparer { 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); } } ... var teachers = school.GetTeachers(); var mailingList = new MailingList(); // Add() is covariant, we can use a more derived type mailingList.Add(teachers); // the Set constructor uses a contravariant interface, IComparer, // 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, new PersonNameComparer()); 
 // Contravariance interface IGobbler { void gobble(T t); } // Since a QuadrupedGobbler can gobble any four-footed // creature, it is OK to treat it as a donkey gobbler. IGobbler dg = new QuadrupedGobbler(); dg.gobble(MyDonkey()); // Covariance interface ISpewer { T spew(); } // A MouseSpewer obviously spews rodents (all mice are // rodents), so we can treat it as a rodent spewer. ISpewer rs = new MouseSpewer(); Rodent r = rs.spew(); 

Por completitud…

 // Invariance interface IHat { void hide(T t); T pull(); } // A RabbitHat… IHat rHat = RabbitHat(); // …cannot be treated covariantly as a mammal hat… IHat 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 cHat = rHat; // Compiler error // …because… rHat.hide(new MarshRabbit()); cHat.pull(); // Pull a marsh rabbit out of a cottontail hat?? 

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

 public interface ICovariant { } public interface IContravariant { } public class Covariant : ICovariant { } public class Contravariant : IContravariant { } public class Fruit { } public class Apple : Fruit { } public class TheInsAndOuts { public void Covariance() { ICovariant fruit = new Covariant(); ICovariant apple = new Covariant(); Covariant(fruit); Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile } public void Contravariance() { IContravariant fruit = new Contravariant(); IContravariant apple = new Contravariant(); Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile Contravariant(apple); } public void Covariant(ICovariant fruit) { } public void Contravariant(IContravariant apple) { } } 

tldr

 ICovariant apple = new Covariant(); //because it's covariant IContravariant fruit = new Contravariant(); //because it's contravariant 

Las palabras clave de entrada y salida controlan las reglas de conversión del comstackdor para las interfaces y las delega con parámetros generics:

 interface IInvariant { // This interface can not be implicitly cast AT ALL // Used for non-readonly collections IList GetList { get; } // Used when T is used as both argument *and* return type T Method(T argument); }//interface interface ICovariant { // This interface can be implicitly cast to LESS DERIVED (upcasting) // Used for readonly collections IEnumerable GetList { get; } // Used when T is used as return type T Method(); }//interface interface IContravariant { // 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 invariantAnimal; ICovariant covariantAnimal; IContravariant contravariantAnimal; IInvariant invariantFish; ICovariant covariantFish; IContravariant 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 : ICollection, IEnumerable, IEnumerable { } public interface IEnumerable : IEnumerable { } class Delegates { // When T is used as both "in" (argument) and "out" (return value) delegate T Invariant(T argument); // When T is used as "out" (return value) only delegate T Covariant(); // When T is used as "in" (argument) only delegate void Contravariant(T argument); // Confusing delegate T CovariantBoth(T argument); // Confusing delegate T ContravariantBoth(T argument); // From .NET Framework: public delegate void Action(T obj); public delegate TResult Func(T arg); }//class 
 class A {} class B : A {} public void SomeFunction() { var someListOfB = new List(); someListOfB.Add(new B()); someListOfB.Add(new B()); someListOfB.Add(new B()); SomeFunctionThatTakesA(someListOfB); } public void SomeFunctionThatTakesA(IEnumerable input) { // Before C# 4, you couldn't pass in List: // cannot convert from // 'System.Collections.Generic.List' to // 'System.Collections.Generic.IEnumerable' } 

Básicamente, cada vez que 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.

Solo para advertirte sobre una trampa:

 var ListOfB = new List(); if(ListOfB is IEnumerable) { // In C# 4, this branch will // execute... Console.Write("It is A"); } else if (ListOfB is IEnumerable) { // ...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.

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

Dada la jerarquía de clases simple:

  Giraffe / LifeForm < - Animal <- \ Zebra 

En codigo:

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

Invarianza (Genérico con tipo parametrizado decorado con ni in ni out )

Aparentemente, un método como este

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

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

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

Sin embargo, se produce un error al pasar una colección de un tipo más derivado .

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

cannot convert from 'System.Collections.Generic.List' to 'System.Collections.Generic.IList'

¿Por qué? Debido a que el parámetro genérico IList no es covariante - IList es invariante, y solo acepta colecciones (que implementan IList) donde el tipo parametrizado T debe ser LifeForm .

Si PrintLifeForms maliciosamente la implementación del método de PrintLifeForms (pero dejo la misma firma de método), la razón por la que el comstackdor impide pasar List vuelve obvia:

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

Como IList permite agregar o eliminar elementos, cualquier subclase de LifeForm podría agregarse al parámetro lifeForms y violaría el tipo de cualquier colección de tipos derivados que se pasen al método. (Aquí, el método malicioso intentaría agregar un Zebra a var myGiraffes ). Afortunadamente, el comstackdor nos protege de este peligro.

Covarianza (Genérico con tipo parametrizado decorado sin 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 utiliza un tipo genérico covariante, por ejemplo, IEnumerable (definido como IEnumerable ). Esto impide el cambio a la colección, y como resultado, cualquier colección con subtipo de LifeForm ahora se puede pasar al método:

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

PrintLifeForms() ahora se puede llamar con Zebras , Giraffes y cualquier IEnumerable<> de cualquier subclase de LifeForm

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

La contradicción se usa frecuentemente cuando las funciones se pasan como parámetros.

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

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

Como se esperaba, esto funciona bien:

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

Intuitivamente, esto fracasará:

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

cannot convert from 'System.Action' to 'System.Action'

Sin embargo, esto tiene éxito

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

e incluso esto también tiene éxito:

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

¿Por qué? Porque Action se define como Action , es decir, es contravariant .

Aunque esto puede no ser intuitivo al principio (por ejemplo, ¿cómo puede pasar un Action como un parámetro que requiere Action ?), Si descomprime los pasos, notará que la función llamada ( PerformZebraAction ) es responsable para pasar datos (en este caso, una instancia de Zebra ) 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, en el momento en que se invoca la Action , es la instancia de objeto más derivada la que se invoca contra la función zebraAction (pasada como un parámetro), que usa el tipo menos derivado .

Desde 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 del = GetString; // Contravariance. A delegate specifies a parameter type as string, // but you can assign a method that takes an object. Action del2 = SetObject; } 

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

 delegate TOutput Converter(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 dogs = new List() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } }; List poodles = dogs.ConvertAll(new Converter(ConvertDogToPoodle)); poodles[0].DoBackflip(); 

La covarianza y la contravarianza proporcionan cierta flexibilidad en el código.

 using System; // ... class Program { static void Main(string[] args) { Console.ForegroundColor = ConsoleColor.Cyan; ReturnPersonDelegate returnPersonDelegate = ReturnPersonMethod; Employee employee = (Employee)returnPersonDelegate(); Console.WriteLine(employee._whatHappened); EmployeeParameterDelegate employeeParameterDelegate = EmployeeParameterMethod; employeeParameterDelegate(new Employee()); Console.ForegroundColor = ConsoleColor.Gray; Console.Write("Press any key to quit . . . "); Console.ReadKey(true); } delegate Person ReturnPersonDelegate(); // Covariance allows derived class as its return value delegate void EmployeeParameterDelegate(Employee employee); // Contravariance allows base class as its parameters static Employee ReturnPersonMethod() { Employee employee = new Employee(); employee._whatHappened = employee + ": Covariance"; return employee; } static void EmployeeParameterMethod(Person person) { person._whatHappened = ": Contravariance"; Console.WriteLine(person + person._whatHappened); } } class Person { public string _whatHappened; } class Employee : Person { }