Dlaczego C # nie ma zasięgu lokalnego w blokach przypadków?

38

Pisałem ten kod:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

Był zaskoczony, gdy znalazł błąd kompilacji:

Zmienna lokalna o nazwie „akcja” jest już zdefiniowana w tym zakresie

Rozwiązanie problemu było dość łatwe; po prostu pozbycie się drugiego varrozwiązało problem.

Oczywiście zmienne zadeklarowane w caseblokach mają zakres elementu nadrzędnego switch, ale jestem ciekawy, dlaczego tak jest. Biorąc pod uwagę, że C # nie zezwala na wykonanie spadać przez innych przypadkach (to wymaga break , return, throwczy goto casesprawozdanie na koniec każdego casebloku), wydaje się dość dziwne, że pozwoliłoby to deklaracje zmiennych wewnątrz jeden casedo wykorzystania lub konflikt ze zmiennymi w jakikolwiek inny case. Innymi słowy, zmienne wydają się przechodzić przez caseinstrukcje, nawet jeśli wykonanie nie może. C # bardzo się stara, aby promować czytelność, zakazując niektórych konstrukcji innych języków, które są mylące lub łatwo nadużywane. Ale wydaje się, że to po prostu spowoduje zamieszanie. Rozważ następujące scenariusze:

  1. Gdyby to zmienić na to:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);
    

    Pojawia się komunikat „ Zastosowanie nieprzypisanej zmiennej lokalnej„ akcja ”. Jest to mylące, ponieważ w każdym innym konstrukcie w języku C #, o którym mogę pomyśleć var action = ..., zainicjuje zmienną, ale tutaj po prostu ją deklaruje.

  2. Gdybym zamienił takie przypadki:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    

    Otrzymuję komunikat „ Nie można użyć zmiennej lokalnej„ akcja ”, zanim nie zostanie zadeklarowana ”. Tak więc kolejność bloków skrzynek wydaje się tutaj ważna w sposób, który nie jest do końca oczywisty - normalnie mógłbym napisać je w dowolnej kolejności, ale chcę, varaby pojawił się w pierwszym bloku, w którym actionjest używany, muszę dostosować casebloki odpowiednio.

  3. Gdyby to zmienić na to:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;
    

    Wtedy nie dostaję błędu, ale w pewnym sensie wygląda na to, że zmiennej przypisuje się wartość, zanim zostanie zadeklarowana.
    (Chociaż nie mogę myśleć o żadnym momencie, w którym naprawdę chciałbyś to zrobić - nawet nie wiedziałem, że goto caseistniał wcześniej)

Więc moje pytanie brzmi: dlaczego projektanci C # nie dali caseblokom własnego lokalnego zasięgu? Czy są tego jakieś historyczne lub techniczne powody?

pswg
źródło
4
Zadeklaruj actionzmienną przed switchinstrukcją lub umieść każdy przypadek w osobnych nawiasach klamrowych, a uzyskasz rozsądne zachowanie.
Robert Harvey
3
@RobertHarvey tak, i mógłbym pójść jeszcze dalej i napisać to bez użycia switchw ogóle - jestem ciekawy uzasadnienia tego projektu.
pswg
jeśli c # potrzebuje przerwy po każdym przypadku, to chyba z powodów historycznych, jak w c / java, tak nie jest!
tgkprog
@tgkprog Wierzę, że to nie z powodów historycznych i że projektanci C # zrobili to celowo, aby upewnić się, że powszechny błąd polegający na zapomnieniu a breaknie jest możliwy w C #.
svick 16.04.13
12
Zobacz odpowiedź Erica Lipperta na to powiązane pytanie SO. stackoverflow.com/a/1076642/414076 Możesz odejść zadowolony lub nie. Brzmi to jako „ponieważ tak postanowili to zrobić w 1999 roku”.
Anthony Pegram

Odpowiedzi:

24

Myślę, że dobrym powodem jest to, że w każdym innym przypadku zakres „normalnej” zmiennej lokalnej jest blokiem ograniczonym przez nawiasy klamrowe ( {}). Zmienne lokalne, które nie są normalne, pojawiają się w specjalnej konstrukcji przed instrukcją (która zwykle jest blokiem), jak forzmienna pętli lub zmienna zadeklarowana w using.

