Uwaga: wydaje się, że zostało to naprawione w Roslyn
To pytanie powstało, pisząc moją odpowiedź na to , które mówi o asocjatywności operatora zerowego koalescencji .
Przypominamy, że operatorem zerowego koalescencji jest wyrażenie formy
x ?? y
najpierw ocenia x
, a następnie:
- Jeśli wartość
x
jest równa null,y
jest obliczana i jest to końcowy wynik wyrażenia - Jeśli wartość
x
jest różna od null, niey
jest obliczana, a wartość jest końcowym wynikiem wyrażenia po konwersji na typ czasu kompilacji w razie potrzebyx
y
Teraz zwykle nie ma potrzeby konwersji, lub jest to po prostu z typu zerowalnego na nieuznawalny - zwykle typy są takie same lub po prostu (powiedzmy) int?
do int
. Jednakże, można tworzyć własne operatory konwersji niejawnych, a te są stosowane tam, gdzie jest to konieczne.
W prostym przypadku x ?? y
nie widziałem żadnego dziwnego zachowania. Jednak (x ?? y) ?? z
widzę pewne mylące zachowanie.
Oto krótki, ale kompletny program testowy - wyniki znajdują się w komentarzach:
using System;
public struct A
{
public static implicit operator B(A input)
{
Console.WriteLine("A to B");
return new B();
}
public static implicit operator C(A input)
{
Console.WriteLine("A to C");
return new C();
}
}
public struct B
{
public static implicit operator C(B input)
{
Console.WriteLine("B to C");
return new C();
}
}
public struct C {}
class Test
{
static void Main()
{
A? x = new A();
B? y = new B();
C? z = new C();
C zNotNull = new C();
Console.WriteLine("First case");
// This prints
// A to B
// A to B
// B to C
C? first = (x ?? y) ?? z;
Console.WriteLine("Second case");
// This prints
// A to B
// B to C
var tmp = x ?? y;
C? second = tmp ?? z;
Console.WriteLine("Third case");
// This prints
// A to B
// B to C
C? third = (x ?? y) ?? zNotNull;
}
}
Więc mamy trzy typy wartości zwyczaj, A
, B
i C
, z konwersji z punktu A do B, A do C, a B do C.
Rozumiem zarówno drugi przypadek, jak i trzeci przypadek ... ale dlaczego w pierwszym przypadku występuje dodatkowa konwersja A do B. W szczególności, bym naprawdę nie spodziewał się, że pierwszy przypadek i drugi przypadek będzie to samo - to tylko wydobywania wyrażenia do zmiennej lokalnej, po wszystkim.
Masz ochotę na to, co się dzieje? Jestem wyjątkowo niezdecydowany, by wykrzykiwać „błąd”, jeśli chodzi o kompilator C #, ale jestem zaskoczony tym, co się dzieje ...
EDYCJA: OK, oto nieprzyjemny przykład tego, co się dzieje, dzięki odpowiedzi konfiguratora, która daje mi kolejny powód, by myśleć, że to błąd. EDYCJA: Próbka nie potrzebuje teraz nawet dwóch zerowych operatorów koalescencyjnych ...
using System;
public struct A
{
public static implicit operator int(A input)
{
Console.WriteLine("A to int");
return 10;
}
}
class Test
{
static A? Foo()
{
Console.WriteLine("Foo() called");
return new A();
}
static void Main()
{
int? y = 10;
int? result = Foo() ?? y;
}
}
Wynikiem tego jest:
Foo() called
Foo() called
A to int
Fakt, że Foo()
tutaj wywoływane są dwa razy, jest dla mnie ogromnie zaskakujący - nie widzę żadnego powodu, aby wyrażenie było oceniane dwukrotnie.
źródło
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Otrzymasz:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Odpowiedzi:
Dziękujemy wszystkim, którzy przyczynili się do analizy tego problemu. Jest to oczywiście błąd kompilatora. Wydaje się, że dzieje się to tylko wtedy, gdy następuje podniesienie konwersji obejmujące dwa typy zerowalne po lewej stronie operatora koalescencyjnego.
Nie określiłem jeszcze, gdzie dokładnie coś pójdzie nie tak, ale w pewnym momencie podczas fazy kompilacji „obniżania wartości zerowych” - po wstępnej analizie, ale przed wygenerowaniem kodu - zmniejszamy wyrażenie
z powyższego przykładu do moralnego odpowiednika:
Oczywiście jest to niepoprawne; prawidłowe obniżenie to
Moim najlepszym przypuszczeniem na podstawie moich dotychczasowych analiz jest to, że optymalizator dopuszczający wartości zerowe zjeżdża tutaj z torów. Mamy optymalizator null, który wyszukuje sytuacje, w których wiemy, że określone wyrażenie typu nullable nie może być zerowe. Rozważ następującą naiwną analizę: moglibyśmy to najpierw powiedzieć
jest taki sam jak
i moglibyśmy to powiedzieć
jest taki sam jak
Ale optymalizator może wkroczyć i powiedzieć: „zaraz, poczekaj chwilę, już sprawdziliśmy, że temp nie jest zerowy; nie ma potrzeby sprawdzania go ponownie po raz drugi tylko dlatego, że nazywamy podniesionym operatorem konwersji”. Zoptymalizowalibyśmy to do sprawiedliwego
Domyślam się, że gdzieś zapamiętujemy fakt, że zoptymalizowana forma
(int?)Foo()
jest,new int?(op_implicit(Foo().Value))
ale tak naprawdę nie jest to zoptymalizowana forma, jakiej chcemy; chcemy zoptymalizowanej formy Foo () - zamienionej na-tymczasową-a-następnie-przekonwertowaną.Wiele błędów w kompilatorze C # jest wynikiem złych decyzji dotyczących buforowania. Słowo dla mądrych: za każdym razem, gdy buforujesz fakt do późniejszego wykorzystania, potencjalnie tworzysz niespójność, jeśli coś istotnego się zmieni . W tym przypadku istotną rzeczą, która zmieniła się po wstępnej analizie, jest to, że wywołanie Foo () powinno zawsze być realizowane jako pobranie tymczasowe.
W C # 3.0 przeprowadziliśmy wiele reorganizacji zerowej przepustki przepisywania. Błąd jest odtwarzany w C # 3.0 i 4.0, ale nie w C # 2.0, co oznacza, że błąd był prawdopodobnie moim złym. Przepraszam!
Dostanę błąd do bazy danych i zobaczymy, czy uda nam się to naprawić w przyszłej wersji języka. Jeszcze raz dziękuję wszystkim za analizę; to było bardzo pomocne!
AKTUALIZACJA: Przepisałem optymalizator null od zera dla Roslyn; teraz wykonuje lepszą pracę i pozwala uniknąć tego rodzaju dziwnych błędów. Aby dowiedzieć się, jak działa optymalizator w Roslyn, zobacz moją serię artykułów, która zaczyna się tutaj: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
źródło
To zdecydowanie błąd.
Ten kod wyświetli:
To sprawiło, że pomyślałem, że pierwsza część każdego
??
wyrażenia koalescencji jest oceniana dwukrotnie. Ten kod to udowodnił:wyjścia:
Wydaje się, że dzieje się tak tylko wtedy, gdy wyrażenie wymaga konwersji między dwoma typami zerowalnymi; Próbowałem różnych kombinacji z jedną ze stron będących sznurkiem i żadna z nich nie spowodowała takiego zachowania.
źródło
X() ?? Y()
rozszerza się wewnętrznieX() != null ? X() : Y()
, dlatego zostałby oceniony dwukrotnie.Jeśli spojrzysz na wygenerowany kod dla sprawy pogrupowanej w lewo, faktycznie robi coś takiego (
csc /optimize-
):Innym znalezisko, jeśli używać
first
będzie generować skrót jeśli obaa
ib
są nieważne i powrotuc
. Jeśli jednaka
czyb
jest niezerowe ponownie je oceniaa
jako część niejawna konwersja doB
przed powrotem, który za
lubb
jest niezerowe.Ze specyfikacji C # 4.0, pkt 6.1.4:
To wydaje się wyjaśniać drugą kombinację rozpakowywania i owijania.
Kompilator C # 2008 i 2010 produkuje bardzo podobny kod, jednak wygląda to na regresję z kompilatora C # 2005 (8.00.50727.4927), który generuje następujący kod dla powyższego:
Zastanawiam się, czy nie wynika to z dodatkowej magii nadanej systemowi wnioskowania typu?
źródło
(x ?? y) ?? z
w zagnieżdżone lambdy, co zapewnia ocenę w kolejności bez podwójnej oceny. Oczywiście nie jest to podejście kompilatora C # 4.0. Z tego, co mogę powiedzieć, do sekcji 6.1.4 podchodzimy bardzo ściśle w tej konkretnej ścieżce kodu, a tymczasowe elementy nie są pomijane, co powoduje podwójną ocenę.Właściwie to nazwę to teraz błędem, z wyraźniejszym przykładem. Nadal tak jest, ale podwójna ocena z pewnością nie jest dobra.
Wygląda na
A ?? B
to, że jest zaimplementowany jakoA.HasValue ? A : B
. W tym przypadku jest też dużo rzutowania (po zwykłym rzutowaniu dla?:
operatora trójskładnikowego ). Ale jeśli zignorujesz to wszystko, ma to sens w zależności od tego, jak jest to zaimplementowane:A ?? B
rozwija się doA.HasValue ? A : B
A
jest naszx ?? y
. Rozwiń dox.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Tutaj możesz zobaczyć, że
x.HasValue
jest sprawdzany dwukrotnie, a jeślix ?? y
wymaga rzucenia,x
zostanie rzucony dwukrotnie.Odłożyłbym to po prostu jako artefakt, w jaki sposóbNa wynos: Nie twórz niejawnych operatorów rzutujących z efektami ubocznymi.??
jest implementowany, a nie błąd kompilatora.Wygląda na to, że jest to błąd kompilatora, który dotyczy sposobu
??
implementacji. Na wynos: nie zagnieżdżaj wyrażeń koalescencyjnych z efektami ubocznymi.źródło
A() ? A() : B()
prawdopodobnie to oceniA()
dwa razy, aleA() ?? B()
nie tak bardzo. A ponieważ zdarza się to tylko podczas castingu ... Hmm .. Właśnie powiedziałem sobie, że z pewnością nie zachowuje się poprawnie.W ogóle nie jestem ekspertem od C #, jak widać z mojej historii pytań, ale wypróbowałem to i myślę, że to błąd .... ale jako nowicjusz muszę powiedzieć, że nie rozumiem, co się dzieje tutaj, więc usunę odpowiedź, jeśli jestem daleko.
Doszedłem do tego
bug
wniosku, tworząc inną wersję twojego programu, która dotyczy tego samego scenariusza, ale o wiele mniej skomplikowaną.Korzystam z trzech właściwości zerowych liczb całkowitych ze sklepami zapasowymi. Ustawiam każdy na 4, a następnie biegnę
int? something2 = (A ?? B) ?? C;
( Pełny kod tutaj )
To po prostu czyta A i nic więcej.
To oświadczenie dla mnie wygląda tak, jak powinno:
Ponieważ A nie jest zerowe, patrzy tylko na A i kończy.
W twoim przykładzie umieszczenie punktu przerwania w Pierwszym Przypadku pokazuje, że x, y i z nie są równe zeru i dlatego spodziewałbym się, że będą traktowane tak samo jak mój mniej złożony przykład .... ale obawiam się, że jestem za bardzo nowicjusza z C # i całkowicie przegapiłem sedno tego pytania!
źródło
int
). Popycha skrzynkę dalej w niejasny kąt, zapewniając wiele niejawnych konwersji typu. Wymaga to od kompilatora zmiany typu danych podczas sprawdzanianull
. To z powodu tych domyślnych konwersji jego przykład różni się od twojego.