Dlaczego ten warunek (null ||! TryParse) powoduje „użycie nieprzypisanej zmiennej lokalnej”?

98

Poniższy kod powoduje użycie nieprzypisanej zmiennej lokalnej „numberOfGroups” :

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

Jednak ten kod działa dobrze (chociaż ReSharper twierdzi, że = 10jest zbędny):

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

Czy coś mi brakuje, czy kompilator nie lubi mojego ||?

Zawęziłem to do dynamicpowodowania problemów ( optionsbyła to zmienna dynamiczna w moim powyższym kodzie). Pozostaje pytanie, dlaczego nie mogę tego zrobić ?

Ten kod nie kompiluje się:

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
}

Jednak ten kod robi :

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
}

Nie zdawałem sobie sprawy, dynamicże będzie to miało znaczenie.

Brandon Martinez
źródło
Nie myśl, że wystarczy wiedzieć, że nie używasz wartości przekazanej do outparametru jako danych wejściowych
Charleh
3
Podany tutaj kod nie demonstruje opisanego zachowania; działa dobrze. Prześlij kod, który faktycznie demonstruje opisywane przez Ciebie zachowanie, a który możemy samodzielnie skompilować. Daj nam cały plik.
Eric Lippert
8
Ach, teraz mamy coś ciekawego!
Eric Lippert
1
Nic dziwnego, że kompilator jest przez to zdezorientowany. Kod pomocniczy dla witryny wywołań dynamicznych prawdopodobnie ma pewien przepływ sterowania, który nie gwarantuje przypisania do outparametru. Z pewnością warto zastanowić się, jaki kod pomocniczy powinien wygenerować kompilator, aby uniknąć problemu lub jeśli jest to w ogóle możliwe.
CodesInChaos
1
Na pierwszy rzut oka wygląda to na błąd.
Eric Lippert

Odpowiedzi:

73

Jestem prawie pewien, że jest to błąd kompilatora. Niezłe znalezisko!

Edycja: to nie jest błąd, jak demonstruje Quartermeister; Dynamic może zaimplementować dziwny trueoperator, który może spowodować, yże nigdy nie zostanie zainicjowany.

Oto minimalna reprodukcja:

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

Nie widzę powodu, dla którego miałoby to być nielegalne; jeśli zamienisz dynamic na bool, kompiluje się dobrze.

Właściwie mam jutro spotkanie z zespołem C #; Wspomnę im o tym. Przepraszamy za błąd!

Eric Lippert
źródło
6
Po prostu cieszę się, że nie wariuję :) Od tego czasu zaktualizowałem swój kod, aby polegać tylko na TryParse, więc na razie jestem ustawiony. Dzięki za wgląd!
Brandon Martinez
4
@NominSim: Załóżmy, że analiza środowiska wykonawczego nie powiedzie się: wtedy wyjątek jest zgłaszany przed odczytaniem lokalnego. Załóżmy, że analiza w czasie wykonywania powiedzie się: wtedy w czasie wykonywania albo d jest prawdziwe, a y jest ustawione, albo d jest fałszywe, a M ustawia y. Tak czy inaczej, y jest ustawione. Fakt, że analiza jest odroczona do czasu wykonania, niczego nie zmienia.
Eric Lippert
2
Na wypadek, gdyby ktoś był ciekawy: właśnie sprawdziłem i kompilator Mono robi to dobrze. imgur.com/g47oquT
Dan Tao
17
Myślę, że zachowanie kompilatora jest w rzeczywistości poprawne, ponieważ wartość dmoże być typu z przeciążonym trueoperatorem. Wysłałem odpowiedź z przykładem, w którym żadna gałąź nie jest zajęta.
Kwatermistrz
2
@Quartermeister w takim przypadku kompilator Mono robi to źle :)
porge
52

Zmienna może zostać nieprzypisana, jeśli wartość wyrażenia dynamicznego jest typu z przeciążonym trueoperatorem .

||Operator będzie wywołać trueoperatora zdecydować, czy do oceny prawą stronę, a następnie ifoświadczenie będzie wywołać trueoperatora do decydowania o ocenę jego ciało. W przypadku normalnych wyników boolzawsze będą one zwracać ten sam wynik, więc dokładnie jeden zostanie oceniony, ale w przypadku operatora zdefiniowanego przez użytkownika nie ma takiej gwarancji!

