44 votes

Covariance des paramètres 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<out T>
{
    string GetName();
}

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

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

Je peux alors implémenter l'interface deux fois sur une seule classe, comme ceci, en utilisant l'implémentation explicite de l'interface :

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

   string IGeneric<Derived2>.GetName()
   {
     return "Derived2";
   }  
}

Si j'utilise le (non-générique) DoubleDown et la transformer en IGeneric<Derived1> o IGeneric<Derived2> il fonctionne comme prévu :

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

Cependant, le fait de lancer le x à IGeneric<Base> donne le résultat suivant :

IGeneric<Base> 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 cela est-il autorisé ?

(inspiré par Une classe implémentant deux IObservables différentes ? . J'ai essayé de montrer à un collègue que cela allait échouer, mais d'une manière ou d'une autre, cela n'a pas fonctionné).

0 votes

Concernant Console.WriteLine(b.GetName()); le site compilateur ne peut pas émettre d'erreur ; il a un IGeneric<Base> pour appeler getName et cet appel est parfaitement valide.

0 votes

@MiserableVariable Le compilateur a plus qu'une implémentation valide - il en a deux. Dans d'autres scénarios, vous pouvez obtenir une erreur de compilation pour un appel ambigu, mais pas dans celui-ci, vous obtenez un comportement qui n'est pas spécifié.

0 votes

@SWeko, le compilateur ne regarde que l'élément statique type de b qui est IGeneric<Base> sur lequel le GetName l'appel est valide. Si vous suggérez que l'erreur devrait se trouver dans DoubleDown ce n'est pas une erreur car il existe une règle bien définie selon laquelle la correspondance est non spécifiée.

27voto

Ken Kin Points 1604

Si vous avez testé les deux :

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

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

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

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

Vous devez avoir réalisé que les résultats dans la réalité, change avec l'ordre dans lequel vous déclarez les interfaces à implémenter . Mais je dirais que c'est juste non spécifié .

Tout d'abord, la spécification (§13.4.4 Mappage d'interface) dit :

  • Si plus d'un membre correspond, il est non spécifié quel membre est la mise en œuvre de l'I.M.
  • Cette situation peut uniquement se produit si S est un type construit où les deux membres tels que déclarés dans le type générique ont différentes signatures mais les arguments de type rendent leurs signatures identiques.

Nous avons ici deux questions à examiner :

  • Q1 : Est-ce que vos interfaces génériques ont différentes signatures ?
    A1 : Oui. Ils sont IGeneric<Derived2> y IGeneric<Derived1> .

  • Q2 : Est-ce que l'affirmation IGeneric<Base> b=x; rendre leurs signatures identiques aux arguments de type ?
    A2 : Non. Vous avez invoqué la méthode par le biais d'une définition d'interface générique covariante.

Ainsi, votre appel répond à la non spécifié condition. Mais comment cela a-t-il pu se produire ?

Rappelez-vous, quelle que soit l'interface que vous avez spécifiée pour référencer l'objet de type DoubleDown il s'agit toujours d'un DoubleDown . C'est-à-dire qu'il a toujours ces deux GetName méthode. L'interface que vous spécifiez pour y faire référence, en fait, effectue sélection des contrats .

Voici une partie de l'image capturée lors du test réel.

enter image description here

Cette image montre ce qui serait retourné avec GetMembers au moment de l'exécution. Dans tous les cas, vous y faites référence, IGeneric<Derived1> , IGeneric<Derived2> o IGeneric<Base> ne sont pas différents. Les deux images suivantes montrent plus de détails :

enter image description hereenter image description here

Comme le montrent les images, ces deux interfaces dérivées génériques n'ont ni le même nom ni d'autres signatures/tokens qui les rendent identiques.

0 votes

Votre analyse est correcte, cependant, puisque nous traitons de l'implémentation explicite d'une méthode d'interface, elle devrait être couverte par le premier point de 13.4.4. Je pense que la clause multiple du deuxième point traite d'un scénario entièrement différent. IMHO, c'est juste un défaut dans la spécification. BTW, merci pour la prime. Je voulais en mettre une, mais la vie m'en a empêché :)

