Warum kann ich Aufzählungswerten 0.0 zuweisen, aber nicht 1.0


90

Nur aus Neugier: Warum kann ich einer Variablen vom Typ Aufzählung 0.0 zuweisen, aber nicht 1.0? Schauen Sie sich den folgenden Code an:

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
    }
}

Ich dachte, dass Konvertierungen zwischen numerischen Typen und Aufzählungswerten nur über Casts zulässig sind. Das heißt, ich könnte Foo schreiben, value2 = (Foo) 1.0;damit Zeile 2 in Mainkompiliert werden kann. Warum gibt es eine Ausnahme für den Wert 0.0in C #?


17
Für mich ist es seltsam, dass Sie der benutzerdefinierten Aufzählung ein Doppelliteral 0.0 zuweisen können . Nicht, dass Sie der benutzerdefinierten Aufzählung kein1.0 Literal zuweisen können .
Ilya Ivanov

2
Ich vermute, der Compiler behandelt es 0stattdessen so. Ich hatte eine ähnliche Frage einmal und Rawlings eine große Antwort gepostet hier .
Freundlich

2
IdeOne kompiliert es nicht.
Johnny Mopp

Antworten:


98

Es ist ein Fehler, den Sie 0.0 verwenden können. Der Compiler behandelt implizit alle konstanten Ausdrücke mit dem Wert Null als nur 0.

Nun ist es richtig, dass der Compiler eine implizite Konvertierung von einem konstanten intAusdruck von 0 in Ihre Aufzählung gemäß Abschnitt 6.1.3 der C # 5-Spezifikation zulässt:

Eine implizite Aufzählungskonvertierung ermöglicht die Konvertierung des Dezimal-Ganzzahl-Literal 0 in einen beliebigen Aufzählungstyp und in einen nullbaren Typ, dessen zugrunde liegender Typ ein Aufzählungstyp ist. Im letzteren Fall wird die Konvertierung ausgewertet, indem in den zugrunde liegenden Aufzählungstyp konvertiert und das Ergebnis umbrochen wird (§4.1.10).

Ich habe bereits zuvor mit dem C # -Team darüber gesprochen: Sie hätten gerne die versehentliche Konvertierung von 0.0 (und tatsächlich 0.0m und 0.0f) in Enum-Werte entfernt, aber leider habe ich festgestellt, dass zu viel Code kaputt gegangen ist - obwohl es hätte niemals erlaubt sein dürfen.

Der Mono- mcsCompiler verbietet alle diese Gleitkommakonvertierungen, erlaubt jedoch Folgendes:

const int Zero = 0;
...

SomeEnum x = Zero;

trotz der Tatsache, dass dies Zeroein konstanter Ausdruck ist, aber kein Dezimal-Ganzzahl-Literal.

Es würde mich nicht wundern, wenn sich die C # -Spezifikation in Zukunft ändern würde, um einen ganzzahligen konstanten Ausdruck mit einem Wert von 0 zuzulassen (dh nachzuahmen mcs), aber ich würde nicht erwarten, dass die Gleitkommakonvertierungen jemals offiziell korrekt sind. (Ich habe mich vorher geirrt, die Zukunft von C # vorherzusagen, natürlich ...)


3
Gemäß der Spezifikation soll es nur das Literal 0 sein. Daher sollte es ablehnen 1-1- ein konstanter intAusdruck mit einem Wert von 0. Wie Sie jedoch beobachten, stimmt der Compiler hier nicht mit der Spezifikation überein.
Damien_The_Unbeliever

4
it broke too much code- Es ist wirklich schwer vorstellbar, warum man solchen Code schreibt.
Ilya Ivanov

1
@ObsidianPhoenix: Ich bin nicht sicher, was du meinst. Es ist genau gleichbedeutend mit : SomeEnum x = (SomeEnum) 0;. Das ist der Fall, ob es einen benannten Nullwert gibt oder nicht.
Jon Skeet

