Zmiana instrukcji przełączania w C #?

365

Omówienie instrukcji Switch jest jednym z moich głównych głównych powodów, by kochać switchkontra if/else ifkonstrukty. Tutaj jest przykład:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Mądrzy ludzie kulą się, ponieważ string[]s należy zadeklarować poza funkcją: no cóż, są, to tylko przykład.

Kompilator kończy się niepowodzeniem z następującym błędem:

Kontrola nie może przechodzić z jednej etykiety przypadku („przypadek 3:”) do drugiej
Kontrola nie może przechodzić z jednej etykiety przypadku („przypadek 2:”) na inną

Dlaczego? I czy jest jakiś sposób na uzyskanie takiego zachowania bez posiadania trzech ifs?

Matthew Scharley
źródło

Odpowiedzi:

648

(Skopiuj / wklej odpowiedź, którą podałem w innym miejscu )

Spadaniu switch- caseS może być uzyskiwane przez nie kodu w case(patrz case 0), albo przy użyciu specjalnego goto case(patrz case 1) lub goto default(patrz case 2) formy:

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}
Alex Lyman
źródło
121
Myślę, że w tym konkretnym przypadku goto nie jest uważane za szkodliwe.
Thomas Owens,
78
Cholera - programuję w C # od pierwszych dni 1.0 i nigdy tego nie widziałem. Po prostu pokazuje, że uczysz się nowych rzeczy każdego dnia.
Erik Forbes,
37
Wszystko w porządku, Erik. Jedynym powodem / wiedziałem o tym jest to, że jestem kujonem teorii kompilatorów, który czytam specyfikacje ECMA-334 za pomocą lupy.
Alex Lyman
13
@Dancrumb: W momencie pisania tej funkcji C # nie dodał jeszcze żadnych „miękkich” słów kluczowych (takich jak „fed”, „var”, „from” i „select”), więc mieli trzy rzeczywiste opcje: 1 ) uczynić z „twardego” słowa kluczowego (nie można go używać jako nazwy zmiennej), 2) napisać kod niezbędny do obsługi takiego miękkiego słowa kluczowego, 3) użyć już zarezerwowanych słów kluczowych. # 1 był dużym problemem dla tych programów; # 2 było dość dużym zadaniem inżynieryjnym, z tego co rozumiem; a opcja, z którą poszli, # 3 miała dodatkową zaletę: inni deweloperzy czytający kod po tym, jak mogli dowiedzieć się o tej funkcji z podstawowej koncepcji goto
Alex Lyman
10
Wszystko to mówi o nowych / specjalnych słowach kluczowych dla wyraźnego przewrotu. Czy nie mogli po prostu użyć słowa kluczowego „kontynuuj”? Innymi słowy. Wyjdź z przełącznika lub przejdź do następnej skrzynki (wpadnij).
Tal Even-Tov,
44

„Dlaczego” ma unikać przypadkowego przewrócenia się, za co jestem wdzięczny. To nierzadkie źródło błędów w C i Javie.

Obejściem tego problemu jest użycie goto, np

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

Ogólny projekt przełącznika / obudowy jest moim zdaniem trochę niefortunny. Utknął zbyt blisko C - istnieje kilka przydatnych zmian, które można wprowadzić w zakresie określania zakresu itp. Prawdopodobnie pomocny byłby mądrzejszy przełącznik, który mógłby dopasowywać wzorce itp., Ale tak naprawdę zmienia się z przełącznika na „sprawdzanie sekwencji warunków” - w którym momencie może być potrzebne inne imię.

Jon Skeet
źródło
1
To jest różnica między przełącznikiem a if / elseif w mojej głowie. Przełącznik służy do sprawdzania różnych stanów pojedynczej zmiennej, natomiast jeśli / elseif może być użyty do sprawdzenia dowolnej liczby rzeczy, które się połączyły, ale niekoniecznie pojedynczej lub tej samej zmiennej.
Matthew Scharley
2
Jeśli miałoby to zapobiec przypadkowemu wypadnięciu, uważam, że ostrzeżenie kompilatora byłoby lepsze. Podobnie jak masz, jeśli twoje oświadczenie if ma przypisane zadanie:if (result = true) { }
Tal Even-Tov
3
@ TalEven-Tov: Ostrzeżenia kompilatora powinny naprawdę dotyczyć przypadków, w których prawie zawsze można poprawić kod, aby był lepszy. Osobiście wolałbym niejawne zerwanie, więc na początku nie byłoby problemu, ale to inna sprawa.
Jon Skeet,
26

