Covariance des parameters de type générique et implémentations d’interfaces multiples

Si j’ai une interface générique avec un paramètre de type covariant, comme ceci:

interface IGeneric { ssortingng GetName(); } 

Et si je définis cette hiérarchie de classe:

 class Base {} class Derived1 : Base{} class Derived2 : Base{} 

Ensuite, je peux implémenter l’interface deux fois sur une seule classe, comme celle-ci, en utilisant une implémentation d’interface explicite:

 class DoubleDown: IGeneric, IGeneric { ssortingng IGeneric.GetName() { return "Derived1"; } ssortingng IGeneric.GetName() { return "Derived2"; } } 

Si j’utilise la classe DoubleDown (non générique) et que je la IGeneric en IGeneric ou IGeneric elle fonctionne comme prévu:

 var x = new DoubleDown(); IGeneric id1 = x; //cast to IGeneric Console.WriteLine(id1.GetName()); //Derived1 IGeneric id2 = x; //cast to IGeneric Console.WriteLine(id2.GetName()); //Derived2 

Cependant, en IGeneric le x en IGeneric , vous IGeneric le résultat suivant:

 IGeneric b = x; Console.WriteLine(b.GetName()); //Derived1 

Je m’attendais à ce que le compilateur émette une erreur, car l’appel est ambigu entre les deux implémentations, mais il a renvoyé la première interface déclarée.

Pourquoi est-ce autorisé?

(inspiré par une classe mettant en œuvre deux IObservables différents?. J’ai essayé de montrer à un collègue que cela échouerait, mais d’une certaine manière, cela n’a pas été le cas)

Le compilateur ne peut pas lancer d’erreur sur la ligne

 IGeneric b = x; Console.WriteLine(b.GetName()); //Derived1 

car il n’y a pas d’ambiguïté que le compilateur puisse connaître. GetName() est en fait une méthode valide sur l’interface IGeneric . Le compilateur ne suit pas le temps d’exécution de b pour savoir qu’il existe un type qui pourrait créer une ambiguïté. C’est donc à l’exécution de décider quoi faire. Le runtime pourrait lancer une exception, mais les concepteurs du CLR ont apparemment décidé de ne pas le faire (ce que j’estime être une bonne décision).

En d’autres termes, disons que vous aviez simplement écrit la méthode:

 public void CallIt(IGeneric b) { ssortingng name = b.GetName(); } 

