Différence entre la covariance et la contre-variance

Réponses:

266

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 Tigerpeut ê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 Xpeut ê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 .

Eric Lippert
la source
4
Pour quelqu'un comme moi, il aurait été préférable d'ajouter des exemples montrant ce qui n'est PAS covariant et ce qui n'est PAS contravariant et ce qui n'est PAS les deux.
bjan
2
@Bargitta: C'est très similaire. La différence est que C # utilise une variance de site définie et Java utilise une variance de site d'appel . Donc, la façon dont les choses varient est la même, mais là où le développeur dit "J'ai besoin que ce soit une variante" est différent. Incidemment, la fonctionnalité dans les deux langues a été en partie conçue par la même personne!
Eric Lippert
2
@AshishNegi: Lisez la flèche comme "peut être utilisé comme". "Une chose qui peut comparer les animaux peut être utilisée comme une chose qui peut comparer les tigres". Ça a du sens maintenant?
Eric Lippert
1
@AshishNegi: Non, ce n'est pas juste. IEnumerable est covariant car T apparaît uniquement dans les retours des méthodes de IEnumerable. Et IComparable est contravariant car T n'apparaît que comme paramètres formels des méthodes de IComparable .
Eric Lippert
2
@AshishNegi: Vous voulez réfléchir aux raisons logiques qui sous-tendent ces relations. Pourquoi pouvons-nous convertir 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?
Eric Lippert
111

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 Tn'é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 Tn'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 Tdans 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 :)

Jon Skeet
la source
1
Pour quelqu'un comme moi, il aurait été préférable d'ajouter des exemples montrant ce qui n'est PAS covariant et ce qui n'est PAS contravariant et ce qui n'est PAS les deux.
bjan
1
@Jon Skeet Bel exemple, je ne comprends pas seulement "un paramètre de type Action<T>est toujours utilisé uniquement Ten position de sortie" . Action<T>le type de retour est void, comment peut-il l'utiliser Tcomme 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?
Alexander Derck
2
A mon futur moi, qui revient à cette excellente réponse à nouveau réapprendre la différence, c'est la ligne que vous voulez: « fonctionne [Covariance] parce que si vous êtes seulement prendre des valeurs de l'API, et il va retourner quelque chose spécifique (comme une chaîne), vous pouvez traiter cette valeur renvoyée comme un type plus général (comme un objet). "
Matt Klein
La partie la plus déroutante de tout cela est que pour la covariance ou la contravariance, si vous ignorez la direction (entrée ou sortie), vous obtenez de toute façon une conversion plus spécifique à plus générique! Je veux dire: "vous pouvez traiter cette valeur retournée comme un type plus général (comme un objet)" pour la covariance et: "L'API attend quelque chose de général (comme un objet), vous pouvez lui donner quelque chose de plus spécifique (comme une chaîne)" pour la contravariance . Pour moi, ce son est un peu la même chose!
XMight
@AlexanderDerck: Je ne sais pas pourquoi je ne vous ai pas répondu avant; Je conviens que ce n'est pas clair et je vais essayer de le clarifier.
Jon Skeet
16

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.

Nico
la source
Vous dites que le remplacement d'un sous-type peut renvoyer un type plus spécialisé . Mais c'est complètement faux. Si Birddé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.
Suamere
1
Pour faire court: vous avez raison! Désolé pour la 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!
Nico du
1
Comme un analogue au commentaire de Matt-Klein sur la réponse de @ Jon-Skeet, "à mon futur moi": La meilleure chose à retenir pour moi ici est "Ils livrent plus" (spécifique) et "Ils exigent moins" (spécifique). «Exiger moins et livrer plus» est un excellent mnémonique! C'est analogue à un travail où j'espère exiger des instructions moins spécifiques (demandes générales) tout en livrant quelque chose de plus spécifique (un produit de travail réel). Dans tous les cas, l'ordre des sous-types (LSP) est ininterrompu.
karfus
@karfus: Merci, mais si je me souviens bien, j'ai paraphrasé l'idée "Exiger moins et livrer plus" d'une autre source. Peut-être était-ce le livre de Liu auquel je fais référence ci-dessus ... ou même une conférence .NET Rock. Btw. en Java, les gens ont réduit le mnémonique à "PECS", qui est directement lié à la manière syntaxique de déclarer les variances, PECS est pour "Producer extends, Consumer super".
Nico le
5

Le délégué convertisseur m'aide à comprendre la différence.

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputreprésente la covariance où une méthode renvoie un type plus spécifique .

TInputrepré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();
woggles
la source
0

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.

Covariance

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ù Cest une sous-classe de B, si Aproduit 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é outpour 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();
}

Contravariance

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ù Cest la sous-classe de B, si Aconsomme une valeur générique. La contravariance concerne les consommateurs, c'est pourquoi C # utilise le mot-clé inpour 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());

Liens

VadzimV
la source