Jeszcze jednym wyjątkiem są zmienne lokalne w wyrażeniach zapytań LINQ, ale są one całkowicie odmienne od normalnych deklaracji zmiennych lokalnych, więc nie sądzę, aby istniała tam możliwość pomylenia.

W celach informacyjnych reguły znajdują się w § 3.7 Zakresy specyfikacji C #:

  • Zakres zmiennej lokalnej zadeklarowanej w deklaracji zmiennej lokalnej to blok, w którym występuje deklaracja.

  • Zakres zmiennej lokalnej zadeklarowanej w przełącznik bloku z switchoświadczeniem jest przełącznik bloku .

  • Zakres zmiennej lokalnej deklarowanego we for-inicjatora z foroświadczeniem jest za-inicjator , za warunek The for-iterator , a zawarte oświadczenie w foroświadczeniu.

  • Zakres zmiennych uznane jako część foreach-rachunku , za pomocą rachunku- , blokady rachunku lub zapytań ekspresji określa się przez ekspansję danej konstrukcji.

(Chociaż nie jestem całkowicie pewien, dlaczego switchblok jest wyraźnie wymieniony, ponieważ nie ma żadnej specjalnej składni dla deklaracji zmiennych lokalnych, w przeciwieństwie do wszystkich innych wspomnianych konstrukcji).

svick
źródło
1
+1 Czy możesz podać link do cytowanego zakresu?
pswg
@pswg Możesz znaleźć link do pobrania specyfikacji na MSDN . (Kliknij link z napisem „Microsoft Developer Network (MSDN)” na tej stronie.)
svick
Sprawdziłem więc specyfikacje Java i switcheswydaje mi się, że zachowują się identycznie pod tym względem (poza wymaganiem przeskoków na końcu case). Wygląda na to, że to zachowanie zostało po prostu skopiowane stamtąd. Myślę, że krótka odpowiedź brzmi - caseinstrukcje nie tworzą bloków, po prostu definiują podziały switchbloku - i dlatego same nie mają zasięgów.
pswg
3
Re: Nie jestem całkowicie pewien, dlaczego wyraźnie wymieniono blok przełączników - autor specyfikacji jest po prostu wybredny, domyślnie wskazując, że blok przełączników ma inną gramatykę niż zwykły blok.
Eric Lippert
42

Ale tak jest. Możesz tworzyć lokalne zakresy w dowolnym miejscu, zawijając linie{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}
Mike Koder
źródło
Yup użył tego i działa zgodnie z opisem
Mvision
1
+1 To dobra sztuczka, ale przyjmuję odpowiedź Svicka za najbliższe zajęcie się moim pierwotnym pytaniem.
pswg
10

Zacytuję Erica Lipperta, którego odpowiedź jest dość jasna na ten temat:

Rozsądnym pytaniem jest „dlaczego to nie jest legalne?” Rozsądną odpowiedzią jest „cóż, dlaczego tak powinno być”? Możesz to zrobić na dwa sposoby. Albo to jest legalne:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

lub jest to legalne:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

ale nie możesz mieć tego na dwa sposoby. Projektanci C # wybrali drugi sposób, który wydaje się być bardziej naturalny.

Ta decyzja została podjęta 7 lipca 1999 roku, nieco mniej niż dziesięć lat temu. Komentarze w notatkach z tego dnia są niezwykle krótkie, po prostu stwierdzając: „Skrzynka przełączników nie tworzy własnej przestrzeni deklaracji”, a następnie podając przykładowy kod, który pokazuje, co działa, a co nie.

Aby dowiedzieć się więcej o tym, co było w głowach projektantów tego konkretnego dnia, musiałbym złorzeczyć wielu ludziom, co myśleli dziesięć lat temu - i zepsuć im to, co ostatecznie jest banalnym problemem; Nie zamierzam tego robić.

