Les opérateurs implicites équivalents: pourquoi sont-ils légaux?

Mettre à jour!

Voir ma dissection d’une partie de la spécification C # ci-dessous; Je pense que je dois manquer quelque chose, car il me semble que le comportement que je décris dans cette question enfreint les spécifications.

Mise à jour 2!

OK, après mûre reflection et sur la base de certains commentaires, je pense maintenant que je comprends ce qui se passe. Les mots “type source” dans la spécification font référence au type converti – c’est-à-dire, Type2 dans mon exemple ci-dessous – ce qui signifie simplement que le compilateur est en mesure de restreindre les candidats aux deux opérateurs définis (puisque Type2 est le type de source pour les deux). Cependant, il ne peut pas restreindre davantage les choix. Ainsi, les mots clés de la spécification (s’appliquant à cette question) sont “type de source” , ce que j’avais mal interprété (je pense) comme signifiant “type de déclaration”.


Question originale

Disons que j’ai ces types définis:

 class Type0 { public ssortingng Value { get; private set; } public Type0(ssortingng value) { Value = value; } } class Type1 : Type0 { public Type1(ssortingng value) : base(value) { } public static implicit operator Type1(Type2 other) { return new Type1("Converted using Type1's operator."); } } class Type2 : Type0 { public Type2(ssortingng value) : base(value) { } public static implicit operator Type1(Type2 other) { return new Type1("Converted using Type2's operator."); } } 

Alors dis que je fais ça:

 Type2 t2 = new Type2("B"); Type1 t1 = t2; 

Évidemment, cela est ambigu, car il est difficile de savoir quel opérateur implicit doit être utilisé. Ma question est la suivante – puisque je ne vois aucun moyen de résoudre cette ambiguïté (ce n’est pas comme si je pouvais effectuer une conversion explicite pour préciser la version que je veux), et pourtant les définitions de classe ci-dessus comstacknt – pourquoi le compilateur permet-il ces opérateurs implicit correspondants du tout?


Dissection

OK, je vais passer à travers l’extrait de la spécification C # citée par Hans Passant dans le but de donner un sens à cela.

Recherchez l’ensemble de types, D, à partir duquel les opérateurs de conversion définis par l’utilisateur seront considérés. Cet ensemble est constitué de S (si S est une classe ou une structure), des classes de base de S (si S est une classe) et de T (si T est une classe ou une structure).

Nous convertissons Type2 ( S ) en Type1 ( T ). Il semble donc qu’ici, D inclurait les trois types dans l’exemple: Type0 (car il s’agit d’une classe de base de S ), Type1 ( T ) et Type2 ( S ).

Recherchez l’ensemble d’opérateurs de conversion définis par l’utilisateur applicables, U. Cet ensemble comprend les opérateurs de conversion implicites définis par l’utilisateur déclarés par les classes ou les structures de D qui convertissent un type englobant S en un type englobé par T. Si U est vide , la conversion n’est pas définie et une erreur de compilation se produit.

Très bien, nous avons deux opérateurs qui remplissent ces conditions. La version déclarée dans Type1 répond aux exigences car Type1 est en D et convertit Type2 (qui englobe évidemment S ) en Type1 (qui est évidemment englobé par T ). La version de Type2 répond également aux exigences pour exactement les mêmes raisons. Donc, U inclut ces deux opérateurs.

Enfin, en ce qui concerne la recherche du “type de source” SX le plus spécifique des opérateurs de U :

Si l’un des opérateurs de U convertit de S, SX est S.

Maintenant, les deux opérateurs en U convertissent de S – donc cela me dit que SX est S.

Cela ne signifie-t-il pas que la version Type2 doit être utilisée?

Mais attendez! Je suis confus!

N’aurais-je pas pu uniquement définir la version de Type1 de l’opérateur, auquel cas il ne restrait que la version de Type1 , et pourtant, selon la spécification SX, ce serait le Type2 ? Cela semble être un scénario possible dans lequel la spécification impose une impossibilité (à savoir que la conversion déclarée dans Type2 devrait être utilisée alors qu’elle n’existe pas).

Nous ne voulons pas vraiment que ce soit une erreur de compilation juste pour définir des conversions susceptibles de créer une ambiguïté. Supposons que nous modifions Type0 pour stocker un double et que, pour une raison quelconque, nous souhaitions fournir des conversions séparées en entier signé et en entier non signé.

 class Type0 { public double Value { get; private set; } public Type0(double value) { Value = value; } public static implicit operator Int32(Type0 other) { return (Int32)other.Value; } public static implicit operator UInt32(Type0 other) { return (UInt32)Math.Abs(other.Value); } } 

