Voici un exemple simple utilisant une hiérarchie d'héritage.
Compte tenu de la hiérarchie de classes simple:

Et en code:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invariance (c.-à-d. Paramètres de type générique * non * décorés avec des mots clés inou out)
Apparemment, une méthode comme celle-ci
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... devrait accepter une collection hétérogène: (ce qu'elle fait)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Cependant, le passage d'une collection d'un type plus dérivé échoue!
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>'
Pourquoi? Parce que le paramètre générique IList<LifeForm>n'est pas covariant -
IList<T>est invariant, donc IList<LifeForm>n'accepte que les collections (qui implémentent IList) où le type paramétré Tdoit être LifeForm.
Si l'implémentation de la méthode de PrintLifeFormsétait malveillante (mais a la même signature de méthode), la raison pour laquelle le compilateur empêche le passage List<Giraffe>devient évidente:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Puisque IListpermet l'ajout ou la suppression d'éléments, toute sous-classe de LifeFormpourrait donc être ajoutée au paramètre lifeForms, et violerait le type de toute collection de types dérivés passés à la méthode. (Ici, la méthode malveillante tenterait d'ajouter un Zebraà var myGiraffes). Heureusement, le compilateur nous protège de ce danger.
Covariance (générique avec type paramétré décoré avec out)
La covariance est largement utilisée avec les collections immuables (c'est-à-dire lorsque de nouveaux éléments ne peuvent pas être ajoutés ou supprimés d'une collection)
La solution à l'exemple ci-dessus est de s'assurer qu'un type de collection générique covariant est utilisé, par exemple IEnumerable(défini comme IEnumerable<out T>). IEnumerablen'a pas de méthode à modifier dans la collection, et en raison de la outcovariance, toute collection avec le sous-type de LifeFormpeut maintenant être passée à la méthode:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeFormspeut maintenant être appelé avec Zebras, Giraffeset n'importe IEnumerable<>quelle sous-classe deLifeForm
Contravariance (générique avec type paramétré décoré avec in)
La contravariance est fréquemment utilisée lorsque les fonctions sont passées en paramètres.
Voici un exemple de fonction, qui prend un Action<Zebra>comme paramètre et l'appelle sur une instance connue d'un Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Comme prévu, cela fonctionne très bien:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuitivement, cela échouera:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Cependant, cela réussit
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
et même cela réussit aussi:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Pourquoi? Parce que Actionest défini comme Action<in T>, c'est-à-dire qu'il est contravariant, ce qui signifie que pour Action<Zebra> myAction, qui myActionpeut être au plus "a" Action<Zebra>, mais des superclasses moins dérivées de Zebrasont également acceptables.
Bien que cela puisse être non intuitif au début (par exemple, comment Action<object>passer un paramètre en tant que paramètre nécessitant Action<Zebra>?), Si vous décompressez les étapes, vous remarquerez que la fonction appelée ( PerformZebraAction) elle-même est responsable de la transmission des données (dans ce cas, une Zebrainstance ) à la fonction - les données ne proviennent pas du code appelant.
En raison de l'approche inversée consistant à utiliser des fonctions d'ordre supérieur de cette manière, au moment où l ' Actionest invoqué, c'est l' Zebrainstance la plus dérivée qui est invoquée contre la zebraActionfonction (passée en paramètre), bien que la fonction elle-même utilise un type moins dérivé.