przełącznik z dziwnym zachowaniem var / null

91

Biorąc pod uwagę następujący kod:

string someString = null;
switch (someString)
{
    case string s:
        Console.WriteLine("string s");
        break;
    case var o:
        Console.WriteLine("var o");
        break;
    default:
        Console.WriteLine("default");
        break;
}

Dlaczego instrukcja switch jest zgodna case var o?

Rozumiem, że case string snie pasuje, kiedy, s == nullponieważ (skutecznie) (null as string) != nullocenia jako fałsz. IntelliSense w programie VS Code mówi mi, że oto stringrównież. jakieś pomysły?


Podobny do: Przypadek przełącznika C # 7 z weryfikacją wartości null

budi
źródło
9
Potwierdzony. Kocham to pytanie, zwłaszcza z obserwacją, że ojest string(potwierdzone z rodzajowych - czyli Foo(o)gdzie Foo<T>(T template) => typeof(T).Name) - jest to bardzo ciekawy przypadek, w którym string xzachowuje się inaczej niż var xnawet gdy xjest wpisane (przez kompilator) jakostring
Marc Gravell
7
Domyślny przypadek to martwy kod. Uwierz, że powinniśmy tam wydać ostrzeżenie. Kontrola.
JaredPar
13
Dziwne jest dla mnie, że projektanci C # varw ogóle zdecydowali się na to w tym kontekście. To z pewnością wydaje się być czymś, co znajdę w C ++, a nie w języku, który rzekomo prowadzi programistę „do otchłani sukcesu”. Tutaj varjest zarówno niejednoznaczne, jak i bezużyteczne, rzeczy, których projekt C # zwykle stara się unikać.
Peter Duniho
1
@PeterDuniho Nie powiedziałbym, że jest bezużyteczny; wyrażenie przychodzące do the switchmoże być niewypowiadalne - typy anonimowe itp .; i nie jest niejednoznaczne - kompilator wyraźnie zna typ; to jest po prostu mylące (przynajmniej dla mnie), że nullzasady są tak różne!
Marc Gravell
1
@PeterDuniho zabawny fakt - kiedyś wyszukaliśmy określone reguły formalne przypisania ze specyfikacji C # 1.2, a ilustracyjny kod rozszerzający miał deklarację zmiennej wewnątrz bloku (tam, gdzie jest teraz); przeniósł się na zewnątrz dopiero w 2.0, a potem z powrotem do środka, gdy problem z przechwytywaniem był oczywisty.
Marc Gravell

Odpowiedzi:

69

Wewnątrz switchinstrukcji dopasowywania wzorców używająca a casedla typu jawnego jest pytaniem, czy dana wartość jest tego konkretnego typu, czy też typu pochodnego. To dokładny odpowiednikis

switch (someString) {
  case string s:
}
if (someString is string) 

Wartość nullnie ma typu i dlatego nie spełnia żadnego z powyższych warunków. W someStringżadnym z przykładów nie ma znaczenia typ statyczny .

varTyp choć w strukturze dopasowywania działa jako dzikie karty i będzie pasował do żadnej wartości łącznie null.

defaultSprawa tutaj jest martwy kod. case var oDopasuje jakąkolwiek wartość, null lub niezerowe. Przypadki inne niż domyślne zawsze wygrywają z przypadkami domyślnymi, więc defaultnigdy nie zostaną trafione. Jeśli spojrzysz na IL, zobaczysz, że nie jest on nawet emitowany.

Na pierwszy rzut oka może się wydawać dziwne, że kompiluje się to bez żadnego ostrzeżenia (zdecydowanie mnie wyrzuciło). Ale to jest zgodne z zachowaniem C #, które sięga wstecz do 1.0. Kompilator dopuszcza defaultprzypadki, nawet jeśli może w trywialny sposób udowodnić, że nigdy nie zostanie trafiony. Jako przykład rozważ następujące kwestie:

bool b = ...;
switch (b) {
  case true: ...
  case false: ...
  default: ...
}

Tutaj defaultnigdy nie zostanie trafiony (nawet jeśli boolma wartość inną niż 1 lub 0). Jednak C # zezwolił na to od 1.0 bez ostrzeżenia. Dopasowywanie wzorców jest tutaj zgodne z tym zachowaniem.

JaredPar
źródło
4
Prawdziwym problemem jest jednak to, że kompilator „pokazuje” varjako typ, stringkiedy naprawdę nie jest (szczerze mówiąc nie jestem pewien, jaki powinien być typ)
shmuelie
@shmuelie varjest obliczany jako typ w przykładzie string.
JaredPar
5
@JaredPar dzięki za wgląd tutaj; Osobiście poparłbym więcej emitowania ostrzeżeń, nawet jeśli wcześniej tego nie robiło, ale rozumiem ograniczenia zespołu językowego. Czy kiedykolwiek zastanawiałeś się nad „narzekaniem na wszystko” (prawdopodobnie domyślnie włączonym), a nie „starszym trybem stoickim” (do wyboru)? możecsc /stiffUpperLip
Marc Gravell
3
@MarcGravell mamy funkcję zwaną falami ostrzeżeń, która ma ułatwić wprowadzanie nowych ostrzeżeń, mniej kompatybilności. Zasadniczo każde wydanie kompilatora jest nową falą i możesz zdecydować się na ostrzeżenia za pomocą / wave: 1, / wave: 2, / wave: all.
JaredPar
4
@JonathanDickinson Nie sądzę, że to pokazuje, co myślisz, że pokazuje. To po prostu pokazuje, że nulljest prawidłowym stringodwołaniem, a każde stringodwołanie (w tym null) może być niejawnie rzutowane (zachowując referencję) na objectodwołanie, a każde objectodwołanie, które nullmoże zostać pomyślnie przesłane (jawne) do dowolnego innego typu, nadal jest null. Nie jest to to samo, jeśli chodzi o system typów kompilatora.
Marc Gravell
22

Łączę tutaj wiele komentarzy na Twitterze - to właściwie dla mnie nowość i mam nadzieję, że jaredpar skoczy z bardziej wyczerpującą odpowiedzią, ale; wersja skrócona, jak rozumiem:

case string s:

jest interpretowany jako if(someString is string) { s = (string)someString; ...lub if((s = (someString as string)) != null) { ... }- z których każdy obejmuje nulltest - który w Twoim przypadku nie powiódł się; odwrotnie:

case var o:

gdzie postanawia kompilatora ojak stringto po prostu o = (string)someString; ...- no nullbadanie, pomimo faktu, że wygląda podobnie na powierzchni, tylko z kompilatora zapewniając typ.

Wreszcie:

default:

tutaj nie można dotrzeć , ponieważ powyższy przypadek obejmuje wszystko. Może to być błąd kompilatora, ponieważ nie emitował ostrzeżenia o nieosiągalnym kodzie.

Zgadzam się, że jest to bardzo subtelne, dopracowane i zagmatwane. Ale najwyraźniej case var oscenariusz ma zastosowania z propagacją zerową ( o?.Length ?? 0itp.). Zgadzam się, że to dziwne, że działa to tak bardzo różnie w przypadku var oi string s, ale tak właśnie działa kompilator.

Marc Gravell
źródło
14

Dzieje się tak, ponieważ case <Type>pasuje do typu dynamicznego (w czasie wykonywania), a nie do typu statycznego (w czasie kompilacji). nullnie ma typu dynamicznego, więc nie może się z nim równać string. varto tylko rozwiązanie awaryjne.

(Publikuję, ponieważ lubię krótkie odpowiedzi.)

user541686
źródło