Pourquoi puis-je attribuer 0,0 aux valeurs d'énumération, mais pas 1,0


90

Juste par curiosité: pourquoi puis-je attribuer 0,0 à une variable de type énumération, mais pas 1,0? Jetez un œil au code suivant:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

Je pensais que les conversions entre les types numériques et les valeurs d'énumération ne sont autorisées que via des casts? C'est-à-dire que je pourrais écrire Foo value2 = (Foo) 1.0;pour que la ligne 2 Mainpuisse compiler. Pourquoi y a-t-il une exception pour la valeur 0.0en C #?


17
Pour moi, c'est étrange que vous puissiez attribuer un double littéral 0.0 à une énumération personnalisée. Non pas que vous ne pouvez pas attribuer un 1.0littéral à une énumération personnalisée.
Ilya Ivanov

2
Je soupçonne que le compilateur le traite comme à la 0place. J'ai eu une question similaire une fois et Rawling a posté une excellente réponse ici .
Amiable

2
IdeOne ne le compile pas.
Johnny Mopp

Réponses:


98

C'est un bogue que vous pouvez utiliser 0.0. Le compilateur traite implicitement toutes les expressions constantes avec une valeur de zéro comme juste 0.

Maintenant, il est correct que le compilateur autorise une conversion implicite d'une intexpression constante de 0 à votre enum selon la section 6.1.3 de la spécification C # 5:

Une conversion d'énumération implicite permet au décimal-entier-littéral 0 d'être converti en n'importe quel type enum et en n'importe quel type nullable dont le type sous-jacent est un type enum. Dans ce dernier cas, la conversion est évaluée en convertissant au type enum sous-jacent et en encapsulant le résultat (§4.1.10).

J'en ai déjà parlé avec l'équipe C #: ils auraient aimé supprimer la conversion accidentelle de 0,0 (et en fait 0,0 m et 0,0f) en valeurs enum, mais malheureusement, je suppose que cela a cassé trop de code - même si cela n'aurait jamais dû être autorisé en premier lieu.

Le Mono mcscompilateur interdit toutes ces conversions à virgule flottante, même si elle ne permet:

const int Zero = 0;
...

SomeEnum x = Zero;

malgré le fait que ce Zerosoit une expression constante mais pas un entier décimal.

Je ne serais pas surpris de voir la spécification C # changer à l'avenir pour autoriser toute expression constante entière avec une valeur de 0 (c'est-à-dire imiter mcs), mais je ne m'attendrais pas à ce que les conversions en virgule flottante soient officiellement correctes. (Je me suis trompé avant de prédire l'avenir de C #, bien sûr ...)


3
Selon la spécification, ce n'est censé être que le littéral 0. Il devrait donc rejeter 1-1- une intexpression constante avec une valeur de 0. Mais comme vous le constatez, le compilateur n'est pas conforme aux spécifications ici.
Damien_The_Unbeliever

4
it broke too much code- il est vraiment difficile d'imaginer des raisons d'écrire un tel code.
Ilya Ivanov

1
@ObsidianPhoenix: Je ne suis pas sûr de ce que vous voulez dire. Il est exactement équivalent à: SomeEnum x = (SomeEnum) 0;. C'est le cas, qu'il y ait une valeur nulle nommée ou non.
Jon Skeet

2
@ObsidianPhoenix: Eh bien non, parce que la valeur de Test.Fooest 1, pas 0 ... encore une fois, c'est exactement la même chose que si vous aviez écrit Test v1 = (Test) 0;- et ce comportement vaut pour toute valeur qui n'est pas une valeur nommée dans l'énumération.
Jon Skeet

2
@JonSkeet va-t-il être corrigé à Roslyn?
Max

98

La réponse de Jon est correcte. J'y ajouterais les points suivants.

  • J'ai causé ce bug stupide et embarrassant. Beaucoup d'excuses.

  • Le bogue a été causé par une mauvaise compréhension de la sémantique d'un prédicat «expression is zero» dans le compilateur; Je pensais qu'il ne vérifiait que l'égalité des entiers zéro, alors qu'en fait, il en recherchait plus du type "est-ce la valeur par défaut de ce type?" En fait, dans une version antérieure du bogue, il était en fait possible d'attribuer la valeur par défaut de n'importe quel type à une énumération! Il ne s'agit désormais que des valeurs par défaut des nombres. (Leçon: Nommez soigneusement vos prédicats d'assistance.)

  • Le comportement que j'essayais d'implémenter et que j'avais foiré était en fait une solution de contournement pour un bogue légèrement différent. Vous pouvez lire toute la terrible histoire ici: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one et https://docs.microsoft .com / fr-fr / archive / blogs / ericlippert / the-root-of-all-evil-part-two (Leçon: Il est très facile d'introduire de nouveaux bogues pires tout en corrigeant les anciens.)

  • L'équipe C # a décidé de consacrer ce comportement bogué plutôt que de le corriger car le risque de casser le code existant sans avantage convaincant était trop élevé. (Leçon: faites les choses correctement du premier coup!)

  • Le code que j'ai écrit dans Roslyn pour préserver ce comportement se trouve dans la méthode IsConstantNumericZeroen https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - voyez-le pour plus de détails sur ce qu'est exactement le comportement de Roslyn. J'ai écrit presque tout le code dans le répertoire Conversions; Je vous encourage à tout lire car il y a de nombreux faits intéressants sur la façon dont C # diverge de la spécification dans les commentaires. J'ai décoré chacun avec SPEC VIOLATION pour les rendre faciles à trouver.