0 votes

Ce que vous dites signifierait qu'il devrait avoir une méthode nommée IGeneric<Base>.GetName() ce qui n'est pas le cas en réalité. Même s'il le faisait au lieu de ce qu'il fait, il en résulte quand même que la contrat réel selected(mapped) au moment de l'exécution, et tombe dans la deuxième situation du §13.4.4 dit. Aucune des implémentations ne causerait de problème de compilation, mais l'implémentation actuelle est plus simple.

0 votes

Au point 1, l'expression utilisée est "mise en œuvre explicite d'un membre d'interface qui correspond à I et M", et IGeneric<DerivedX>.GetName() correspond IGeneric<Base>.GetName() (même s'ils ne sont pas identiques, ce qui est le but de la co/contra-variance). Au point 2, il est dit que les méthodes doivent être "une déclaration d'un membre public non statique qui correspond à M", ce qui signifie que le membre public non statique n'est pas un membre public. IGeneric<DerivedX>.GetName() ne l'est pas.

25voto

jam40jeff Points 1464

Le compilateur ne peut pas lancer une erreur sur la ligne

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

car il n'y a aucune ambiguïté que le compilateur puisse connaître. GetName() est en fait une méthode valide sur l'interface IGeneric<Base> . Le compilateur ne suit pas le type d'exécution de b pour savoir qu'il y a un type là-dedans qui pourrait causer une ambiguïté. C'est donc au runtime de décider ce qu'il doit faire. Le runtime pourrait lever une exception, mais les concepteurs du CLR ont apparemment décidé de ne pas le faire (ce que je considère personnellement comme une bonne décision).

Pour le dire autrement, disons qu'à la place, vous aviez simplement écrit la méthode :

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}

et vous ne fournissez aucune classe mettant en œuvre IGeneric<T> dans votre assemblée. Vous distribuez cette interface et beaucoup d'autres n'implémentent cette interface qu'une seule fois et sont capables d'appeler votre méthode sans problème. Cependant, quelqu'un finit par consommer votre assembly et crée l'interface DoubleDown et le passe dans votre méthode. À quel moment le compilateur doit-il lancer une erreur ? Sûrement l'assemblage déjà compilé et distribué contenant l'appel à la méthode GetName() ne peut pas produire une erreur de compilation. On pourrait dire que l'affectation de DoubleDown à IGeneric<Base> produit l'ambiguïté. mais une fois encore, nous pourrions ajouter un autre niveau d'indirection dans l'assemblage original :

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

Là encore, de nombreux consommateurs pourraient appeler soit CallIt o CallItOnDerived1 et aller bien. Mais notre consommateur passant DoubleDown fait également un appel parfaitement légal qui ne pourrait pas causer d'erreur de compilation lorsqu'ils appellent CallItOnDerived1 comme la conversion de DoubleDown à IGeneric<Derived1> devrait certainement être OK. Ainsi, il n'y a pas de point où le compilateur peut jeter une erreur autre qu'éventuellement sur la définition de DoubleDown mais cela éliminerait la possibilité de faire quelque chose de potentiellement utile sans solution de rechange.

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

Pas d'avertissement ou d'erreur (ou d'échec d'exécution) lorsque la contravariance conduit à une ambiguïté.

Étant donné que la probabilité que le langage évolue pour prendre en charge cette fonction est pratiquement nulle, je pense que le comportement actuel est correct, sauf qu'il devrait être défini dans les spécifications de manière à ce que toutes les implémentations du CLR soient censées se comporter de la même manière.

1 votes

Belle réponse ! J'ai voté pour. Vous avez fourni une hypothèse beaucoup plus profonde pour expliquer le fait. Les commentaires entre SWeko et moi de ma réponse, j'ai répondu avec la mise en œuvre actuelle est plus simple .