Przełączanie się jest historycznie jednym z głównych źródeł błędów we współczesnym oprogramowaniu. Projektant języka postanowił wprowadzić obowiązek przeskakiwania na końcu sprawy, chyba że bezpośrednio przechodzi się do następnej sprawy bez przetwarzania.

switch(value)
{
    case 1:// this is still legal
    case 2:
}
Moneta
źródło
23
Nigdy nie rozumiem, dlaczego to nie jest „przypadek 1, 2:”
BCS,
11
@David Pfeffer: Tak, i tak case 1, 2:w językach, które na to pozwalają. Nigdy nie zrozumiem, dlaczego żaden współczesny język nie zdecydowałby się na to.
BCS,
@BCS z instrukcją goto, wiele opcji oddzielonych przecinkami może być trudnych w obsłudze?
pingwin
@pengut: dokładniej można powiedzieć, że case 1, 2:jest to jedna etykieta, ale z wieloma nazwami. - FWIW, myślę, że większość języków, które zabraniają upadku, nie zajmuje się specjalnym przypadkiem indu „kolejne etykiety spraw”, ale raczej traktuje etykiety spraw jako adnotację do następnej instrukcji i wymaga ostatniej instrukcji przed instrukcją oznaczoną (jednym lub więcej) etykiety skrzynek na skok.
BCS,
22

Aby dodać do odpowiedzi tutaj, myślę, że warto rozważyć przeciwne pytanie w związku z tym, a mianowicie. dlaczego C pozwolił przede wszystkim na upadek?

Każdy język programowania służy oczywiście dwóm celom:

  1. Prześlij instrukcje do komputera.
  2. Pozostaw zapis intencji programisty.

Stworzenie dowolnego języka programowania stanowi zatem równowagę między tym, jak najlepiej służyć tym dwóm celom. Z jednej strony, im łatwiej jest zamienić się w instrukcje komputerowe (bez względu na to, czy są to kod maszynowy, kod bajtowy jak IL, czy instrukcje są interpretowane podczas wykonywania), tym bardziej proces kompilacji lub interpretacji będzie bardziej wydajny, niezawodny i kompaktowa moc wyjściowa. W skrajności ten cel powoduje, że po prostu piszemy w asemblerze, IL, a nawet surowe kody operacyjne, ponieważ najłatwiejsza kompilacja jest tam, gdzie w ogóle nie ma kompilacji.

I odwrotnie, im bardziej język wyraża zamiar programisty, a nie środki podjęte w tym celu, tym bardziej zrozumiały program zarówno podczas pisania, jak i podczas konserwacji.

Teraz switchzawsze można go było skompilować, przekształcając go w równoważny łańcuchif-else bloków lub podobny, ale został on zaprojektowany jako umożliwiający kompilację w konkretny wspólny wzorzec składania, w którym bierze się wartość, oblicza przesunięcie od niej (czy patrząc na tabelę indeksowane przez idealny skrót wartości lub przez rzeczywistą arytmetykę wartości *). Warto w tym miejscu zauważyć, że dzisiaj kompilacja w języku C # czasami zamienia switchsię w równoważny if-else, a czasem stosuje podejście oparte na haszowaniu (podobnie jak w przypadku C, C ++ i innych języków o porównywalnej składni).

W takim przypadku istnieją dwa dobre powody, aby pozwolić na awarię:

  1. W każdym razie dzieje się to naturalnie: jeśli zbudujesz tabelę skoków w zestawie instrukcji, a jedna z wcześniejszych partii instrukcji nie zawiera jakiegoś rodzaju skoku lub powrotu, wówczas wykonanie po prostu naturalnie przejdzie do następnej partii. Dopuszczenie upadku było tym, co „po prostu się zdarzy”, jeśli obróciszswitch -poprawiający C w skokowy stół-przy użyciu kodu maszynowego.

  2. Koderzy, którzy pisali w asemblerze, byli już przyzwyczajeni do ekwiwalentu: podczas pisania tabeli skoków ręcznie w asemblerze musieliby rozważyć, czy dany blok kodu zakończy się zwrotem, skokiem poza tabelę, czy po prostu kontynuować do następnego bloku. W związku z tym koder musi dodać wyraźnebreak razie potrzeby było , było również „naturalne” dla kodera.

W tym czasie była to rozsądna próba zrównoważenia dwóch celów języka komputerowego, ponieważ odnosi się to zarówno do wytworzonego kodu maszynowego, jak i do ekspresyjności kodu źródłowego.

