Roslyn konnte keinen Code kompilieren

95

Nachdem ich mein Projekt von VS2013 auf VS2015 migriert habe, wird das Projekt nicht mehr erstellt. In der folgenden LINQ-Anweisung tritt ein Kompilierungsfehler auf:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

Der Compiler gibt einen Fehler zurück:

Fehler CS0165 Verwendung der nicht zugewiesenen lokalen Variablen 'b'

Was verursacht dieses Problem? Ist es möglich, es über eine Compilereinstellung zu beheben?

ramil89
quelle
11
@BinaryWorrier: Warum? Es wird nur verwendet, bnachdem es über einen outParameter zugewiesen wurde.
Jon Skeet
1
In der VS 2015-Dokumentation heißt es: "Obwohl Variablen, die als Out-Argumente übergeben werden, vor der Übergabe nicht initialisiert werden müssen, muss die aufgerufene Methode einen Wert zuweisen, bevor die Methode zurückgegeben wird." Das sieht also wie ein Fehler aus. Ja, es wird garantiert von diesem tryParse initialisiert.
Rup
3
Unabhängig vom Fehler veranschaulicht dieser Code alles, was an outArgumenten schlecht ist . Würde das einen TryParsenullbaren Wert (oder einen äquivalenten Wert) zurückgeben?
Konrad Rudolph
1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bsieht viel besser aus
Rawling
2
Nur um zu beachten, können Sie dies vereinfachen decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Ich habe einen Roslyn-Fehler geöffnet , der dies auslöst.
Rawling

Antworten:

112

Was verursacht dieses Problem?

Sieht für mich wie ein Compiler-Bug aus. Zumindest war es so. Obwohl die Ausdrücke decimal.TryParse(v, out a)und decimal.TryParse(v, out b)dynamisch ausgewertet werden, habe ich erwartet , dass der Compiler immer noch versteht, dass a <= bbeide aund bdefinitiv zugewiesen sind, wenn sie erreicht sind. Selbst mit den Verrücktheiten, die beim dynamischen Tippen auftreten können, würde ich erwarten, dass die a <= bBewertung erst nach der Bewertung beider TryParseAnrufe erfolgt.

Es stellt sich jedoch heraus, dass es durch Operator und Konvertierung schwierig ist, einen Ausdruck zu haben, A && B && Cder ausgewertet wird Aund Caber nicht B- wenn Sie schlau genug sind. Das geniale Beispiel von Neal Gafter finden Sie im Roslyn-Fehlerbericht .

Es dynamicist noch schwieriger , diese Arbeit zu erledigen - die Semantik bei dynamischen Operanden ist schwieriger zu beschreiben, da Sie zur Durchführung der Überlastungsauflösung Operanden auswerten müssen, um herauszufinden, um welche Typen es sich handelt, was möglicherweise nicht intuitiv ist. Aber auch hier ist Neal hat mit einem Beispiel , das zeigt , dass die Compiler - Fehler kam erforderlich ... dies ist kein Fehler, es ist ein Bug - Fix . Riesige Anerkennung an Neal für den Beweis.

Ist es möglich, es durch Compiler-Einstellungen zu beheben?

Nein, aber es gibt Alternativen, die den Fehler vermeiden.

Erstens könnten Sie verhindern, dass es dynamisch ist. Wenn Sie wissen, dass Sie immer nur Zeichenfolgen verwenden, können Sie der Bereichsvariablen den Typ (dh ) verwenden IEnumerable<string> oder geben . Das wäre meine bevorzugte Option.vstringfrom string v in array

Wenn Sie es wirklich dynamisch halten müssen, geben Sie zunächst beinen Wert an:

decimal a, b = 0m;

Dies wird nicht schaden - wir wissen , dass tatsächlich Ihre dynamische Auswertung etwas verrückt nicht tun, so dass Sie immer noch einen Wert zuweisen am Ende dann , bbevor Sie es verwenden, so dass der Ausgangswert irrelevant.

Darüber hinaus scheint das Hinzufügen von Klammern auch zu funktionieren:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Dies ändert den Punkt, an dem verschiedene Teile der Überlastungsauflösung ausgelöst werden, und macht den Compiler glücklich.

Es bleibt noch ein Problem offen: Die Regeln der Spezifikation für die definitive Zuordnung zum &&Operator müssen präzisiert werden, um festzustellen, dass sie nur gelten, wenn der &&Operator in seiner "regulären" Implementierung mit zwei boolOperanden verwendet wird. Ich werde versuchen sicherzustellen, dass dies für den nächsten ECMA-Standard behoben ist.

Jon Skeet
quelle
Ja! Das Anwenden IEnumerable<string>oder Hinzufügen von Klammern hat bei mir funktioniert. Jetzt erstellt der Compiler fehlerfrei.
Ramil89
1
using decimal a, b = 0m;könnte den Fehler beseitigen, a <= bwürde dann aber immer verwenden 0m, da der out-Wert noch nicht berechnet wurde.
Paw Baltzersen
12
@PawBaltzersen: Warum denkst du das? Es ist immer wird vor dem Vergleich zugeordnet werden - es ist nur , dass der Compiler kann es nicht beweisen, aus irgendeinem Grunde (ein Fehler, im Grunde).
Jon Skeet
1
Eine Parsing-Methode ohne Nebenwirkung haben, dh. decimal? TryParseDecimal(string txt)kann auch eine Lösung sein
zahir
1
Ich frage mich, ob es eine faule Initialisierung ist. es denkt "wenn der erste wahr ist, dann muss ich den zweiten nicht bewerten, was bedeutet, dass er bmöglicherweise nicht zugewiesen wird"; Ich weiß, dass das eine ungültige Argumentation ist, aber es erklärt, warum die Klammern es
beheben
16