2
@ObsidianPhoenix: Nun nein, denn der Wert von Test.Fooist 1, nicht 0 ... wieder ist das genau das gleiche, als ob Sie geschrieben hätten Test v1 = (Test) 0;- und dieses Verhalten gilt für jeden Wert, der kein benannter Wert in der Aufzählung ist.
Jon Skeet

2
@ JonSkeet wird es in Roslyn behoben?
Max

98

Jons Antwort ist richtig. Ich würde die folgenden Punkte hinzufügen.

  • Ich habe diesen dummen und peinlichen Fehler verursacht. Viele Entschuldigungen.

  • Der Fehler wurde dadurch verursacht, dass ich die Semantik eines Prädikats "Ausdruck ist Null" im Compiler falsch verstanden habe. Ich glaubte, es wurde nur auf Ganzzahl-Null-Gleichheit geprüft, obwohl tatsächlich nach mehr gesucht wurde, wie folgt: "Ist dies der Standardwert dieses Typs?" Tatsächlich war es in einer früheren Version des Fehlers tatsächlich möglich, einer Aufzählung den Standardwert eines beliebigen Typs zuzuweisen! Es sind jetzt nur Standardwerte von Zahlen. (Lektion: Nennen Sie Ihre Helfer-Prädikate sorgfältig.)

  • Das Verhalten, das ich zu implementieren versuchte und das ich durcheinander gebracht habe, war tatsächlich eine Problemumgehung für einen etwas anderen Fehler. Sie können die ganze schreckliche Geschichte hier lesen: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one und https://docs.microsoft .com / de-de / archive / blogs / ericlippert / die wurzel des ganzen bösen teils zwei (Lektion: Es ist sehr einfach, neue, schlimmere Fehler einzuführen, während alte behoben werden.)

  • Das C # -Team hat beschlossen, dieses fehlerhafte Verhalten zu verankern, anstatt es zu beheben, da das Risiko, vorhandenen Code ohne zwingenden Nutzen zu beschädigen, zu hoch war. (Lektion: Mach es gleich beim ersten Mal richtig!)

  • Der Code, den ich in Roslyn geschrieben habe, um dieses Verhalten beizubehalten, befindet sich in der Methode IsConstantNumericZerounter https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - Weitere Informationen zum genauen Verhalten von Roslyn finden Sie hier. Ich habe fast den gesamten Code in das Conversions-Verzeichnis geschrieben. Ich ermutige Sie, alles zu lesen, da es viele interessante Fakten darüber gibt, wie C # von der Spezifikation in den Kommentaren abweicht. Ich habe jedes mit SPEC VIOLATION dekoriert, damit sie leicht zu finden sind.

Noch ein Punkt von Interesse: Mit C # kann auch jeder Aufzählungswert in einem Aufzählungsinitialisierer verwendet werden, unabhängig von seiner Nullheit:

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

Die Spezifikation ist etwas vage, ob dies legal sein sollte oder nicht, aber da dies schon lange im Compiler ist, werden die neuen Compiler das Verhalten wahrscheinlich beibehalten.


10
Das ist wirklich cool, ich sehe endlich den Code, den du geschrieben hast. Es ist großartig, dass Roslyn-Quellcode Open Source ist. Jetzt verstehe ich vollkommen, dass es gültige Gründe (technisch / rechtlich) gibt, keine Änderungshistorie bereitzustellen, aber es wäre super toll gewesen, die Änderungshistorie zu sehen, um zu sehen, wie sich der Code entwickelt hat.
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.Ich glaube nicht, dass es viele Menschen geben würde, die sich auf dieses Verhalten verlassen, und es ist eine dieser Kuriositäten, die vielleicht besser zu beheben war. Es schadet jedoch auch nicht wirklich (außer bei Projekten, die die Spezifikation implementieren).
Aidiakapi

5
@Aidiakapi: In der Tat sollte die Anzahl der Betroffenen gering sein; es ist nicht Null. Das C # -Team nimmt wichtige Änderungen sehr ernst. Es fällt Ihnen leicht zu sagen, dass es besser ist, das Problem zu beheben. Sie müssen sich nicht mit wütenden Kunden auseinandersetzen, die Ihren Vizepräsidenten anrufen, um sich darüber zu beschweren, dass Ihre triviale Änderung, die keinerlei Vorteile bringt, ihre Systemintegration um einen Tag verzögert.
Eric Lippert

