c# - generic - Ejemplo del mundo real de covarianza y contravarianza
generic delegates in c# (8)
Aquí hay un ejemplo simple usando una jerarquía de herencia.
Covarianza
La covarianza se usa ampliamente con colecciones inmutables (es decir, cuando no se pueden agregar o eliminar elementos nuevos de una colección)
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 { }
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 cual hace)
var myAnimals = new List<LifeForm>
{
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<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<LifeForm>
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 compilador impide pasar List<Giraffe>
vuelve obvia:
public static void PrintLifeForms(IList<LifeForm> 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 compilador nos protege de este peligro.
La solución es garantizar que se use un tipo genérico covariante, por ejemplo, IEnumerable
(definido como IEnumerable<out T>
). 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<LifeForm> 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
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<Zebra>
como parámetro, y lo invoca en una instancia conocida de Zebra:
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 fracasará:
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 Action
se define como Action<in T>
, es decir, es contravariant
.
Aunque esto puede no ser intuitivo al principio (por ejemplo, ¿cómo puede pasar un Action<object>
como un parámetro que requiere Action<Zebra>
?), 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 .
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.
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<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;
}
Digamos que tienes una Persona de clase y una clase que deriva de ella, Maestra. Tiene algunas operaciones que toman como argumento IEnumerable<Person>
. En su clase de la 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). 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<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);
}
}
...
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<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());
El conversor delegado me ayuda a visualizar ambos conceptos trabajando juntos:
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();
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
Las palabras clave de entrada y salida controlan las reglas de conversión del compilador para las interfaces y las delega 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
// 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??
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, 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<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.