Roslyn nie mógł skompilować kodu

95

Po migracji projektu z VS2013 do VS2015 projekt nie jest już kompilowany. Błąd kompilacji występuje w następującej instrukcji LINQ:

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

Kompilator zwraca błąd:

Błąd CS0165 Użycie nieprzypisanej zmiennej lokalnej „b”

Co powoduje ten problem? Czy można to naprawić za pomocą ustawień kompilatora?

ramil89
źródło
11
@BinaryWorrier: Dlaczego? Używa tylko bpo przypisaniu go za pomocą outparametru.
Jon Skeet
1
Dokumentacja VS 2015 mówi: „Chociaż zmienne przekazywane jako argumenty wyjściowe nie muszą być inicjowane przed przekazaniem, wywołana metoda jest wymagana do przypisania wartości, zanim metoda zwróci”. więc to wygląda na błąd tak, jest gwarantowane, że zostanie zainicjowane przez tę tryParse.
Rup
3
Niezależnie od błędu, ten kod ilustruje wszystko, co złe w outargumentach. Czy to TryParsezwróciło wartość dopuszczającą wartość null (lub równoważną).
Konrad Rudolph
1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bwygląda znacznie lepiej
Rawling
2
Uwaga, możesz uprościć to do decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Mam otwarte błąd Roslyn podnoszenie tego.
Rawling

Odpowiedzi:

112

Co powoduje ten problem?

Wydaje mi się, że to błąd kompilatora. A przynajmniej tak. Chociaż wyrażenia decimal.TryParse(v, out a)i decimal.TryParse(v, out b)są obliczane dynamicznie, spodziewałem się, że kompilator nadal zrozumie, że zanim dotrze a <= b, oba ai bzostaną ostatecznie przypisane. Nawet z dziwactwami, które możesz wymyślić podczas dynamicznego pisania, spodziewałbym się, że kiedykolwiek ocenię tylko a <= bpo ocenie obu TryParsewywołań.

Jednak okazuje się, że za pośrednictwem operatora i nawrócenia trudne, jest to całkowicie możliwe, aby mieć wyraz A && B && Cktóra ocenia Ai C, ale nie B- Jeśli jesteś przebiegły dość. Zobacz raport o błędzie Roslyn, aby zapoznać się z genialnym przykładem Neala Gaftera.

Wykonanie tej pracy dynamicjest jeszcze trudniejsze - semantyka stosowana, gdy operandy są dynamiczne, jest trudniejsza do opisania, ponieważ aby wykonać rozwiązanie przeciążenia, musisz ocenić operandy, aby dowiedzieć się, jakie typy są zaangażowane, co może być sprzeczne z intuicją. Jednak ponownie Neal wymyślił przykład, który pokazuje, że błąd kompilatora jest wymagany ... to nie jest błąd, to naprawa błędu . Ogromne uznanie dla Neala za udowodnienie tego.

Czy można to naprawić poprzez ustawienia kompilatora?

Nie, ale istnieją alternatywy, które pozwalają uniknąć błędu.

Po pierwsze, możesz zatrzymać dynamikę - jeśli wiesz, że będziesz używać tylko łańcuchów, możesz użyć IEnumerable<string> lub nadać zmiennej zakresu vtyp string(tj from string v in array.). To byłaby moja preferowana opcja.

Jeśli naprawdę chcesz zachować dynamikę, po prostu podaj bwartość na początek:

decimal a, b = 0m;

To nie zaszkodzi - wiemy, że tak naprawdę twoja dynamiczna ocena nie zrobi nic szalonego, więc nadal będziesz przypisywać wartość bprzed jej użyciem, sprawiając, że początkowa wartość będzie nieistotna.

Dodatkowo wydaje się, że dodawanie nawiasów też działa:

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

To zmienia punkt, w którym wyzwalane są różne elementy rozwiązania przeciążenia, i sprawia, że ​​kompilator jest zadowolony.

Pozostaje jeszcze jedna kwestia - zasady specyfikacji dotyczące określonego przypisania do &&operatora muszą zostać wyjaśnione, aby stwierdzić, że mają one zastosowanie tylko wtedy, gdy &&operator jest używany w jego „zwykłej” implementacji z dwoma booloperandami. Spróbuję się upewnić, że zostanie to naprawione dla następnego standardu ECMA.

Jon Skeet
źródło
Tak! Stosowanie IEnumerable<string>lub dodawanie nawiasów zadziałało. Teraz kompilator kompiluje się bez błędów.
ramil89
1
użycie decimal a, b = 0m;może usunąć błąd, ale a <= bzawsze będzie używać 0m, ponieważ wartość wyjściowa nie została jeszcze obliczona.
Paw Baltzersen,
12
@PawBaltzersen: Dlaczego tak myślisz? Zawsze zostanie przypisany przed porównaniem - po prostu kompilator nie może tego udowodnić, z jakiegoś powodu (w zasadzie błąd).
Jon Skeet
1
Posiadanie metody analizy bez efektu ubocznego, tj. decimal? TryParseDecimal(string txt)może być też rozwiązaniem
zahir
1
Zastanawiam się, czy to leniwa inicjalizacja; myśli: „jeśli pierwsza jest prawdziwa, to nie muszę oceniać drugiej, co oznacza, bże może nie być przypisana”; Wiem, że to nieprawidłowe rozumowanie, ale wyjaśnia, dlaczego nawiasy to naprawiają ...
durron597
16