3
Es wird schlimmer. Alle derartigen Änderungen werden (idealerweise) im Microsoft Framework-Migrationshandbuch aufgeführt. Je länger diese Liste ist, desto zögerlicher sind Benutzer, ihre Anwendung zu migrieren. Selbst eine geringfügige Änderung des Bruchs führt also dazu, dass: 1. eine kleine Anzahl von Anwendungen kaputt geht. 2. Eine kleine Anzahl von Benutzern, die ein Upgrade ablehnen (auch wenn das Problem sie nicht betrifft). 3. Eine kleine Anzahl von Benutzern verschwendet Ressourcen, um zu bewerten, ob die Änderung sie betrifft. 4. Benutzer von # 1, # 2 und # 3 beschweren sich bei allen anderen.
Brian

@EricLippert Wenn "Die Spezifikation ist etwas vage", wäre es nicht sinnvoll, die Spezifikation zu aktualisieren? (Echte Frage!)
James

10

Aufzählungen in C # sind per Definition Integralwerte. Aus Konsistenzgründen sollte C # keine dieser Zuweisungen akzeptieren, wird jedoch 0.0stillschweigend als Integral behandelt 0. Dies ist wahrscheinlich ein Überbleibsel von C, wo das Literal 0speziell behandelt wurde und im Wesentlichen einen bestimmten Typ annehmen kann - Ganzzahl, Gleitkommazahl, Nullzeiger ... Sie nennen es.


3
Die Frage ist warum ? Wenn Sie zu gehen IL- es drückt ganzzahligen Wert auf den StapelIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov Siehe Update. Aber um ehrlich zu sein, lautet die Antwort „kein guter Grund“.
Konrad Rudolph

2
Ich denke, dass dies einer der Fälle ist, in denen es nicht legal ist, wenn Sie sich die C # -Spezifikation ansehen, aber wenn Sie sich einen von MS erstellten C # -Compiler ansehen, tut dies dies.
Damien_The_Unbeliever

3

enum soll wirklich (in allen Sprachen, die es unterstützen) eine Möglichkeit sein, mit aussagekräftigen und eindeutigen Zeichenfolgen (Beschriftungen) anstatt mit numerischen Werten zu arbeiten. Daher sollten Sie in Ihrem Beispiel nur Bar und Baz verwenden, wenn Sie mit einem aufgezählten Foo- Datentyp arbeiten. Sie sollten niemals eine Ganzzahl verwenden (vergleichen oder zuweisen), obwohl viele Compiler Sie damit durchkommen lassen (Aufzählungen sind normalerweise interne Ganzzahlen), und in diesem Fall wird eine 0.0 vom Compiler unachtsam als 0 behandelt.

Konzeptionell sollte es in Ordnung sein, einem aufgezählten Wert eine Ganzzahl n hinzuzufügen , n Werte weiter unten zu erhalten oder val2 - val1 zu nehmen, um zu sehen, wie weit sie voneinander entfernt sind, aber es sei denn, die Sprachspezifikation erlaubt dies ausdrücklich, I. würde es vermeiden. (Stellen Sie sich einen aufgezählten Wert wie einen C-Zeiger vor, wie Sie ihn verwenden können.) Es gibt keinen Grund, warum Aufzählungen nicht mit Gleitkommazahlen und einem festen Inkrement zwischen ihnen implementiert werden könnten, aber ich habe noch nichts davon gehört Dies geschieht in jeder Sprache.


Ich weiß, dass ich in C # keine Aufzählungen auf diese Weise verwenden sollte - aber ich fand diesen Denksport und wollte wissen, warum 0.0 funktioniert, 1.0 jedoch nicht. Ich wusste, dass es etwas mit dem C # -Compiler sein musste, weil Sie sehen können, dass der IL-Code für Foo v1 = 0.0;derselbe ist wie für Foo v2 = Foo.Bar.
feO2x
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.