Warum führt diese (null ||! TryParse) Bedingung zur Verwendung einer nicht zugewiesenen lokalen Variablen?

98

Der folgende Code führt zur Verwendung der nicht zugewiesenen lokalen Variablen "numberOfGroups" :

int numberOfGroups;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Allerdings funktioniert dieser Code in Ordnung (obwohl, ReSharper , sagt der = 10überflüssig ist):

int numberOfGroups = 10;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Vermisse ich etwas oder mag der Compiler meine nicht ||?

Ich habe dies eingegrenzt, dynamicum die Probleme zu verursachen ( optionswar eine dynamische Variable in meinem obigen Code). Es bleibt die Frage, warum ich das nicht tun kann .

Dieser Code wird nicht kompiliert:

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        dynamic myString = args[0];

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Dieser Code bewirkt jedoch :

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        var myString = args[0]; // var would be string

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Ich wusste nicht, dynamicdass dies ein Faktor sein würde.

Brandon Martinez
quelle
Denken Sie nicht, dass es klug genug ist zu wissen, dass Sie den an Ihren outParameter übergebenen Wert nicht als Eingabe verwenden
Charleh
3
Der hier angegebene Code zeigt nicht das beschriebene Verhalten. es funktioniert gut. Bitte geben Sie eine Postleitzahl ein, die tatsächlich das von Ihnen beschriebene Verhalten demonstriert, das wir selbst kompilieren können. Gib uns die ganze Datei.
Eric Lippert
8
Ah, jetzt haben wir etwas Interessantes!
Eric Lippert
1
Es ist nicht allzu überraschend, dass der Compiler davon verwirrt ist. Der Hilfecode für die dynamische outAufrufsite verfügt wahrscheinlich über einen Kontrollfluss, der keine Zuordnung zum Parameter garantiert . Es ist sicherlich interessant zu überlegen, welchen Hilfscode der Compiler erstellen sollte, um das Problem zu vermeiden, oder ob dies überhaupt möglich ist.
CodesInChaos
1
Auf den ersten Blick sieht das sicher wie ein Käfer aus.
Eric Lippert

Antworten:

73

Ich bin mir ziemlich sicher, dass dies ein Compiler-Fehler ist. Schöner Fund!

Bearbeiten: Es ist kein Fehler, wie Quartermeister demonstriert; dynamic implementiert möglicherweise einen seltsamen trueOperator, der dazu führen kann y, dass er niemals initialisiert wird.

Hier ist ein minimaler Repro:

class Program
{
    static bool M(out int x) 
    { 
        x = 123; 
        return true; 
    }
    static int N(dynamic d)
    {
        int y;
        if(d || M(out y))
            y = 10;
        return y; 
    }
}

Ich sehe keinen Grund, warum das illegal sein sollte; Wenn Sie Dynamic durch Bool ersetzen, wird es problemlos kompiliert.

Ich treffe mich morgen mit dem C # -Team. Ich werde es ihnen gegenüber erwähnen. Entschuldigung für den Fehler!

Eric Lippert
quelle
6
Ich bin nur froh zu wissen, dass ich nicht verrückt werde :) Ich habe seitdem meinen Code aktualisiert, um mich nur auf TryParse zu verlassen, also bin ich jetzt bereit. Vielen Dank für Ihren Einblick!
Brandon Martinez
4
@NominSim: Angenommen, die Laufzeitanalyse schlägt fehl: Dann wird eine Ausnahme ausgelöst, bevor die lokale gelesen wird. Angenommen, die Laufzeitanalyse ist erfolgreich: Zur Laufzeit ist entweder d wahr und y ist gesetzt, oder d ist falsch und M setzt y. In beiden Fällen ist y festgelegt. Die Tatsache, dass die Analyse bis zur Laufzeit verschoben wird, ändert nichts.
Eric Lippert
2
Für den Fall, dass jemand neugierig ist: Ich habe es gerade überprüft und der Mono-Compiler macht es richtig. imgur.com/g47oquT
Dan Tao
17
Ich denke, das Compiler-Verhalten ist tatsächlich korrekt, da der Wert von dvon einem Typ mit einem überladenen trueOperator sein kann. Ich habe eine Antwort mit einem Beispiel gepostet, in dem keiner der Zweige verwendet wird.
Quartiermeister
2
@Quartermeister in welchem ​​Fall der Mono-Compiler es falsch macht :)
porges
52

Es ist möglich, dass die Variable nicht zugewiesen wird, wenn der Wert des dynamischen Ausdrucks von einem Typ mit einem überladenen trueOperator ist .

Der ||Bediener ruft den trueBediener auf, um zu entscheiden, ob die rechte Seite bewertet werden soll, und dann ifruft die Anweisung den trueBediener auf, um zu entscheiden, ob der Körper bewertet werden soll. Für einen Normalen geben booldiese immer das gleiche Ergebnis zurück und so wird genau eines ausgewertet, aber für einen benutzerdefinierten Operator gibt es keine solche Garantie!

