Aquí hay un ejemplo simple usando una jerarquía de herencia.
Dada la jerarquía de clases simple:
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 in
o out
palabras 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 T
debe estar el tipo parametrizado LifeForm
.
Si la implementación del método PrintLifeForms
fue 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 IList
permite agregar o eliminar elementos, cualquier subclase de LifeForm
podrí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 Zebra
a 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>
). IEnumerable
no tiene métodos para cambiar a la colección, y como resultado de la out
covarianza, cualquier colección con subtipo de LifeForm
ahora puede pasarse 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 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 Action
se define como Action<in T>
, es decir contravariant
, significa que para Action<Zebra> myAction
, eso myAction
puede ser "a lo sumo" a Action<Zebra>
, pero Zebra
tambié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 Zebra
instancia ) 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 Action
se invoca, es la Zebra
instancia más derivada la que se invoca contra la zebraAction
función (pasada como parámetro), aunque la función en sí misma usa un tipo menos derivado.