Cztery dekady później sprawy nie są takie same z kilku powodów:

  1. Kodery w C mogą dzisiaj mieć niewielkie doświadczenie lub nie mieć go wcale. Kodery w wielu innych językach w stylu C są jeszcze mniej prawdopodobne (szczególnie JavaScript!). Żadna koncepcja „do czego ludzie są przyzwyczajeni od montażu” nie ma już znaczenia.
  2. Ulepszenia optymalizacji oznaczają, że prawdopodobieństwo switch, że zostanie ono zmienione, if-elseponieważ uznano, że podejście może być najbardziej wydajne, lub też zostanie przekształcone w szczególnie ezoteryczny wariant podejścia z tabelą skoków, jest wyższe. Mapowanie między podejściami wyższego i niższego poziomu nie jest tak silne, jak kiedyś.
  3. Doświadczenie pokazuje, że przewaga jest raczej przypadkiem mniejszości niż normą (badanie kompilatora firmy Sun wykazało, że 3% switchbloków używało przewrotu innego niż wiele etykiet na tym samym bloku i sądzono, że użycie przypadek tutaj oznaczał, że te 3% było w rzeczywistości znacznie wyższe niż normalnie). Tak więc badany język sprawia, że ​​niezwykłe jest łatwiejsze do zaspokojenia niż zwykłe.
  4. Doświadczenie pokazuje, że awaria może być źródłem problemów zarówno w przypadkach, w których jest to przypadkowo wykonane, jak i w przypadkach, w których ktoś nie utrzymuje poprawnego błędu. To ostatnie jest subtelnym dodatkiem do błędów związanych z awarią, ponieważ nawet jeśli kod jest całkowicie wolny od błędów, awaria może nadal powodować problemy.

W odniesieniu do tych dwóch ostatnich punktów rozważ następujący cytat z bieżącej edycji K&R:

Przechodzenie z jednego przypadku do drugiego nie jest niezawodne, ponieważ jest podatne na rozpad, gdy program jest modyfikowany. Z wyjątkiem wielu etykiet dla jednego obliczenia, przejścia należy używać oszczędnie i komentować.

Ze względu na dobrą formę, należy położyć przerwę po ostatniej sprawie (tutaj domyślnej), nawet jeśli jest to logicznie niepotrzebne. Pewnego dnia, gdy na końcu zostanie dodana kolejna sprawa, ten fragment programowania obronnego cię uratuje.

Tak więc z pyska konia problematyczne jest przejście przez C. Dobrą praktyką jest zawsze dokumentowanie błędów za pomocą komentarzy, co jest zastosowaniem ogólnej zasady, że należy udokumentować, gdzie robi się coś niezwykłego, ponieważ to potknie się później podczas badania kodu i / lub sprawi, że twój kod będzie wyglądał tak zawiera błąd nowicjusza, gdy jest on poprawny.

A kiedy o tym pomyślisz, kod taki jak ten:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

To dodanie czegoś, co wyraźnie pokazuje błąd w kodzie, po prostu nie jest to coś, co może zostać wykryte (lub którego brak może zostać wykryty) przez kompilator.

Jako taki, fakt, że on musi być jawny z przewrotnością w C #, nie dodaje żadnej kary dla ludzi, którzy dobrze pisali w innych językach w stylu C, ponieważ byliby już wyraźni w swoich błędach. †

Wreszcie, użycie gototutaj jest już normą z C i innych takich języków:

switch(x)
{
  case 0:
  case 1:
  case 2:
    foo();
    goto below_six;
  case 3:
    bar();
    goto below_six;
  case 4:
    baz();
    /* FALLTHRU */
  case 5:
  below_six:
    qux();
    break;
  default:
    quux();
}

W przypadku, gdy chcemy, aby blok został uwzględniony w kodzie wykonanym dla wartości innej niż tylko ta, która przenosi go do poprzedniego bloku, to już musimy go użyć goto. (Oczywiście istnieją sposoby i sposoby na uniknięcie tego przy różnych warunkach warunkowych, ale dotyczy to prawie wszystkiego, co dotyczy tego pytania). Jako taki C # zbudował na już normalnym sposobie radzenia sobie z jedną sytuacją, w której chcemy trafić więcej niż jeden blok kodu w switch, i po prostu uogólniłem go, aby objąć również awarię. Uczyniło to również oba przypadki wygodniejszymi i samo dokumentującymi się, ponieważ musimy dodać nową etykietę w C, ale możemy użyć jej casejako etykiety w C #. W języku C # możemy pozbyć się below_sixetykiety i używania, goto case 5co jest wyraźniejsze co do tego, co robimy. (Musielibyśmy również dodaćbreak dodefault, które pominąłem, aby powyższy kod C wyraźnie nie był kodem C #).

