J'ai du mal à comprendre la différence entre covariance et contravariance.
La question est "quelle est la différence entre covariance et contravariance?"
La covariance et la contravariance sont des propriétés de ne fonction de mappage qui associe un membre d'un ensemble à un autre. Plus précisément, un mappage peut être covariant ou contravariant par rapport à une relation sur cet ensemble.
Considérez les deux sous-ensembles suivants de l'ensemble de tous les types C #. Première:
{ Animal,
Tiger,
Fruit,
Banana }.
Et deuxièmement, cet ensemble clairement lié:
{ IEnumerable<Animal>,
IEnumerable<Tiger>,
IEnumerable<Fruit>,
IEnumerable<Banana> }
Il y a une opération mapping du premier ensemble au deuxième ensemble. Autrement dit, pour chaque T dans le premier ensemble, le type correspondant dans le deuxième ensemble est IEnumerable<T>
. Ou, en bref, le mappage est T → IE<T>
. Notez qu'il s'agit d'une "flèche fine".
Jusqu'à présent avec moi?
Considérons maintenant une relation. Il existe une relation de compatibilité d'attribution entre les paires de types dans le premier ensemble. Une valeur de type Tiger
peut être affectée à une variable de type Animal
, donc ces types sont dits "compatibles avec les affectations". Écrivons "une valeur de type X
peut être affectée à une variable de type Y
" sous une forme plus courte: X ⇒ Y
. Notez qu'il s'agit d'une "grosse flèche".
Donc, dans notre premier sous-ensemble, voici toutes les relations de compatibilité des affectations:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
Dans C # 4, qui prend en charge la compatibilité d'attribution covariante de certaines interfaces, il existe une relation de compatibilité d'attribution entre les paires de types dans le deuxième ensemble:
IE<Tiger> ⇒ IE<Tiger>
IE<Tiger> ⇒ IE<Animal>
IE<Animal> ⇒ IE<Animal>
IE<Banana> ⇒ IE<Banana>
IE<Banana> ⇒ IE<Fruit>
IE<Fruit> ⇒ IE<Fruit>
Notez que le mappage T → IE<T>
préserve l'existence et le sens de la compatibilité des affectations. Autrement dit, si X ⇒ Y
, il est également vrai que IE<X> ⇒ IE<Y>
.
Si nous avons deux choses de chaque côté d'une grosse flèche, nous pouvons remplacer les deux côtés par quelque chose sur le côté droit d'une fine flèche correspondante.
Un mappage qui a cette propriété par rapport à une relation particulière est appelé "mappage covariant". Cela devrait avoir du sens: une séquence de tigres peut être utilisée lorsqu'une séquence d'animaux est nécessaire, mais l'inverse n'est pas vrai. Une séquence d'animaux ne peut pas nécessairement être utilisée lorsqu'une séquence de tigres est nécessaire.
C'est de la covariance. Considérez maintenant ce sous-ensemble de l'ensemble de tous les types:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
nous avons maintenant le mappage du premier ensemble au troisième ensemble T → IC<T>
.
En C # 4:
IC<Tiger> ⇒ IC<Tiger>
IC<Animal> ⇒ IC<Tiger> Backwards!
IC<Animal> ⇒ IC<Animal>
IC<Banana> ⇒ IC<Banana>
IC<Fruit> ⇒ IC<Banana> Backwards!
IC<Fruit> ⇒ IC<Fruit>
Autrement dit, le mappage T → IC<T>
a préservé l'existence mais inversé le sens de compatibilité d'affectation. Autrement dit, si X ⇒ Y
, puis IC<X> ⇐ IC<Y>
.
Un mappage qui préserve mais inverse une relation est appelé un mappage contravariant.
Encore une fois, cela devrait être clairement correct. Un appareil qui peut comparer deux animaux peut également comparer deux tigres, mais un appareil qui peut comparer deux tigres ne peut pas nécessairement comparer deux animaux.
Voilà donc la différence entre la covariance et la contravariance en C # 4. Covariance préserve la direction de l'assignation. Contravariance inverse it.
Il est probablement plus facile de donner des exemples - c'est certainement ainsi que je m'en souviens.
Covariance
Exemples canoniques: IEnumerable<out T>
, Func<out T>
Vous pouvez convertir à partir de IEnumerable<string>
à IEnumerable<object>
, ou Func<string>
à Func<object>
. Les valeurs proviennent uniquement de ces objets.
Cela fonctionne parce que si vous ne retirez que des valeurs de l'API et que cela va retourner quelque chose de spécifique (comme string
), vous pouvez traiter cette valeur retournée comme un type plus général (comme object
).
Contravariance
Exemples canoniques: IComparer<in T>
, Action<in T>
Vous pouvez convertir à partir de IComparer<object>
à IComparer<string>
, ou Action<object>
à Action<string>
; les valeurs vont seulement dans ces objets.
Cette fois, cela fonctionne car si l'API attend quelque chose de général (comme object
), vous pouvez lui donner quelque chose de plus spécifique (comme string
).
Plus généralement
Si vous avez une interface IFoo<T>
il peut être covariant dans T
(c'est-à-dire le déclarer comme IFoo<out T>
si T
n'est utilisé que dans une position de sortie (par exemple un type de retour) dans l'interface. Il peut être contravariant dans T
(c'est-à-dire IFoo<in T>
) si T
n'est utilisé que dans une position d'entrée (par exemple un type de paramètre).
Cela devient potentiellement déroutant car la "position de sortie" n'est pas aussi simple qu'il y paraît - un paramètre de type Action<T>
n'utilise toujours que T
dans une position de sortie - la contravariance de Action<T>
le retourne, si vous voyez ce que je veux dire. C'est une "sortie" dans la mesure où les valeurs peuvent passer de l'implémentation de la méthode vers le code de l'appelant, tout comme une valeur de retour peut. Habituellement, ce genre de chose ne se présente pas, heureusement :)
J'espère que mon message aide à obtenir une vue indépendante du langage du sujet.
Pour nos formations internes, j'ai travaillé avec le merveilleux livre "Smalltalk, Objects and Design (Chamond Liu)" et j'ai reformulé les exemples suivants.
Que signifie "cohérence"? L'idée est de concevoir des hiérarchies de types sécurisées avec des types hautement substituables. La clé pour obtenir cette cohérence est la conformité basée sur les sous-types, si vous travaillez dans un langage typé statiquement. (Nous discuterons du principe de substitution de Liskov (LSP) à un niveau élevé ici.)
Exemples pratiques (pseudo code/invalide en C #):
Covariance: Supposons que les oiseaux pondent des œufs "de manière cohérente" avec une saisie statique: si le type oiseau pond un œuf, le sous-type d'oiseau ne pondrait-il pas un sous-type d'oeuf? Par exemple. le type Duck pose un DuckEgg, puis la cohérence est donnée. Pourquoi est-ce cohérent? Parce que dans une telle expression: Egg anEgg = aBird.Lay();
la référence aBird pourrait être légalement substituée par un Bird ou par une instance de Duck. Nous disons que le type de retour est covariant au type dans lequel Lay () est défini. Le remplacement d'un sous-type peut renvoyer un type plus spécialisé. => "Ils livrent plus."
Contravariance: Supposons que les pianistes puissent jouer "régulièrement" avec la frappe statique: si un pianiste joue du piano, serait-elle capable de jouer un grand piano? Un Virtuose ne jouerait-il pas plutôt un GrandPiano? (Attention, il y a une torsion!) C'est incohérent! Parce que dans une telle expression: aPiano.Play(aPianist);
aPiano ne pouvait pas être légalement substitué par un Piano ou par une instance de GrandPiano! Un GrandPiano ne peut être joué que par un Virtuose, les pianistes sont trop généraux! Les pianos à queue doivent être jouables par des types plus généraux, alors le jeu est cohérent. Nous disons que le type de paramètre est contraire au type, dans lequel Play () est défini. La substitution d'un sous-type peut accepter un type plus généralisé. => "Ils nécessitent moins."
Retour en C #:
. pour faire fonctionner correctement le LSP. Dans les langages typés dynamiquement, la cohérence LSP n'est généralement pas un problème, en d'autres termes, vous pouvez complètement vous débarrasser du "balisage" co et contravariant sur les interfaces et les délégués .Net, si vous n'utilisez que le type dynamic dans vos types. - Mais ce n'est pas la meilleure solution en C # (vous ne devriez pas utiliser dynamique dans les interfaces publiques).
Retour à la théorie:
La conformité décrite (types de retour covariants/types de paramètres contravariants) est l'idéal théorique (supporté par les langages Emeraude et POOL-1). Certaines langues oop (par exemple Eiffel) ont décidé d'appliquer un autre type de cohérence, en particulier. également des types de paramètres covariants, car il décrit mieux la réalité que l'idéal théorique. Dans les langues typées statiquement, la cohérence souhaitée doit souvent être obtenue en appliquant des modèles de conception tels que "double répartition" et "visiteur". D'autres langages proposent des méthodes dites de "répartition multiple" ou multi (il s'agit essentiellement de sélectionner des surcharges de fonction à au moment de l'exécution, par exemple avec CLOS) ou d'obtenir l'effet souhaité en utilisant le typage dynamique.
Si vous souhaitez affecter une méthode à un délégué, la signature de la méthode doit correspondre exactement à la signature du délégué. Cela dit, la covariance et la contravariance permettent une certaine souplesse pour faire correspondre la signature des méthodes à celle des délégués.
Vous pouvez vous référer à ceci article pour comprendre la covariance, la contravariance et les différences entre eux .
Le délégué du convertisseur m'aide à comprendre la différence.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
représente covariance où une méthode retourne un type plus spécifique .
TInput
représente contravariance où une méthode est passée d'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();