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 in
ou 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é T
doit ê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 IList
permet l'ajout ou la suppression d'éléments, toute sous-classe de LifeForm
pourrait 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>
). IEnumerable
n'a pas de méthode à modifier dans la collection, et en raison de la out
covariance, toute collection avec le sous-type de LifeForm
peut maintenant être passée à la méthode:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
peut maintenant être appelé avec Zebras
, Giraffes
et 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 Action
est défini comme Action<in T>
, c'est-à-dire qu'il est contravariant
, ce qui signifie que pour Action<Zebra> myAction
, qui myAction
peut être au plus "a" Action<Zebra>
, mais des superclasses moins dérivées de Zebra
sont é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 Zebra
instance ) à 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 ' Action
est invoqué, c'est l' Zebra
instance la plus dérivée qui est invoquée contre la zebraAction
fonction (passée en paramètre), bien que la fonction elle-même utilise un type moins dérivé.