Podsumowując zatem:

  1. C # nie odnosi się już do niezoptymalizowanych danych wyjściowych kompilatora, tak jak kod C 40 lat temu (podobnie jak C obecnie), co sprawia, że ​​jedna z inspiracji polegających na przewróceniu się nie ma znaczenia.
  2. C # pozostaje kompatybilny z C, nie tylko domyślnie break, dla łatwiejszej nauki języka przez osoby znające podobne języki i łatwiejszego przenoszenia.
  3. C # usuwa potencjalne źródło błędów lub źle zrozumianego kodu, który został dobrze udokumentowany jako powodujący problemy przez ostatnie cztery dekady.
  4. C # umożliwia egzekwowanie istniejących najlepszych praktyk z C (przechodzenie dokumentu) przez kompilator.
  5. C # sprawia, że ​​nietypowy przypadek jest tym z bardziej wyraźnym kodem, zwykły przypadek z kodem, który po prostu zapisuje automatycznie.
  6. C # używa tego samego gotopodejścia do trafiania w ten sam blok z różnych caseetykiet, co jest używane w C. To po prostu uogólnia go na inne przypadki.
  7. C # sprawia, że gotooparte na nim podejście jest wygodniejsze i bardziej przejrzyste niż w C, pozwalając case, aby instrukcje działały jak etykiety.

Podsumowując, całkiem rozsądna decyzja projektowa


* Niektóre formy języka BASIC pozwoliłyby na takie działania, GOTO (x AND 7) * 50 + 240które choć kruche, a przez to szczególnie przekonujące w przypadku banowania goto, służą do pokazania wyższego języka odpowiednika tego, w jaki sposób kod niższego poziomu może wykonać skok w oparciu o arytmetyka na wartości, która jest znacznie bardziej rozsądna, gdy jest wynikiem kompilacji, a nie czymś, co należy utrzymywać ręcznie. W szczególności implementacje urządzenia Duffa dobrze nadają się do równoważnego kodu maszynowego lub IL, ponieważ każdy blok instrukcji często ma tę samą długość bez potrzeby dodawania nopwypełniaczy.

† Urządzenie Duffa pojawia się tutaj ponownie, jako rozsądny wyjątek. Fakt, że przy tych i podobnych wzorcach występuje powtarzanie operacji, sprawia, że ​​użycie metody fall-through jest stosunkowo jasne, nawet bez wyraźnego komentarza na ten temat.

Jon Hanna
źródło
17

Możesz „goto case label” http://www.blackwasp.co.uk/CSharpGoto.aspx

Instrukcja goto to prosta komenda, która bezwarunkowo przenosi kontrolę nad programem do innej instrukcji. Polecenie to jest często krytykowane, ponieważ niektórzy programiści zalecają jego usunięcie ze wszystkich języków programowania wysokiego poziomu, ponieważ może to prowadzić do kodu spaghetti . Dzieje się tak, gdy istnieje tak wiele instrukcji goto lub podobnych instrukcji skoku, że kod staje się trudny do odczytania i utrzymania. Są jednak programiści, którzy zwracają uwagę, że przy gotowaniu instrukcji goto można w elegancki sposób rozwiązać niektóre problemy ...

Kenny
źródło
8

Pominęli to zachowanie z założenia, aby uniknąć sytuacji, w których nie był używany przez wolę, ale powodował problemy.

Można go użyć tylko wtedy, gdy w części przypadku nie ma instrukcji, na przykład:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}
Biri
źródło
4

Zmienili zachowanie instrukcji switch (z C / Java / C ++) dla c #. Wydaje mi się, że powodem było to, że ludzie zapomnieli o upadku i powstały błędy. Jedna z książek, które przeczytałem, mówi, że używam goto do symulacji, ale nie wydaje mi się to dobrym rozwiązaniem.