Aufbauend auf Eric Lipperts Repro ist hier ein kurzes und vollständiges Programm, das einen Fall demonstriert, in dem keiner der Pfade ausgeführt wird und die Variable ihren Anfangswert hat:

using System;

class Program
{
    static bool M(out int x)
    {
        x = 123;
        return true;
    }

    static int N(dynamic d)
    {
        int y = 3;
        if (d || M(out y))
            y = 10;
        return y;
    }

    static void Main(string[] args)
    {
        var result = N(new EvilBool());
        // Prints 3!
        Console.WriteLine(result);
    }
}

class EvilBool
{
    private bool value;

    public static bool operator true(EvilBool b)
    {
        // Return true the first time this is called
        // and false the second time
        b.value = !b.value;
        return b.value;
    }

    public static bool operator false(EvilBool b)
    {
        throw new NotImplementedException();
    }
}
Quartiermeister
quelle
8
Gute Arbeit hier. Ich habe dies an die C # -Test- und Designteams weitergegeben. Ich werde sehen, ob sie Kommentare dazu haben, wenn ich sie morgen sehe.
Eric Lippert
3
Das ist sehr seltsam für mich. Warum sollte dzweimal bewertet werden? (Ich bestreite nicht, dass dies eindeutig der Fall ist , wie Sie gezeigt haben.) Ich hätte erwartet, dass das ausgewertete Ergebnis true(vom ersten Operatoraufruf, verursacht durch ||) an die ifAnweisung "weitergegeben" wird . Das würde sicherlich passieren, wenn Sie dort beispielsweise einen Funktionsaufruf einfügen.
Dan Tao
3
@DanTao: Der Ausdruck dwird erwartungsgemäß nur einmal ausgewertet. Es ist der trueOperator, der zweimal aufgerufen wird, einmal nach ||und nach if.
Quartiermeister
2
@DanTao: Es könnte klarer sein, wenn wir sie auf separate Anweisungen setzen als var cond = d || M(out y); if (cond) { ... }. Zuerst werten wir aus d, um eine EvilBoolObjektreferenz zu erhalten . Um das zu bewerten ||, rufen wir zuerst EvilBool.truemit dieser Referenz auf. Das gibt true zurück, also schließen wir kurz und rufen nicht auf Mund weisen dann die Referenz zu cond. Dann fahren wir mit der ifAussage fort. Die ifAnweisung wertet ihren Zustand durch Aufrufen aus EvilBool.true.
Quartiermeister
2
Das ist wirklich cool. Ich hatte keine Ahnung, dass es einen wahren oder falschen Operator gibt.
IllidanS4 will Monica
7

Von MSDN (Schwerpunkt Mine):

Der dynamische Typ ermöglicht es den Operationen, in denen er auftritt, die Typprüfung zur Kompilierungszeit zu umgehen . Stattdessen werden diese Vorgänge zur Laufzeit aufgelöst . Der dynamische Typ vereinfacht den Zugriff auf COM-APIs wie die Office Automation-APIs sowie auf dynamische APIs wie IronPython-Bibliotheken und auf das HTML Document Object Model (DOM).

Typendynamik verhält sich in den meisten Fällen wie Typobjekt. Allerdings Operationen , die Ausdrücke vom Typ dynamischen enthalten , werden nicht aufgelöst oder Typ vom Compiler geprüft.

Da der Compiler keine Operationen prüft oder auflöst, die Ausdrücke vom Typ dynamisch enthalten, kann er nicht sicherstellen, dass die Variable mithilfe von zugewiesen wird TryParse().

NominSim
quelle
Wenn die erste Bedingung erfüllt ist, numberGroupswird zugewiesen (im if trueBlock), wenn nicht, garantiert die zweite Bedingung die Zuweisung (via out).
Leppie
1
Das ist ein interessanter Gedanke, aber der Code lässt sich gut kompilieren, ohne das myString == null(nur das TryParse).
Brandon Martinez
1
@leppie Der Punkt ist, dass die erste Bedingung (in der Tat der gesamte ifAusdruck) eine dynamicVariable enthält und zur Kompilierungszeit nicht aufgelöst wird (der Compiler kann diese Annahmen daher nicht treffen).
NominSim
@NominSim: Ich verstehe Ihren Standpunkt :) +1 Könnte ein Opfer des Compilers sein (Verstoß gegen C # -Regeln), aber andere Vorschläge scheinen einen Fehler zu implizieren. Erics Ausschnitt zeigt, dass dies kein Opfer, sondern ein Fehler ist.
Leppie
@NominSim Das kann nicht richtig sein; Nur weil bestimmte Compilerfunktionen verzögert werden, heißt das nicht, dass alle verzögert sind. Es gibt zahlreiche Belege dafür, dass der Compiler unter leicht unterschiedlichen Umständen die definitive Zuweisungsanalyse trotz des Vorhandenseins eines dynamischen Ausdrucks ohne Probleme durchführt.
dlev