J'ai un peu de mal à comprendre comment utiliser la covariance et la contravariance dans le monde réel.
Jusqu'à présent, les seuls exemples que j'ai vus sont les mêmes exemples de tableaux.
object[] objectArray = new string[] { "string 1", "string 2" };
Ce serait bien de voir un exemple qui me permettrait de l'utiliser pendant mon développement si je pouvais le voir utilisé ailleurs.
Disons que vous avez une personne de classe et une classe qui en dérive, Maître. Vous avez des opérations qui prennent un IEnumerable<Person>
comme argument. Dans votre classe d'école, vous avez une méthode qui retourne un IEnumerable<Teacher>
. Covariance vous permet d’utiliser directement ce résultat pour les méthodes qui utilisent un IEnumerable<Person>
, en substituant un type plus dérivé à un type moins dérivé (plus générique). Contravariance, de manière contre-intuitive, vous permet d’utiliser un type plus générique, où un type plus dérivé est spécifié.
Voir aussi Covariance et contravariance dans les génériques sur MSDN .
Classes :
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);
}
}
Utilisation :
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());
// 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();
Pour être complet…
// 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??
Voici ce que j'ai mis en place pour m'aider à comprendre la différence
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
Les mots-clés in et out contrôlent les règles de transtypage du compilateur pour les interfaces et les délégués avec des paramètres génériques:
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
Voici un exemple simple utilisant une hiérarchie d'héritage.
Étant donné la hiérarchie simple des classes:
Et en code:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Invariance (paramètres de type générique * pas * décorée avec in
ou out
mots-clés)
Apparemment, une telle méthode
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 qui est le cas)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Cependant, passer une collection de 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 une covariante - IList<T>
est invariant, donc IList<LifeForm>
accepte uniquement les collections (qui implémentent IList) où le type paramétré T
doit être LifeForm
.
Si la méthode implémentée de PrintLifeForms
était malveillante (mais a la même signature de méthode), le compilateur empêche le passage de List<Giraffe>
devient évident:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Puisque IList
permet d’ajouter ou de supprimer des éléments, toute sous-classe de LifeForm
pourrait ainsi être ajoutée au paramètre lifeForms
et violerait le type de toute collection de types dérivés passée à la méthode. (Ici, la méthode malveillante essaierait 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é de out
)
La covariance est largement utilisée avec des 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 consiste à faire en sorte qu'un type de collection générique covariant soit utilisé, par ex. IEnumerable
(défini comme IEnumerable<out T>
). IEnumerable
ne dispose d'aucune méthode pour modifier la collection et, en conséquence de la covariance out
, toute collection ayant un sous-type de LifeForm
peut maintenant être transmise à 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 quel IEnumerable<>
de toute sous-classe de LifeForm
Contravariance (Générique avec type paramétré décoré de in
)
La contradiction est fréquemment utilisée lorsque des fonctions sont transmises en tant que paramètres.
Voici un exemple de fonction qui prend un Action<Zebra>
en tant que paramètre et l’appelle sur une instance connue d’un zèbre:
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 contravariant
, ce qui signifie que pour Action<Zebra> myAction
, que myAction
peut être au "plus" a Action<Zebra>
, mais les superclasses moins dérivées de Zebra
sont également acceptables.
Bien que cela puisse être initialement non intuitif (par exemple, comment un Action<object>
être passé en tant que paramètre nécessitant Action<Zebra>
?), si vous décompressez les étapes, vous remarquerez que la fonction appelée (PerformZebraAction
) est elle-même responsable de la transmission des données (dans ce cas, une instance Zebra
) à la fonction - la les données ne proviennent pas du code d'appel.
En raison de l'approche inversée consistant à utiliser les fonctions d'ordre supérieur de cette manière, au moment où la Action
est invoquée, c'est l'instance la plus dérivée Zebra
qui est invoquée contre la zebraAction
. function (passé en tant que paramètre), bien que la fonction elle-même utilise un type moins dérivé.
De MSDN
L'exemple de code suivant montre la prise en charge des covariances et des contravariances pour les groupes de méthodes
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;
}
Le délégué du convertisseur m'aide à visualiser les deux concepts travaillant ensemble:
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
représente covariance lorsqu'une méthode retourne un type plus spécifique .
TInput
représente contravariance où une méthode est passée à un type moins spécifique .
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();