Krótko mówiąc, nie ma szczególnie ważnego powodu, aby wybrać taki czy inny sposób; oba mają zalety. Zespół projektantów języków wybrał jeden sposób, ponieważ musiał wybrać jeden; ten, który wybrali, wydaje mi się rozsądny.

Tak więc, o ile nie będziesz bliżej współpracować z zespołem programistów C # z 1999 roku niż Eric Lippert, nigdy nie poznasz dokładnego powodu!

Cyril Gandon
źródło
Nie downvoter, ale był to komentarz do wczorajszego pytania .
Jesse C. Slicer,
4
Widzę to, ale skoro komentator nie opublikował odpowiedzi, pomyślałem, że dobrym pomysłem może być utworzenie odpowiedzi bezpośrednio poprzez zacytowanie treści linku. I nie dbam o opinie! OP pyta o powód historyczny, a nie odpowiedź „Jak to zrobić”.
Cyril Gandon
5

Wyjaśnienie jest proste - dzieje się tak dlatego, że tak jest w C. Języki takie jak C ++, Java i C # skopiowały składnię i zakres instrukcji switch w celu znajomości.

(Jak stwierdzono w innej odpowiedzi, programiści C # nie mają dokumentacji na temat tego, jak podjęto decyzję dotyczącą zakresów wielkości liter. Jednak nieokreśloną zasadą składni C # było to, że jeśli nie mieli istotnych powodów, aby zrobić to inaczej, skopiowali Javę. )

W C instrukcje przypadków są podobne do goto-label. Instrukcja switch jest naprawdę ładniejszą składnią obliczonego goto . Przypadki określają punkty wejścia do bloku przełączników. Domyślnie reszta kodu zostanie wykonana, chyba że istnieje wyraźne wyjście. Dlatego sensowne jest, że używają tego samego zakresu.

(Zasadniczo. Goto nie są ustrukturyzowane - nie definiują ani nie ograniczają sekcji kodu, definiują jedynie punkty przejścia. Dlatego etykieta goto nie może wprowadzić zakresu.)

C # zachowuje składnię, ale wprowadza zabezpieczenie przed „wypadnięciem”, wymagając wyjścia po każdej (niepustej) klauzuli przypadku. Ale to zmienia nasz sposób myślenia o przełączniku! Sprawy są teraz alternatywnymi gałęziami , podobnie jak gałęzie w if-else. Oznacza to, że spodziewalibyśmy się, że każda gałąź zdefiniuje swój zakres, podobnie jak klauzule if lub klauzule iteracyjne.

Krótko mówiąc: przypadki mają ten sam zakres, ponieważ tak jest w C. Ale wydaje się dziwne i niespójne w C #, ponieważ myślimy o przypadkach jako o alternatywnych gałęziach, a nie o gotowych celach.

JacquesB
źródło
1
wydaje się dziwne i niespójne w języku C #, ponieważ uważamy przypadki za alternatywne gałęzie zamiast goto celów ”. Właśnie takie zamieszanie miałem. +1 za wyjaśnienie powodów estetycznych, dlaczego jest źle w C #.
pswg
4

Uproszczonym sposobem patrzenia na zakres jest rozważanie zakresu według bloków {}.

Ponieważ switchnie zawiera żadnych bloków, nie może mieć różnych zasięgów.

Guvante
źródło
3
Co for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Nie ma bloków, ale istnieją różne zakresy.
svick
@svick: Jak powiedziałem, uproszczony, istnieje blok utworzony przez forinstrukcję. switchnie tworzy bloków dla każdego przypadku, tylko na najwyższym poziomie. Podobieństwo polega na tym, że każda instrukcja tworzy jeden blok (liczony switchjako instrukcja).
Guvante 16.04.13
1
Dlaczego więc nie mógłbyś liczyć case?
svick 16.04.13
1
@svick: Ponieważ casenie zawiera bloku, chyba że zdecydujesz się go opcjonalnie dodać. fortrwa dla następnej instrukcji, chyba że dodasz, {}więc zawsze zawiera blok, ale casetrwa, dopóki coś nie spowoduje opuszczenia prawdziwego bloku, switchinstrukcji.
Guvante 16.04.13