et vous ne fournissez aucune classe implémentant IGeneric dans votre assembly. Vous dissortingbuez cela et beaucoup d’autres implémentent cette interface une seule fois et sont capables d’appeler votre méthode correctement. Cependant, quelqu’un consum éventuellement votre assembly et crée la classe DoubleDown et la transmet à votre méthode. À quel moment le compilateur doit-il lancer une erreur? L’assembly déjà compilé et dissortingbué contenant l’appel à GetName() ne peut GetName() pas générer d’erreur de compilation. Vous pourriez dire que l’atsortingbution de DoubleDown à IGeneric produit l’ambiguïté. mais encore une fois, nous pourrions append un autre niveau d’indirection dans l’assemblage d’origine:

 public void CallItOnDerived1(IGeneric b) { return CallIt(b); //b will be cast to IGeneric } 

Encore une fois, de nombreux consommateurs peuvent appeler CallIt ou CallItOnDerived1 et se contenter de cela. Mais notre client qui passe DoubleDown également un appel parfaitement légal qui ne pourrait pas provoquer d’erreur de compilation quand il appelle CallItOnDerived1 car la conversion de DoubleDown à IGeneric devrait certainement être correcte. Par conséquent, le compilateur ne peut en aucun cas DoubleDown une erreur autre que la définition de DoubleDown , mais cela éliminerait la possibilité de faire quelque chose de potentiellement utile sans solution de contournement.

J’ai en fait répondu à cette question plus en profondeur ailleurs, et j’ai fourni une solution potentielle si le langage pouvait être modifié:

Aucun avertissement ou erreur (ou échec d’exécution) lorsque la contravariance entraîne une ambiguïté

Étant donné que la probabilité que la langue change pour le supporter est pratiquement nulle, je pense que le comportement actuel est correct, sauf qu’il devrait être présenté dans les spécifications pour que toutes les implémentations du CLR se comportent de la même manière.

Si vous avez testé les deux:

 class DoubleDown: IGeneric, IGeneric { ssortingng IGeneric.GetName() { return "Derived1"; } ssortingng IGeneric.GetName() { return "Derived2"; } } class DoubleDown: IGeneric, IGeneric { ssortingng IGeneric.GetName() { return "Derived1"; } ssortingng IGeneric.GetName() { return "Derived2"; } } 

Vous devez avoir déjà réalisé que le résultat dans la réalité, change avec l’ordre dans lequel vous déclarez les interfaces à mettre en œuvre . Mais je dirais que ce n’est pas précisé .

Tout d’abord, la spécification (mappage d’interface §13.4.4) dit:

  • Si plusieurs membres correspondent, il n’est pas précisé quel membre est l’implémentation de la messagerie instantanée.
  • Cette situation ne peut se produire que si S est un type construit où les deux membres déclarés dans le type générique ont des signatures différentes , mais les arguments de type rendent leurs signatures identiques.

Nous avons ici deux questions à considérer:

  • Q1: Vos interfaces génériques ont-elles des signatures différentes ?
    A1: Oui Ils sont IGeneric et IGeneric .

  • Q2: Est-ce que la déclaration IGeneric b=x; rend leurs signatures identiques aux arguments de type?
    A2: Non. Vous avez appelé la méthode via une définition d’interface covariante générique.

Ainsi, votre appel répond à la condition non spécifiée. Mais comment cela pourrait-il arriver?
Rappelez-vous, quelle que soit l’interface spécifiée pour référencer l’object de type DoubleDown , il s’agit toujours d’une DoubleDown . Autrement dit, il a toujours ces deux méthodes GetName . L’interface que vous spécifiez pour la référencer effectue en fait la sélection du contrat .

Voici la partie de l’image capturée à partir du test réel

entrer la description de l'image ici

Cette image montre ce qui serait retourné avec GetMembers à l’exécution. Dans tous les cas, vous le référencez, IGeneric , IGeneric ou IGeneric , ne sont pas différents. Deux images suivantes sont présentées plus en détail:

entrer la description de l'image icientrer la description de l'image ici

Les images montrent que, ces deux interfaces dérivées génériques n’ayant ni le même nom, ni d’autres signatures / jetons les rendent identiques.

Et maintenant, vous savez juste pourquoi.

Bonté sainte, beaucoup de très bonnes réponses ici à une question assez délicate. En résumé:

  • La spécification du langage ne dit pas clairement quoi faire ici.
  • Ce scénario survient généralement lorsque quelqu’un tente d’émuler une covariance ou une contravariance d’interface; maintenant que C # a une variance d’interface, nous espérons que moins de personnes utiliseront ce modèle.
  • La plupart du temps, «choisissez-en un» est un comportement raisonnable.
  • La manière dont le CLR choisit réellement quelle implémentation est utilisée dans une conversion de covariant ambiguë est définie par l’implémentation. Fondamentalement, il parsing les tables de métadonnées et choisit la première correspondance, et C # émet les tables dans l’ordre du code source. Vous ne pouvez cependant pas compter sur ce comportement; soit peut changer sans préavis.

J’appendais seulement une autre chose: la mauvaise nouvelle est que la sémantique de la réimplémentation des interfaces ne correspond pas exactement au comportement spécifié dans la spécification de la CLI dans les scénarios où ce type d’ambiguïtés se présente. La bonne nouvelle est que le comportement réel du CLR lors de la ré-implémentation d’une interface avec ce type d’ambiguïté est généralement le comportement que vous souhaitez. La découverte de ce fait a suscité un débat animé entre moi, Anders et certains responsables de la spécification de la CLI. Le résultat final n’a été ni un changement de la spécification ni de la mise en œuvre. Étant donné que la plupart des utilisateurs de C # ne savent même pas par quoi commencer la réinstallation de l’interface, nous espérons que cela n’affectera pas les utilisateurs. (Aucun client ne l’a jamais signalé.)

La question demandait “Pourquoi cela ne génère-t-il pas un avertissement du compilateur?”. En VB, c’est le cas (je l’ai implémenté).

Le système de type ne contient pas suffisamment d’informations pour fournir un avertissement au moment de l’appel concernant l’ambiguïté de la variance. Donc, l’avertissement doit être émis plus tôt …

  1. Dans VB, si vous déclarez une classe C qui implémente à la fois IEnumerable(Of Fish) et IEnumerable(Of Dog) , elle affiche un avertissement indiquant que les deux seront en conflit dans le cas commun IEnumerable(Of Animal) . Cela suffit à éliminer l’ambiguïté de la variance du code écrit entièrement en VB.

    Cependant, cela n’aide pas si la classe de problèmes a été déclarée en C #. Notez également qu’il est tout à fait raisonnable de déclarer une telle classe si personne n’invoque un membre problématique.

  2. Dans VB, si vous effectuez un transtypage d’une telle classe C en IEnumerable(Of Animal) , cela donne un avertissement sur la dissortingbution. Cela suffit à éliminer l’ambiguïté de la variance même si vous importez la classe de problème à partir des métadonnées .

    Cependant, il s’agit d’un emplacement d’avertissement médiocre car il ne peut pas être utilisé: vous ne pouvez pas modifier la dissortingbution. Le seul avertissement pouvant donner lieu à une action serait de revenir en arrière et de modifier la définition de la classe . Notez également qu’il est tout à fait raisonnable d’ effectuer une telle dissortingbution si personne n’invoque un membre problématique.

  • Question:

    Comment se fait-il que VB émette ces avertissements, mais pas C #?

    Répondre:

    Lorsque je les ai mises dans VB, j’étais enthousiaste à propos de l’informatique formelle et je n’écrivais des compilateurs que depuis quelques années, et j’ai eu le temps et l’enthousiasme de les coder.

    Eric Lippert les faisait en C #. Il a eu la sagesse et la maturité de voir que le codage de tels avertissements dans le compilateur prendrait beaucoup de temps et pourrait être mieux dépensé ailleurs, et était suffisamment complexe pour présenter un risque élevé. En effet, les compilateurs VB avaient des bogues dans ces mêmes avertissements qui n’étaient corrigés que dans VS2012.

En outre, pour être franc, il était impossible de produire un message d’avertissement suffisamment utile pour que les gens le comprennent. Incidemment,

  • Question:

    Comment le CLR résout-il l’ambiguïté lorsqu’il choisit lequel invoquer?

    Répondre:

    Il se base sur l’ ordre lexical des déclarations d’inheritance dans le code source d’origine, c’est-à-dire l’ordre lexical dans lequel vous avez déclaré que C implémente IEnumerable(Of Fish) et IEnumerable(Of Dog) .

En essayant de fouiller dans les “spécifications du langage C #”, il semble que le comportement n’est pas spécifié (si je ne me suis pas égaré).

7.4.4 Invocation de membre de fonction

Le traitement au moment de l’exécution d’un appel de membre de fonction comprend les étapes suivantes, où M est le membre de la fonction et, si M est un membre d’instance, E est l’expression de l’instance:

[…]

o L’implémentation de la fonction membre à appeler est déterminée:

• Si le type de compilation E est une interface, le membre de la fonction à invoquer est l’implémentation de M fournie par le type d’exécution de l’instance référencée par E. Ce membre de fonction est déterminé en appliquant les règles de mappage d’interface (§ 13.4.4) pour déterminer l’implémentation de M fournie par le type d’exécution de l’instance référencée par E.

13.4.4 Mappage d’interface

Le mappage d’interface pour une classe ou une structure C localise une implémentation pour chaque membre de chaque interface spécifiée dans la liste de classes de base C. L’implémentation d’un membre d’interface IM particulier, où I est l’interface dans laquelle le membre M est déclaré, est déterminée en examinant chaque classe ou structure S, en commençant par C et en répétant pour chaque classe de base successive de C, jusqu’à ce qu’une correspondance soit trouvée:

• Si S contient une déclaration d’implémentation d’un membre d’interface explicite correspondant à I et M, alors ce membre est l’implémentation de IM

• Sinon, si S contient une déclaration d’un membre public non statique correspondant à M, alors ce membre est l’implémentation de la messagerie instantanée Si plusieurs membres correspondent, le membre qui est l’implémentation de la messagerie instantanée n’est pas spécifié . Cette situation ne peut se produire que si S est un type construit où les deux membres déclarés dans le type générique ont des signatures différentes, mais les arguments de type rendent leurs signatures identiques.