Rozpoznać
źródło
2
C # obsługuje goto, ale nie przełom? Łał. I to nie tylko te. C # jest jedynym znanym mi językiem, który zachowuje się w ten sposób.
Matthew Scharley,
Na początku mi się nie podobało, ale „fall-thru” to tak naprawdę przepis na katastrofę (szczególnie wśród młodszych programistów). Jak wielu zauważyło, C # nadal pozwala na fall-thru dla pustych linii (co stanowi większość przypadków.) „Kenny” opublikował link, który podkreśla eleganckie użycie Goto w przypadku przełączników.
Pretzel
Moim zdaniem nie jest to wielka sprawa. W 99% przypadków nie chcę upaść i w przeszłości byłem spalony przez błędy.
Ken
1
„nie wydaje mi się to dobrym rozwiązaniem” - przykro mi to słyszeć o tobie, bo po to goto casejest. Jego przewaga nad przewrotem polega na tym, że jest wyraźny. To, że niektórzy ludzie sprzeciwiają się goto casetylko pokazaniu, że zostali indoktrynowani przeciwko „goto” bez jakiegokolwiek zrozumienia problemu i nie są w stanie samodzielnie myśleć. Kiedy Dijkstra napisał „GOTO uważane za szkodliwe”, zwracał się do języków, które nie miały żadnych innych możliwości zmiany przepływu kontroli.
Jim Balter
@JimBalter, a następnie ilu ludzi, którzy zacytują Dijkstrę, zacytują Knutha, że ​​„przedwczesna optymalizacja jest źródłem wszelkiego zła”, choć ten cytat miał miejsce, gdy Knuth wyraźnie pisał o tym, jak przydatne gotomoże być podczas optymalizacji kodu?
Jon Hanna,
1

Możesz osiągnąć upadek jak c ++ przez słowo kluczowe goto.

DAWNY:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}
Dai Tran
źródło
9
Gdyby tylko ktoś opublikował to dwa lata wcześniej!
0

Instrukcja skoku, taka jak przerwa, jest wymagana po każdym bloku obserwacji, w tym ostatnim bloku, niezależnie od tego, czy jest to instrukcja przypadku, czy instrukcja domyślna. Z jednym wyjątkiem (w przeciwieństwie do instrukcji przełączania C ++), C # nie obsługuje niejawnego przechodzenia z jednej etykiety na drugą. Jedynym wyjątkiem jest sytuacja, gdy instrukcja case nie ma kodu.

- Dokumentacja C # switch ()

Władca
źródło
1
Zdaję sobie sprawę, że to zachowanie jest udokumentowane, chcę wiedzieć DLACZEGO jest tak, jak jest i jakie są alternatywy, aby uzyskać stare zachowanie.
Matthew Scharley
0

Po każdym przypadku instrukcja wymaga instrukcji break lub goto , nawet jeśli jest to przypadek domyślny.

Shilpa gulati
źródło
2
Gdyby tylko ktoś opublikował to dwa lata wcześniej!
1
@Poldie było zabawne za pierwszym razem ... Shilpa, nie potrzebujesz przerwy ani goto dla każdej sprawy, tylko dla każdej sprawy z własnym kodem. Możesz mieć wiele spraw, które współużytkują kod w porządku.
Maverick
0

Wystarczy krótka uwaga, aby dodać, że kompilator dla Xamarin rzeczywiście źle to zrobił i pozwala na przewrót. Podobno został naprawiony, ale nie został wydany. Odkryłem to w jakimś kodzie, który faktycznie padał, a kompilator nie narzekał.


źródło
0

przełącznik (C # Reference) mówi

C # wymaga końca sekcji przełączników, w tym ostatniej,

Musisz także dodać a break;do swojej defaultsekcji, w przeciwnym razie nadal będzie błąd kompilatora.

gm2008
źródło
Dzięki, pomogło mi to;)
CareTaker22 15.03.16
-1

C # nie obsługuje przechodzenia przez instrukcje switch / case. Nie jestem pewien, dlaczego, ale tak naprawdę nie ma na to wsparcia. Połączenie

BFree
źródło
To takie złe. Zobacz inne odpowiedzi ... efekt niejawnego upadku można uzyskać za pomocą jawnego goto case . Był to celowy, mądry wybór projektantów C #.
Jim Balter
-11

Zapomniałeś dodać „break”; instrukcja do przypadku 3. W przypadku 2 zapisałeś go w bloku if. Dlatego spróbuj tego:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}
Marcus
źródło
2
Daje to bardzo zły wynik. Zostawiłem instrukcje przełączników poza projektem. Pytanie brzmi, dlaczego kompilator C # postrzega to jako błąd, skoro prawie żaden inny język nie ma tego ograniczenia.
Matthew Scharley
5
Co za oszałamiający brak zrozumienia. Masz 5 lat na usunięcie tego i nadal tego nie zrobiłeś? Mindboggling.
Jim Balter