Cela comstack bien, et je peux utiliser les deux conversions avec

 Type0 t = new Type0(0.9); int i = t; UInt32 u = t; 

Cependant, essayer de float f = t est une erreur de compilation, car l’une ou l’autre des conversions implicites peut être utilisée pour obtenir un type entier, qui peut ensuite être converti en float.

Nous voulons seulement que le compilateur se plaint de ces ambiguïtés plus complexes lorsqu’elles sont réellement utilisées, car nous aimerions que le Type0 ci-dessus soit compilé. Par souci de cohérence, l’ambiguïté la plus simple devrait également provoquer une erreur au moment où vous l’utilisez plutôt que lorsque vous la définissez.

MODIFIER

Depuis que Hans a retiré sa réponse qui cite la spécification, voici une brève parsing de la partie de la spécification C # qui détermine si une conversion est ambiguë, U ayant été défini comme étant l’ensemble de toutes les conversions pouvant éventuellement effectuer le travail:

  • Recherchez le type de source le plus spécifique, SX, des opérateurs de U:
    • Si l’un des opérateurs de U convertit de S, SX est S.
    • Sinon, SX est le type le plus englobé dans l’ensemble combiné de types cibles d’opérateurs en U. Si aucun type le plus englobé ne peut être trouvé, la conversion est ambiguë et une erreur lors de la compilation se produit.

En paraphrasant, nous préférons une conversion qui convertit directement à partir de S, sinon nous préférons le type “le plus facile” pour convertir S en. Dans les deux exemples, nous avons deux conversions de S disponibles. S’il n’y a aucune conversion de Type2 , nous préférerions une conversion de Type0 une conversion d’ object . Si aucun type n’est évidemment le meilleur choix pour convertir, nous échouons ici.

  • Recherchez le type de cible le plus spécifique, TX, des opérateurs de U:
    • Si l’un des opérateurs de U se convertit en T, TX est T.
    • Sinon, TX est le type le plus englobant de l’ensemble combiné des types cibles des opérateurs de U. Si aucun type très englobant ne peut être trouvé, la conversion est ambiguë et une erreur lors de la compilation se produit.

Encore une fois, nous préférerions convertir directement en T, mais nous nous contenterons du type “le plus facile” à convertir en T. Dans l’exemple de Dan, nous avons deux conversions en T disponibles. Dans mon exemple, les cibles possibles sont Int32 et UInt32 . UInt32 n’est meilleure que l’autre. La conversion échoue donc. Le compilateur n’a aucun moyen de savoir si float f = t signifie float f = (float)(Int32)t ou float f = (float)(UInt32)t .

  • Si U contient exactement un opérateur de conversion défini par l’utilisateur qui convertit de SX en TX, il s’agit de l’opérateur de conversion le plus spécifique. Si aucun opérateur de ce type n’existe ou s’il en existe plusieurs, la conversion est ambiguë et une erreur de compilation se produit.

Dans l’exemple de Dan, nous échouons ici car il nous rest deux conversions de SX à TX. Nous pourrions ne pas avoir de conversions de SX en TX si nous choisissions des conversions différentes pour choisir SX et TX. Par exemple, si nous avions un Type1a dérivé de Type1 , alors nous pourrions avoir des conversions de Type2 en Type1a et de Type0 en Type1 Cela nous donnerait toujours SX = Type2 et TX = Type1, mais nous n’avons aucune conversion de Type2 à Type1. C’est bon, parce que c’est vraiment ambigu. Le compilateur ne sait pas s’il faut convertir Type2 en Type1a, puis transtyper en Type1 ou d’abord en Type0 afin qu’il puisse utiliser cette conversion en Type1.

En fin de compte, cela ne peut être interdit avec un succès complet. Vous et moi pourrions publier deux assemblées. Nous pourrions commencer à utiliser les assemblages de chacun, tout en mettant à jour le nôtre. Ensuite, nous pourrions chacun fournir des conversions implicites entre les types définis dans chaque assemblage. Cela n’est possible que lors de la publication de la prochaine version, plutôt que lors de la compilation.

Il est avantageux de ne pas interdire les choses qui ne peuvent pas être interdites, car cela permet une clarté et une cohérence (et les législateurs en tirent une leçon).