Opierając się na reprodukcji Erica Lipperta, oto krótki i kompletny program, który demonstruje przypadek, w którym żadna ścieżka nie zostanie wykonana, a zmienna będzie miała swoją początkową wartość:

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();
    }
}
Kwatermistrz
źródło
8
Dobra robota. Przekazałem to razem zespołom testowym i projektowym języka C #; Zobaczę, czy jutro będą mieli jakieś uwagi na ten temat.
Eric Lippert
3
To jest dla mnie bardzo dziwne. Dlaczego należy doceniać dwukrotnie? (Nie zaprzeczam, że tak jest , jak pokazałeś). Spodziewałbym się, że oszacowany wynik true(od pierwszego wywołania operatora, przyczyna by ||) zostanie „przekazany” do ifinstrukcji. Z pewnością tak by się stało, gdybyś na przykład umieścił tam wywołanie funkcji.
Dan Tao
3
@DanTao: Wyrażenie djest oceniane tylko raz, zgodnie z oczekiwaniami. Jest to trueoperator wywoływany dwukrotnie, raz ||i raz if.
Quartermeister
2
@DanTao: Mogłoby być bardziej jasne, gdybyśmy umieścili je w osobnych oświadczeniach, jak var cond = d || M(out y); if (cond) { ... }. Najpierw oceniamy, daby uzyskać EvilBoolodniesienie do obiektu. Aby ocenić ||, najpierw wywołujemy EvilBool.truez tym odniesieniem. To zwraca prawdę, więc dokonujemy zwarcia i nie wywołujemy M, a następnie przypisujemy odwołanie do cond. Następnie przechodzimy do ifoświadczenia. ifSprawozdanie ocenia swój stan poprzez wywołanie EvilBool.true.
Quartermeister
2
Teraz to jest naprawdę fajne. Nie miałem pojęcia, że ​​istnieje operator prawda lub fałsz.
IllidanS4 chce, aby Monica wróciła
7

Z MSDN (wyróżnienie moje):

Typ dynamiczny umożliwia operacjom, w których występuje, pomijanie sprawdzania typu w czasie kompilacji . Zamiast tego te operacje są rozwiązywane w czasie wykonywania . Typ dynamiczny upraszcza dostęp do interfejsów API COM, takich jak interfejsy API automatyzacji pakietu Office, a także do dynamicznych interfejsów API, takich jak biblioteki IronPython, oraz do modelu obiektu dokumentu HTML (DOM).

Dynamiczny typ zachowuje się w większości przypadków jak obiekt tekstu. Jednak operacje zawierające wyrażenia typu dynamic nie są rozpoznawane ani sprawdzane przez kompilator.

Ponieważ kompilator nie sprawdza typów ani nie rozwiązuje żadnych operacji, które zawierają wyrażenia typu dynamic, nie może zagwarantować, że zmienna zostanie przypisana za pomocą TryParse().

NominSim
źródło
Jeśli pierwszy warunek jest spełniony, numberGroupsjest przypisywany (w if truebloku), jeśli nie, drugi warunek gwarantuje przypisanie (przez out).
leppie
1
To interesująca myśl, ale kod kompiluje się dobrze bez myString == null(polegając tylko TryParse).
Brandon Martinez
1
@leppie Chodzi o to, że skoro pierwszy warunek (a więc całe ifwyrażenie) dotyczy dynamiczmiennej, nie jest ona rozwiązywana w czasie kompilacji (kompilator nie może zatem przyjąć takich założeń).
NominSim
@NominSim: Rozumiem, o co ci chodzi :) +1 Może to być poświęcenie ze strony kompilatora (łamanie zasad C #), ale inne sugestie sugerują błąd. Fragment Erica pokazuje, że to nie jest ofiara, ale błąd.
leppie
@NominSim To nie może być prawda; tylko dlatego, że niektóre funkcje kompilatora są odroczone, nie oznacza, że ​​wszystkie są. Istnieje wiele dowodów na to, że w nieco innych okolicznościach kompilator bez problemu przeprowadza analizę określonego przypisania, pomimo obecności wyrażenia dynamicznego.
dlev