Da ich im Fehlerbericht so hart geschult wurde, werde ich versuchen, dies selbst zu erklären.


Stellen Sie sich vor, es Thandelt sich um einen benutzerdefinierten Typ mit einer impliziten Umwandlung, booldie zwischen falseund wechselt true, beginnend mit false. Soweit der Compiler weiß, könnte das dynamicerste Argument zum ersten &&für diesen Typ ausgewertet werden, daher muss es pessimistisch sein.

Wenn dann der Code kompiliert wird, kann dies passieren:

  • Wenn der dynamische Binder den ersten auswertet &&, führt er Folgendes aus:
    • Bewerten Sie das erste Argument
    • Es ist ein T- implizit gegossen bool.
    • Oh, das ist es false, also müssen wir das zweite Argument nicht bewerten.
    • Machen Sie das Ergebnis der &&Auswertung als erstes Argument. (Nein, falseaus irgendeinem Grund nicht.)
  • Wenn der dynamische Binder den zweiten auswertet &&, führt er Folgendes aus:
    • Bewerten Sie das erste Argument.
    • Es ist ein T- implizit gegossen bool.
    • Oh, das ist es true, also bewerte das zweite Argument.
    • ... Oh Mist, bist nicht zugeordnet.

Kurz gesagt, es gibt spezielle "definitive Zuweisungs" -Regeln, die uns nicht nur sagen lassen, ob eine Variable "definitiv zugewiesen" oder "nicht definitiv zugewiesen" ist, sondern auch, ob sie "definitiv nach falseAnweisung zugewiesen" oder "definitiv zugewiesen" ist zugewiesen nach trueAnweisung ".

Diese sind vorhanden, damit der Compiler beim Umgang mit &&und ||(und !und ??und ?:) prüfen kann, ob Variablen in bestimmten Zweigen eines komplexen booleschen Ausdrucks zugewiesen werden können.

Diese funktionieren jedoch nur, solange die Typen der Ausdrücke boolesch bleiben . Wenn ein Teil des Ausdrucks dynamic(oder ein nicht-boolescher statischer Typ) ist, können wir nicht mehr zuverlässig sagen, dass der Ausdruck trueoder false- wenn wir ihn das nächste Mal umwandeln, um boolzu entscheiden, welcher Zweig verwendet werden soll, hat er möglicherweise seine Meinung geändert.


Update: Dies wurde nun behoben und dokumentiert :

Die definitiven Zuweisungsregeln, die von früheren Compilern für dynamische Ausdrücke implementiert wurden, ermöglichten einige Fälle von Code, die dazu führen konnten, dass Variablen gelesen wurden, die nicht definitiv zugewiesen wurden. Einen Bericht hierzu finden Sie unter https://github.com/dotnet/roslyn/issues/4509 .

...

Aufgrund dieser Möglichkeit darf der Compiler nicht zulassen, dass dieses Programm kompiliert wird, wenn val keinen Anfangswert hat. In früheren Versionen des Compilers (vor VS2015) konnte dieses Programm auch dann kompiliert werden, wenn val keinen Anfangswert hat. Roslyn diagnostiziert nun diesen Versuch, eine möglicherweise nicht initialisierte Variable zu lesen.

Rawling
quelle
1
Mit VS2013 auf meinem anderen Computer habe ich es tatsächlich geschafft, nicht zugewiesenen Speicher damit zu lesen. Es ist nicht sehr aufregend :(
Rawling
Sie können nicht initialisierte Variablen mit einem einfachen Delegaten lesen. Erstellen Sie einen Delegaten, der outzu einer Methode mit verfügt ref. Es wird es gerne tun und es werden Variablen zugewiesen, ohne den Wert zu ändern.
IllidanS4 will Monica
Aus Neugier habe ich dieses Snippet mit C # v4 getestet. Aus Neugier - wie entscheidet sich der Compiler, den Operator false/ trueim Gegensatz zum impliziten Cast-Operator zu verwenden? Lokal wird implicit operator booldas erste Argument aufgerufen , dann der zweite Operand aufgerufen, operator falseder erste Operand aufgerufen , gefolgt vom implicit operator boolersten Operanden erneut . Das macht für mich keinen Sinn, der erste Operand sollte sich im Wesentlichen einmal auf einen Booleschen Wert beschränken, oder?
Rob
@Rob Ist das der dynamicverkettete &&Fall? Ich habe gesehen, wie es im Grunde ging (1) das erste Argument auswerten (2) implizite Besetzung verwenden, um zu sehen, ob ich kurzschließen kann (3) ich kann nicht, also evauiere das zweite Argument (4) jetzt kenne ich beide Typen, ich Das Beste &&ist ein benutzerdefinierter &(5) Aufrufoperator falsefür das erste Argument, um zu sehen, ob ich kurzschließen kann. (6) Ich kann (weil falseund nicht implicit booleinverstanden). Das Ergebnis ist also das erste Argument ... und dann das nächste &&, (7) benutze implizite Besetzung, um zu sehen, ob ich (wieder) kurzschließen kann.
Rawling
@ IllidanS4 Klingt interessant, aber ich habe nicht herausgefunden, wie es geht. Kannst du mir einen Ausschnitt geben?
Rawling
15

Dies ist kein Fehler. Unter https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 finden Sie ein Beispiel dafür, wie ein dynamischer Ausdruck dieses Formulars eine solche ausgelassene Variable nicht zuweisen kann.

Neal Gafter
quelle
1
Da meine Antwort akzeptiert und hoch bewertet wird, habe ich sie bearbeitet, um die Auflösung anzuzeigen. Vielen Dank für all Ihre Arbeit daran - einschließlich der Erklärung meines Fehlers zu mir :)
Jon Skeet