0 votes

"A quel moment le compilateur doit-il lancer une erreur ?" Le compilateur devrait lancer une erreur lorsque vous convertissez implicitement DoubleDown à IGeneric<Base> car elle est ambiguë (le compilateur ne sait pas laquelle des deux interfaces il doit utiliser).

0 votes

@Ark-kun Même si le compilateur lançait une erreur dans ce cas, il n'y a nulle part dans le second cas (où DoubleDown est d'abord attribué à IGeneric<Derived1> et plus tard IGeneric<Dervied1> est affecté à IGeneric<Base> ) que le compilateur peut lancer une erreur.

11voto

Lucian Wischik Points 656

La question posée est la suivante : "Pourquoi cela ne produit-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 assez d'informations pour fournir un avertissement. au moment de l'invocation sur l'ambiguïté de la variance. L'alerte doit donc être émise plus tôt ...

  1. En VB, si vous déclarez une classe C qui met en œuvre à la fois IEnumerable(Of Fish) y IEnumerable(Of Dog) puis il donne un avertissement disant que les deux seront en conflit dans le cas commun IEnumerable(Of Animal) . C'est suffisant pour éliminer l'ambiguïté de la variance d'un code entièrement écrit en VB.

    Toutefois, cela ne sert à rien si la classe problématique 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 sur elle.

  2. En VB, si vous effectuez un cast depuis une telle classe C en IEnumerable(Of Animal) alors il donne un avertissement sur le casting. C'est suffisant pour éradiquer la variance-ambiguïté. même si vous avez importé la classe problématique depuis les métadonnées .

    Cependant, il s'agit d'un mauvais emplacement d'avertissement car il n'est pas réalisable : vous ne pouvez pas aller changer le casting. Le seul avertissement réalisable pour les gens serait pour revenir en arrière et changer la définition de la classe . Notez également qu'il est tout à fait raisonnable de réaliser un tel casting si personne n'invoque un membre problématique sur elle.

  • Question :

    Comment se fait-il que VB émette ces avertissements alors que C# ne le fait pas ?

    Réponse :

    Lorsque je les ai mis en VB, j'étais enthousiaste à l'égard de l'informatique formelle, et je n'écrivais des compilateurs que depuis deux ans, et j'avais le temps et l'enthousiasme pour les coder.

    Eric Lippert les faisait en C#. Il avait la sagesse et la maturité de voir que coder de tels avertissements dans le compilateur prendrait beaucoup de temps qui pourrait être mieux utilisé ailleurs, et était suffisamment complexe pour comporter un risque élevé. En effet, les compilateurs VB présentaient des bogues dans ces mêmes avertissements qui n'ont été corrigés que dans VS2012.

De plus, pour être franc, il était impossible de trouver un message d'avertissement suffisamment utile pour que les gens le comprennent. Au fait,

  • Question :

    Comment le CLR résout-il l'ambiguïté lors du choix de la méthode à invoquer ?

    Réponse :

    Elle se base sur le ordre lexical des déclarations d'héritage dans le code source d'origine, c'est-à-dire l'ordre lexical dans lequel vous avez déclaré cet héritage. C met en œuvre IEnumerable(Of Fish) y IEnumerable(Of Dog) .

6 votes

Vous êtes très gentil, Lucian, et un peu inutilement dur envers vous-même ; c'était un choix difficile et je peux voir les arguments de chaque côté. Et je note que je a fait ajouter un avertissement à C# pour un cas similaire qui a un comportement défini par l'implémentation en raison d'une unification malheureuse des types : blogs.msdn.com/b/ericlippert/archive/2006/04/06/570126.aspx

11voto

Eric Lippert Points 300275