Un autre point d'intérêt: C # permet également d'utiliser n'importe quelle valeur d'énumération dans un initialiseur d'énumération indépendamment de son zéro:

enum E { A = 1 }
enum F { B = E.A }  // ???

La spécification est quelque peu vague quant à savoir si cela devrait être légal ou non, mais encore une fois, comme cela est dans le compilateur depuis longtemps, les nouveaux compilateurs sont susceptibles de maintenir le comportement.


10
C'est vraiment cool, je peux enfin voir le code que vous avez écrit. C'est génial que le code source de Roslyn soit open source. Maintenant, je comprends parfaitement qu'il existe des raisons valables (techniques / juridiques) pour ne pas fournir d'historique des modifications, mais il aurait été super génial de voir l'historique des modifications pour voir comment le code a évolué.
SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.Je ne pense pas qu'il y ait beaucoup de gens qui s'appuient sur ce comportement, et c'est l'une de ces bizarreries qu'il aurait peut-être été préférable de corriger. Cependant, cela ne fait pas vraiment de mal non plus (sauf pour les projets mettant en œuvre la spécification).
Aidiakapi

5
@Aidiakapi: En effet, le nombre de personnes touchées devrait être faible; ce n'est pas zéro. L'équipe C # prend les changements de rupture très au sérieux. Il est facile pour vous de dire qu'il vaut mieux faire le correctif; vous n'avez pas à traiter avec des clients en colère qui appellent votre vice-président pour se plaindre que votre changement insignifiant qui n'ajoute aucun avantage a retardé leur intégration système d'un jour.
Eric Lippert

3
Ça s'empire. Toutes ces modifications majeures seront (idéalement) répertoriées dans le guide de migration de Microsoft Framework. Plus cette liste est longue, plus les utilisateurs hésitent à migrer leur application. Ainsi, même un changement de rupture mineur entraîne: 1. Un petit nombre d'applications à interrompre. 2. Un petit nombre d'utilisateurs à refuser de mettre à niveau (même si le problème ne les affecte pas). 3. Un petit nombre d'utilisateurs gaspillent des ressources en évaluant si le changement de rupture les affecte. 4. Les utilisateurs des numéros 1, 2 et 3 se plaignent à tout le monde.
Brian

@EricLippert Si "La spécification est quelque peu vague", ne serait-il pas logique de mettre à jour la spécification? (Véritable question!)
James

10

Les énumérations en C # sont par définition des valeurs intégrales. Par souci de cohérence, C # ne doit accepter aucune de ces affectations, mais 0.0est traité silencieusement comme intégrale 0. C'est probablement un héritage de C, où le littéral a 0été traité spécialement et pourrait essentiellement prendre n'importe quel type donné - entier, nombre à virgule flottante, pointeur nul… vous le nommez.


3
la question est pourquoi ? Si vous allez à IL- il pousse une valeur entière sur la pileIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov Voir la mise à jour. Mais pour être honnête, la réponse est «pas de bonne raison».
Konrad Rudolph

2
Je pense que c'est l'un de ces cas où si vous regardez la spécification C # , ce n'est pas légal, mais si vous regardez n'importe quel compilateur C # que MS a produit, il le fait.
Damien_The_Unbeliever

3

enum est vraiment destiné (dans toutes les langues qui le supportent) à être un moyen de travailler avec des chaînes significatives et uniques (étiquettes) plutôt que des valeurs numériques. Ainsi, dans votre exemple, vous ne devez utiliser Bar et Baz que pour un type de données énuméré Foo . Vous ne devriez jamais utiliser (comparer ou affecter) un entier, même si de nombreux compilateurs vous permettront de vous en sortir (les énumérations sont généralement entiers en interne), et dans ce cas, un 0.0 est traité négligemment comme un 0 par le compilateur.

Conceptuellement, il devrait être correct d'ajouter un entier n à une valeur énumérée, pour obtenir n valeurs plus loin dans la ligne, ou de prendre val2 - val1 pour voir à quelle distance ils sont, mais à moins que la spécification du langage ne le permette explicitement, je Je l'éviterais. (Pensez à une valeur énumérée comme étant comme un pointeur C, de la manière dont vous pouvez l'utiliser.) Il n'y a aucune raison pour laquelle les énumérations ne peuvent pas être implémentées avec des nombres à virgule flottante, et un incrément fixe entre eux, mais je n'en ai pas entendu parler ceci étant fait dans n'importe quelle langue.


I know that I shoudn't use enums that way in C# - but I found this brain teaser and wanted to know why 0.0 is working, but 1.0 is not. I knew that it had to be something with the C# compiler because you can see that the IL Code for Foo v1 = 0.0; is the same as for Foo v2 = Foo.Bar.
feO2x
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.