Ponieważ tak ciężko uczył mnie w raporcie o błędzie, spróbuję sam to wyjaśnić.


Wyobraź sobie, że Tjest to typ zdefiniowany przez użytkownika z niejawnym rzutowaniem na boolnaprzemiennie między falsei true, zaczynając od false. O ile kompilator wie, dynamicpierwszy argument do pierwszego &&może mieć wartość tego typu, więc musi być pesymistyczny.

Jeśli więc pozwoli na kompilację kodu, może się to zdarzyć:

  • Gdy spinacz dynamiczny ocenia pierwszy &&, wykonuje następujące czynności:
    • Oceń pierwszy argument
    • Jest to T- niejawnie rzut do bool.
    • Och, tak false, więc nie musimy oceniać drugiego argumentu.
    • Wynik &&oceny należy traktować jako pierwszy argument. (Nie, nie false, z jakiegoś powodu.)
  • Gdy spinacz dynamiczny ocenia drugi &&, wykonuje następujące czynności:
    • Oceń pierwszy argument.
    • Jest to T- niejawnie rzut do bool.
    • Och, tak true, więc oceń drugi argument.
    • ... O cholera, bnie przydzielono.

Mówiąc konkretnie, w skrócie, istnieją specjalne reguły „określonego przypisania”, które pozwalają nam powiedzieć nie tylko, czy zmienna jest „definitywnie przypisana”, czy „nieprzypisana ostatecznie”, ale także czy jest „zdecydowanie przypisana po falsewyrażeniu” lub „zdecydowanie przypisane po truestwierdzeniu ”.

Istnieją one tak, że podczas pracy z &&i ||(i !i ??i ?:) kompilator może zbadać, czy zmienne mogą być przypisane do poszczególnych gałęzi złożonego wyrażenia boolowskiego.

Jednak działają one tylko wtedy, gdy typy wyrażeń pozostają logiczne . Gdy część wyrażenia jest dynamic(lub nie jest logicznym typem statycznym), nie możemy już wiarygodnie powiedzieć, że wyrażenie to jest truelub false- następnym razem, gdy rzucimy je, boolaby zdecydować, którą gałąź wziąć, mogło zmienić zdanie.


Aktualizacja: to zostało już rozwiązane i udokumentowane :

Określone reguły przypisywania zaimplementowane przez poprzednie kompilatory dla wyrażeń dynamicznych pozwoliły na niektóre przypadki kodu, które mogą spowodować odczytanie zmiennych, które nie są ostatecznie przypisane. Zobacz https://github.com/dotnet/roslyn/issues/4509, aby uzyskać jeden raport na ten temat.

...

Z tego powodu kompilator nie może pozwolić na kompilację tego programu, jeśli val nie ma wartości początkowej. Poprzednie wersje kompilatora (przed VS2015) pozwalały temu programowi na kompilację, nawet jeśli val nie ma wartości początkowej. Roslyn teraz diagnozuje tę próbę odczytania prawdopodobnie niezainicjowanej zmiennej.

Rawling
źródło
1
Używając VS2013 na moim drugim komputerze, faktycznie udało mi się odczytać nieprzypisaną pamięć za pomocą tego. To nie jest zbyt ekscytujące :(
Rawling
Możesz odczytać niezainicjowane zmienne za pomocą prostego delegata. Utwórz delegata, który przejdzie outdo metody, która ma ref. Z przyjemnością to zrobi i przypisze zmienne bez zmiany wartości.
IllidanS4 chce, aby Monica wróciła
Z ciekawości przetestowałem ten fragment kodu w C # v4. Z ciekawości jednak - jak kompilator decyduje się na użycie operatora false/ truew przeciwieństwie do niejawnego operatora rzutowania? Lokalnie wywoła implicit operator boolpierwszy argument, następnie wywoła drugi operand, wywoła operator falsepierwszy operand, a następnie ponownieimplicit operator bool pierwszy operand . To nie ma dla mnie sensu, pierwszy operand powinien kiedyś sprowadzać się do wartości logicznej, prawda?
Rob
@Rob Czy to jest dynamicprzykuta &&sprawa? Widziałem, że w zasadzie idę (1) ocenić pierwszy argument (2) użyć niejawnego rzutowania, aby sprawdzić, czy mogę zwrzeć (3) nie mogę, więc pomiń drugi argument (4) teraz znam oba typy, ja widzę najlepiej &&zdefiniowany przez użytkownika &(5) operator wywołania falsena pierwszym argumencie, aby sprawdzić, czy mogę zwrzeć (6) Mogę (ponieważ falsei się implicit boolnie zgadzam), więc wynikiem jest pierwszy argument ... a następnie następny &&, (7) użyj niejawnego rzutowania, aby sprawdzić, czy mogę zwrzeć (ponownie).
Rawling
@ IllidanS4 Brzmi interesująco, ale nie dowiedziałem się, jak to zrobić. Czy możesz dać mi fragment?
Rawling
15

To nie jest błąd. Zobacz https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713, aby zobaczyć przykład, jak dynamiczne wyrażenie tego formularza może pozostawić taką zmienną out nieprzypisaną.

Neal Gafter
źródło
1
Ponieważ moja odpowiedź została przyjęta i bardzo pozytywnie oceniona, zredagowałem ją, aby wskazać rezolucję. Dziękuję za całą twoją pracę nad tym - łącznie z wyjaśnieniem mi mojego błędu :)
Jon Skeet