Réponses:
La question est "quelle est la différence entre la covariance et la contravariance?"
La covariance et la contravariance sont les propriétés d' une fonction de mappage qui associe un membre d'un ensemble à un autre . Plus spécifiquement, une cartographie peut être covariante ou contravariante 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 de mappage du premier ensemble au second ensemble. Autrement dit, pour chaque T dans le premier ensemble, le type correspondant dans le second ensemble est IEnumerable<T>
. Ou, en bref, la cartographie est T → IE<T>
. Notez qu'il s'agit d'une "flèche fine".
Avec moi si loin?
Considérons maintenant une relation . Il existe une relation de compatibilité d'affectation entre les paires de types du premier ensemble. Une valeur de type Tiger
peut être affectée à une variable de type Animal
, donc ces types sont dits "compatibles avec l'affectation". L'écriture de Let « une valeur de type X
peut être affecté à 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é d'affectation:
Tiger ⇒ Tiger
Tiger ⇒ Animal
Animal ⇒ Animal
Banana ⇒ Banana
Banana ⇒ Fruit
Fruit ⇒ Fruit
En C # 4, qui prend en charge la compatibilité d'affectation de covariantes de certaines interfaces, il existe une relation de compatibilité d'affectation entre les paires de types du 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 la direction de la compatibilité des affectations . Autrement dit, si X ⇒ Y
, alors il est également vrai que IE<X> ⇒ IE<Y>
.
Si nous avons deux choses de chaque côté d'une grosse flèche, alors nous pouvons remplacer les deux côtés par quelque chose sur le côté droit d'une flèche mince correspondante.
Un mappage qui a cette propriété par rapport à une relation particulière est appelé un «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 la covariance. Considérons maintenant ce sous-ensemble de l'ensemble de tous les types:
{ IComparable<Tiger>,
IComparable<Animal>,
IComparable<Fruit>,
IComparable<Banana> }
maintenant nous avons 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 conservé l'existence mais inversé le sens de la compatibilité des affectations. Autrement dit, si X ⇒ Y
, alors IC<X> ⇐ IC<Y>
.
Un mapping qui préserve mais inverse une relation est appelé un mapping 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. La covariance préserve la direction de l'assignabilité. La contravariance l' inverse .
IEnumerable<Tiger>
en IEnumerable<Animal>
toute sécurité? Parce qu'il n'y a aucun moyen d' entrer une girafe dans IEnumerable<Animal>
. Pourquoi pouvons-nous convertir un IComparable<Animal>
en IComparable<Tiger>
? Parce qu'il n'y a aucun moyen de sortir une girafe d'un fichier IComparable<Animal>
. Ça a du sens?
Il est probablement plus facile de donner des exemples - c'est certainement ainsi que je m'en souviens.
Covariance
Exemples: Canonical IEnumerable<out T>
,Func<out T>
Vous pouvez convertir de IEnumerable<string>
vers IEnumerable<object>
ou Func<string>
vers Func<object>
. Les valeurs ne proviennent que de ces objets.
Cela fonctionne parce que si vous ne retirez que des valeurs de l'API et que cela va renvoyer quelque chose de spécifique (comme string
), vous pouvez traiter cette valeur renvoyée comme un type plus général (comme object
).
Contravariance
Exemples: Canonical IComparer<in T>
,Action<in T>
Vous pouvez convertir de IComparer<object>
vers IComparer<string>
ou Action<object>
vers Action<string>
; les valeurs n'entrent que 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>
elle peut être covariante dans T
(c'est- à -dire la déclarer comme IFoo<out T>
si elle T
n'était utilisée que dans une position de sortie (par exemple un type de retour) dans l'interface. Elle peut être contravariante dans T
(c'est- à -dire IFoo<in T>
) si elle T
n'est utilisée que dans une position d'entrée ( par exemple un type de paramètre).
Cela devient potentiellement déroutant parce que la "position de sortie" n'est pas aussi simple que cela en a l'air - un paramètre de type Action<T>
n'utilise toujours que T
dans une position de sortie - la contravariance de Action<T>
faire demi-tour, si vous voyez ce que je veux dire. C'est une "sortie" en ce que 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 le faire. Habituellement, ce genre de chose ne se produit pas, heureusement :)
Action<T>
est toujours utilisé uniquement T
en position de sortie" . Action<T>
le type de retour est void, comment peut-il l'utiliser T
comme sortie? Ou est-ce ce que cela signifie, parce qu'il ne renvoie rien, vous pouvez voir qu'il ne peut jamais enfreindre la règle?
J'espère que mon article vous aidera à avoir une vision indépendante de la langue 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 de type sécurisé 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 à typage statique. (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 un typage statique: si le type Bird pond un œuf, le sous-type de Bird 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? Car dans une telle expression: Egg anEgg = aBird.Lay();
la référence aBird pourrait être légalement remplacée par une instance Bird ou par une instance 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 pianos puissent jouer «de manière cohérente» avec une frappe statique: si un pianiste joue du piano, pourrait-elle jouer un piano à queue? Ne préférerait-il pas qu'un Virtuoso joue un GrandPiano? (Attention, il y a une torsion!) C'est incohérent! Parce que dans une telle expression: aPiano.Play(aPianist);
aPiano ne peut pas être légalement remplacé par un Piano ou par une instance GrandPiano! Un piano à queue ne peut être joué que par un virtuose, les pianistes sont trop généraux! GrandPianos doit être jouable par des types plus généraux, alors le jeu est cohérent. Nous disons que le type de paramètre est contravariant au type dans lequel Play () est défini. Le remplacement d'un sous-type peut accepter un type plus généralisé. => "Ils nécessitent moins."
Retour à C #:
Parce que C # est fondamentalement un langage typé statiquement, les "emplacements" de l'interface d'un type qui devraient être co- ou contravariants (par exemple les paramètres et les types de retour), doivent être marqués explicitement pour garantir une utilisation / développement cohérent de ce type , pour que le LSP fonctionne correctement. Dans les langages à typage dynamique, la cohérence LSP n'est généralement pas un problème, en d'autres termes, vous pourriez vous débarrasser complètement du "balisage" co- et contravariant sur les interfaces et les délégués .Net, si vous n'utilisiez que le type dynamic dans vos types. - Mais ce n'est pas la meilleure solution en C # (vous ne devriez pas utiliser de 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 Emerald et POOL-1). Certains langages oop (par exemple Eiffel) ont décidé d'appliquer un autre type de cohérence, esp. également des types de paramètres covariants, car il décrit mieux la réalité que l'idéal théorique. Dans les langages à typage statique, la cohérence souhaitée doit souvent être obtenue par l'application de 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 les surcharges de fonctions au moment de l'exécution , par exemple avec CLOS) ou d'obtenir l'effet souhaité en utilisant le typage dynamique.
Bird
définit public abstract BirdEgg Lay();
, alors Duck : Bird
DOIT implémenter public override BirdEgg Lay(){}
Donc votre assertion qui BirdEgg anEgg = aBird.Lay();
a une sorte de variance est tout simplement fausse. Étant la prémisse du point de l'explication, le point entier a maintenant disparu. Diriez-vous plutôt que la covariance existe dans l'implémentation où un DuckEgg est implicitement converti en type BirdEgg sortie / retour? Quoi qu'il en soit, veuillez dissiper ma confusion.
DuckEgg Lay()
n'est pas un remplacement valide pour Egg Lay()
en C # , et c'est le point crucial. C # ne prend pas en charge les types de retour covariants, contrairement à Java et C ++. J'ai plutôt décrit l'idéal théorique en utilisant une syntaxe de type C #. En C #, vous devez laisser Bird et Duck implémenter une interface commune, dans laquelle Lay est défini pour avoir un type de retour covariant (c'est-à-dire le hors-spécification), alors les choses s'emboîtent!
extends
, Consumer super
".
Le délégué convertisseur m'aide à comprendre la différence.
delegate TOutput Converter<in TInput, out TOutput>(TInput input);
TOutput
représente la covariance où une méthode renvoie un type plus spécifique .
TInput
représente une contravariance où une méthode reçoit 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();
La variance Co et Contra sont des choses assez logiques. Le système de type de langage nous oblige à prendre en charge la logique de la vie réelle. C'est facile à comprendre par l'exemple.
Par exemple, vous voulez acheter une fleur et vous avez deux magasins de fleurs dans votre ville: un magasin de roses et un magasin de marguerites.
Si vous demandez à quelqu'un "où est le magasin de fleurs?" et quelqu'un vous dit où est le magasin de roses, est-ce que ça ira? Oui, parce que la rose est une fleur, si vous voulez acheter une fleur, vous pouvez acheter une rose. Il en va de même si quelqu'un vous a répondu avec l'adresse du magasin de marguerites.
Ceci est un exemple de covariance : vous êtes autorisé à effectuer un cast A<C>
vers A<B>
, où C
est une sous-classe de B
, si A
produit des valeurs génériques (retourne comme résultat de la fonction). La covariance concerne les producteurs, c'est pourquoi C # utilise un mot-clé out
pour la covariance.
Les types:
class Flower { }
class Rose: Flower { }
class Daisy: Flower { }
interface FlowerShop<out T> where T: Flower {
T getFlower();
}
class RoseShop: FlowerShop<Rose> {
public Rose getFlower() {
return new Rose();
}
}
class DaisyShop: FlowerShop<Daisy> {
public Daisy getFlower() {
return new Daisy();
}
}
La question est "où est le magasin de fleurs?", La réponse est "magasin de roses là-bas":
static FlowerShop<Flower> tellMeShopAddress() {
return new RoseShop();
}
Par exemple, vous voulez offrir une fleur à votre petite amie et votre petite amie aime toutes les fleurs. Pouvez-vous la considérer comme une personne qui aime les roses ou comme une personne qui aime les marguerites? Oui, car si elle aime une fleur, elle adorerait à la fois la rose et la marguerite.
Voici un exemple de contravariance : vous êtes autorisé à effectuer un cast A<B>
vers A<C>
, où C
est la sous-classe de B
, si A
consomme une valeur générique. La contravariance concerne les consommateurs, c'est pourquoi C # utilise le mot-clé in
pour contravariance.
Les types:
interface PrettyGirl<in TFavoriteFlower> where TFavoriteFlower: Flower {
void takeGift(TFavoriteFlower flower);
}
class AnyFlowerLover: PrettyGirl<Flower> {
public void takeGift(Flower flower) {
Console.WriteLine("I like all flowers!");
}
}
Vous considérez votre petite amie qui aime toutes les fleurs comme quelqu'un qui aime les roses et vous lui donnez une rose:
PrettyGirl<Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());