Bonté divine, beaucoup de très bonnes réponses ici à ce qui est une question délicate. Je résume :

  • La spécification du langage ne dit pas clairement ce qu'il faut faire ici.
  • Ce scénario se produit généralement lorsque quelqu'un tente d'émuler la covariance ou la contravariance de l'interface ; maintenant que C# dispose de la variance de l'interface, nous espérons que moins de personnes utiliseront ce motif.
  • La plupart du temps, "choisir un seul" est un comportement raisonnable.
  • La façon dont le CLR choisit réellement quelle implémentation est utilisée dans une conversion covariante ambiguë est définie par l'implémentation. En gros, il parcourt les tableaux de métadonnées et choisit la première correspondance, et il se trouve que C# émet les tableaux dans l'ordre du code source. Vous ne pouvez cependant pas vous fier à ce comportement ; l'un ou l'autre peut changer sans préavis.

Je n'ajouterais qu'une seule chose : la mauvaise nouvelle est que sémantique de réimplémentation des interfaces ne correspondent pas exactement au comportement spécifié dans la spécification CLI dans les scénarios où ces types d'ambiguïtés se présentent. 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 donné lieu à un débat animé entre moi, Anders et certains des mainteneurs de la spécification CLI et le résultat final a été de ne modifier ni la spécification ni l'implémentation. Étant donné que la plupart des utilisateurs de C# ne savent même pas ce qu'est la réimplémentation d'interface, nous espérons que cela n'aura pas d'effet négatif sur les utilisateurs. (Aucun client ne l'a jamais signalé à mon attention).

0 votes

La situation décrite s'appliquerait-elle si la classe Foo implémente IEnumerable<Bar> , and class DerivedFoo` implémente IEnumerable<DerivedBar> ? Existe-t-il des moyens pratiques permettant d'éviter de créer des liens ambigus tout en permettant à un groupe de personnes d'avoir accès à l'information ? DerivedFoo pour être transmis à un code qui requiert une énumération de choses qui ne sont pas seulement une sorte de Bar mais plus spécifiquement une sorte de DerivedBar ?

2voto

Teudimundo Points 1055

En essayant de se plonger dans les "spécifications du langage C#", il semble que le comportement ne soit pas spécifié (si je ne me suis pas perdu en chemin).

7.4.4 Invocation d'un membre de la fonction

Le traitement d'exécution d'une invocation de membre de fonction consiste en les étapes suivantes, où M est le membre de fonction et, si M est un membre d'instance, E est l'expression d'instance :

[...]

o L'implémentation du membre de la fonction à invoquer est déterminée :

- Si le type de compilation de E est une interface, le membre de 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é par appliquer 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 des interfaces

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 des classes de base de C. L'implémentation d'un membre d'interface particulier I.M, 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 explicite de l'interface qui correspond à I et M, alors ce membre est l'implémentation de I.M.

- Sinon, si S contient une déclaration d'un membre public non statique qui correspond à M, alors ce membre est l'implémentation de I.M. Si plus d'un membre correspond à, il n'est pas précisé quel membre est la mise en œuvre de l'I.M. . Cette situation ne peut se produire que si S est un type construit où les deux membres tels que déclarés dans le type générique ont des signatures différentes, mais les arguments du type rendent leurs signatures identiques.

2 votes

Je ne pense pas que ce soit ça. Le mappage d'interface consiste à décider quelle méthode de la classe finit par implémenter chaque méthode déclarée dans les interfaces que la classe implémente, sans compter que les membres en question ici ne sont pas publics (les interfaces sont implémentées explicitement).

0 votes

Merci Jon. J'ai modifié la réponse, la résolution d'invocation de méthode utilise le même mécanisme, en vérifiant le type d'exécution.

1 votes

Cette autre citation semble pertinente ici. Mais je pense que le comportement est en fait expliqué par la premièrement balle : les interfaces de base de DoubleDown sont recherchées tour à tour jusqu'à ce qu'une méthode qui correspond à IGeneric<Base>.GetName() est trouvé. Ainsi, l'appel de la méthode correspond à IGeneric<Derived1>.GetName() car cette interface apparaît en premier dans la liste des interfaces héritées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X