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 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.