Dlaczego musimy używać przełącznika przerwania?

74

Kto zdecydował (i na podstawie jakich pojęć), że switchkonstrukcja (w wielu językach) musi być użyta breakw każdym stwierdzeniu?

Dlaczego musimy napisać coś takiego:

switch(a)
{
    case 1:
        result = 'one';
        break;
    case 2:
        result = 'two';
        break;
    default:
        result = 'not determined';
        break;
}

(zauważyłem to w PHP i JS; prawdopodobnie używa wielu innych języków)

Jeśli switchjest to alternatywa if, dlaczego nie możemy użyć tej samej konstrukcji, co dla if? To znaczy:

switch(a)
{
    case 1:
    {
        result = 'one';
    }
    case 2:
    {
        result = 'two';
    }
    default:
    {
        result = 'not determined';
    }
}

Mówi się, że breakuniemożliwia wykonanie bloku następującego po bieżącym. Ale czy ktoś naprawdę wpadł na sytuację, w której istniała potrzeba wykonania bieżącego bloku i kolejnych? Ja nie. Dla mnie breakzawsze tam jest. W każdym bloku. W każdym kodzie.

trejder
źródło
1
Jak zauważyło większość odpowiedzi, jest to związane z „awaryjnością”, w której wiele warunków jest kierowanych przez te same bloki „wtedy”. Jest to jednak zależne od języka - na przykład RPG traktuje CASEinstrukcję równorzędnie z gigantycznym blokiem if / elseif.
Clockwork-Muse,
34
Była to kiepska decyzja podjęta przez projektantów C (podobnie jak wiele innych decyzji, dokonano przejścia od asemblacji -> C, a tłumaczenie z powrotem łatwiej, a nie dla ułatwienia programowania) , które niestety zostało odziedziczone w innych językach opartych na C. Używając prostej reguły „Zawsze ustaw domyślny przypadek !!” , widzimy, że domyślnym zachowaniem powinno być zerwanie, z osobnym słowem kluczowym do przejścia, ponieważ jest to zdecydowanie najczęstszy przypadek.
BlueRaja - Danny Pflughoeft
4
Kilkakrotnie korzystałem z metody awaryjnej, choć zapewne instrukcja switch w moim preferowanym języku w tamtym czasie (C) mogła zostać zaprojektowana w taki sposób, aby tego uniknąć; np. zrobiłem, case 'a': case 'A': case 'b': case 'B'ale głównie dlatego, że nie mogę case in [ 'a', 'A', 'b', 'B' ]. Nieco lepszym pytaniem jest, w moim obecnym preferowanym języku (C #), przerwa jest obowiązkowa i nie ma żadnego ukrytego upadku; zapominanie breakjest błędem składni ...: \
KutuluMike
1
Błąd implementacji rzeczywistego kodu często znajduje się w implementacjach parsera. case TOKEN_A: /*set flag*/; case TOKEN_B: /*consume token*/; break; case TOKEN_C: /*...*/
OrangeDog,
1
@ BlueRaja-DannyPflughoeft: C został również zaprojektowany tak, aby był łatwy do skompilowania. „Emituj skok, jeśli breakjest obecny w dowolnym miejscu” jest znacznie prostszą zasadą do wdrożenia niż „Nie wysyłaj skoku, jeśli fallthroughjest obecny w switch”.
Jon Purdy,

Odpowiedzi:

95

C był jednym z pierwszych języków, w którym switchoświadczenie było w tej formie, i wszystkie inne główne języki odziedziczyły je stamtąd, przeważnie decydując się zachować domyślną semantykę C - albo nie zastanawiały się nad zaletami jego zmiany, albo osądzały mniej ważne niż zachowanie, do którego wszyscy byli przyzwyczajeni.

Co do tego, dlaczego C został zaprojektowany w ten sposób, prawdopodobnie wynika on z koncepcji C jako „przenośnego zestawu”. switchStwierdzenie jest w zasadzie poboru tabeli oddziału i stół oddział ma również niejawna jesieni-through i wymaga dodatkowej instrukcji skoku, aby tego uniknąć.

Zasadniczo projektanci C również postanowili zachować semantykę asemblera jako domyślną.

Michael Borgwardt
źródło
3
VB.NET jest przykładem języka, który go zmienił. Nie ma niebezpieczeństwa, że ​​wpłynie do klauzuli sprawy.
poke
1
Tcl to kolejny język, który nigdy nie przechodzi do następnej klauzuli. Praktycznie nigdy też nie chciałem tego robić (w przeciwieństwie do C, gdzie jest to przydatne podczas pisania niektórych rodzajów programów).
Donal Fellows
4
Języki przodków C, B i starsza BCPL , mają (miały?) Instrukcje przełączania; BCPL przeliterował to switchon.
Keith Thompson
Deklaracje sprawy Ruby nie przewracają się; podobne zachowanie można uzyskać, mając dwie wartości testowe w jednym when.
Nathan Long
86

Ponieważ switchnie jest alternatywą dla if ... elseoświadczeń w tych językach.

Używając switch, możemy dopasować więcej niż jeden warunek na raz, co jest bardzo cenione w niektórych przypadkach.

Przykład:

public Season SeasonFromMonth(Month month)
{
    Season season;
    switch (month)
    {
        case Month.December:
        case Month.January:
        case Month.February:
            season = Season.Winter;
            break;

        case Month.March:
        case Month.April:
        case Month.May:
            season = Season.Spring;
            break;

        case Month.June:
        case Month.July:
        case Month.August:
            season = Season.Summer;
            break;

        default:
            season = Season.Autumn;
            break;
    }

    return season;
}
Satish Pandey
źródło
15
more than one block ... unwanted in most casesNie zgadzam się z tobą.
Satish Pandey,
2
@DanielB Zależy to jednak od języka; Instrukcje przełączników C # nie pozwalają na taką możliwość.
Andy,
5
@Andy Czy mówimy o upadku? Instrukcje przełączników C # zdecydowanie pozwalają na to (sprawdź trzeci przykład), stąd potrzeba break. Ale tak, o ile mi wiadomo, urządzenie Duffa nie działa w języku C #.
Daniel B
8
@DanielB Tak, jesteśmy, ale kompilator nie zezwoli na warunek, kod, a następnie na inny warunek bez przerwy. Możesz mieć wiele warunków ułożonych w stosy bez kodu pomiędzy, lub potrzebujesz przerwy. Z twojego linku C# does not support an implicit fall through from one case label to another. Możesz wpaść, ale nie ma szans przypadkowo zapomnieć o przerwie.
Andy
3
@Matsemann .. Teraz możemy robić wszystkie rzeczy, if..elseale w niektórych sytuacjach switch..casebyłoby lepiej. Powyższy przykład byłby nieco skomplikowany, gdybyśmy użyli if-else.
Satish Pandey,
15

To pytanie zostało zadane w przypadku przepełnienia stosu w kontekście C: Dlaczego instrukcja switch została zaprojektowana tak, aby wymagała przerwy?

Podsumowując przyjętą odpowiedź, prawdopodobnie był to błąd. Większość innych języków prawdopodobnie właśnie zastosowało się do C. Jednak niektóre języki, takie jak C #, wydają się naprawiać to, pozwalając na awarię - ale tylko wtedy, gdy programista wyraźnie to mówi (źródło: link powyżej, nie mówię w języku C #) .

Tapio
źródło
Google Go robi to samo IIRC (pozwalając na przewrót, jeśli jest to wyraźnie określone).
Leo
PHP też. Im więcej spojrzysz na wynikowy kod, tym trudniej go poczuć. Podoba mi się /* FALLTHRU */komentarz w odpowiedzi pod linkiem @Tapio powyżej, aby udowodnić , że miałeś na myśli.
DaveP
1
Powiedzenie C # goto case, podobnie jak link, nie ilustruje, dlaczego to działa tak dobrze. Nadal możesz układać w stos wiele przypadków, jak wyżej, ale jak tylko wstawisz kod, musisz mieć wyraźny wyraz, goto case whateveraby przejść do następnej sprawy lub otrzymasz błąd kompilatora. Jeśli o mnie zapytacie, umiejętność układania spraw bez martwienia się o nieumyślne przejście przez zaniedbanie jest zdecydowanie lepszą cechą niż umożliwienie jawnego rozwiązania goto case.
Jesper
9

Odpowiem przykładem. Jeśli chcesz podać liczbę dni dla każdego miesiąca w roku, oczywiste jest, że niektóre miesiące mają 31, około 30 i 1 28/29. Tak by wyglądało

switch(month) {
    case 4:
    case 6:
    case 9:
    case 11;
        days = 30;
        break;
    case 2:
        //Find out if is leap year( divisible by 4 and all that other stuff)
        days = 28 or 29;
        break;
    default:
        days = 31;
}

Jest to przykład, w którym wiele przypadków ma ten sam efekt i wszystkie są zgrupowane razem. Istniał oczywiście powód wyboru słowa kluczowego break, a nie argumentu if ... else if construct.

Najważniejszą rzeczą do odnotowania tutaj jest to, że instrukcja switch z wieloma podobnymi przypadkami nie jest jeśli… jeśli… jeśli… jeśli… jeśli… w każdym przypadku, ale jeśli (1, 2, 3) ... inaczej jeśli (4,5,6) jeszcze ...

Awemo
źródło
2
Twój przykład wygląda bardziej wyczerpująco niż ifbez żadnych korzyści. Być może, jeśli twój przykład nadałby nazwę lub pracował nad konkretnym miesiącem oprócz upadku, użyteczność upadku byłaby bardziej widoczna? (bez komentowania, czy POWINIEN być na pierwszym miejscu)
horatio
1
To prawda. Próbowałem jednak tylko pokazać przypadki, w których można wykorzystać przewrócenie się. Byłoby oczywiście znacznie lepiej, gdyby każdy miesiąc potrzebował czegoś zrobionego indywidualnie.
Awemo,
8

Istnieją dwie sytuacje, w których może wystąpić „wpadnięcie” z jednej skrzynki do drugiej - pusta skrzynka:

switch ( ... )
{
  case 1:
  case 2:
    do_something_for_1_and_2();
    break;
  ...
}

i niepusty pojemnik

switch ( ... )
{
  case 1:
    do_something_for_1();
    /* Deliberately fall through */
  case 2:
    do_something_for_1_and_2();
    break;
...
}

Niezależnie od odniesienia do Urządzenia Duffa , uzasadnione przypadki dla drugiego przypadku są nieliczne i daleko od nich, i generalnie są zabronione przez standardy kodowania i oznaczone podczas analizy statycznej. A tam, gdzie to jest znalezione, jest to najczęściej spowodowane pominięciem litery break.

Ten pierwszy jest całkowicie rozsądny i powszechny.

Szczerze mówiąc, nie widzę powodu, dla którego potrzebuję breakparsera i języka, wiedząc, że puste ciało-sprawa to upadek, podczas gdy niepuste pudełko jest samodzielne.

Szkoda, że ​​panel ISO C wydaje się bardziej zajęty dodawaniem do języka nowych (niechcianych) i źle zdefiniowanych funkcji, niż naprawianiem nieokreślonych, nieokreślonych lub zdefiniowanych przez implementację funkcji, nie wspominając o nielogiczności.

Andrzej
źródło
2
Dzięki Bogu, ISO C nie wprowadza przełomowych zmian w języku!
Jack Aidley
4

Brak wymuszania breakdopuszcza wielu rzeczy, które w innym przypadku mogłyby być trudne. Inni zauważyli przypadki grupowania, dla których istnieje szereg spraw niebanalnych.

Jednym z przypadków, w których konieczne breakjest nieużywanie, jest Urządzenie Duffa . Służy do „ rozwijania ” pętli, w których może przyspieszyć operacje, ograniczając liczbę wymaganych porównań. Wierzę, że początkowe użycie pozwoliło na funkcjonalność, która wcześniej była zbyt wolna z całkowicie zwiniętą pętlą. W niektórych przypadkach zamienia rozmiar kodu na szybkość.

Dobrą praktyką jest zastąpienie breakodpowiedniego komentarzem, jeśli sprawa ma jakiś kod. W przeciwnym razie ktoś naprawi brakujące narzędzie breaki wprowadzi błąd.

BillThor
źródło
Dzięki za przykład urządzenia Duffa (+1). Nie byłam tego świadoma.
trejder,
3

W C, gdzie wydaje się, że początek, blok kodu switchinstrukcji nie jest specjalną konstrukcją. Jest to normalny blok kodu, podobnie jak blok pod ifinstrukcją.

switch ()
{
}

if ()
{
}

casei defaultprzeskakują etykiety wewnątrz tego bloku, szczególnie związane z switch. Są obsługiwane tak jak normalne etykiety skoku goto. Ważna jest tutaj jedna konkretna zasada: etykiety skoków mogą znajdować się prawie wszędzie w kodzie, bez zakłócania przepływu kodu.

Jako zwykły blok kodu nie musi to być instrukcja złożona. Etykiety są również opcjonalne. Są to ważne switchoświadczenia w katalogu C:

switch (a)
    case 1: Foo();

switch (a)
    Foo();

switch (a)
{
    Foo();
}

Sam standard C podaje to jako przykład (6.8.4.2):

switch (expr) 
{ 
    int i = 4; 
    f(i); 
  case 0: 
    i=17; 
    /*falls through into default code */ 
  default: 
    printf("%d\n", i); 
} 

W sztucznym fragmencie programu obiekt o identyfikatorze i istnieje z automatycznym czasem przechowywania (w bloku), ale nigdy nie jest inicjowany, a zatem jeśli wyrażenie kontrolne ma wartość niezerową, wywołanie funkcji printf uzyska dostęp do nieokreślonej wartości. Podobnie nie można nawiązać połączenia z funkcją f.

Co więcej, defaultjest również etykietą skoku, a zatem może być w dowolnym miejscu, bez potrzeby bycia ostatnim przypadkiem.

To wyjaśnia również urządzenie Duffa:

switch (count % 8) {
    case 0: do {  *to = *from++;
    case 7:       *to = *from++;
    case 6:       *to = *from++;
    case 5:       *to = *from++;
    case 4:       *to = *from++;
    case 3:       *to = *from++;
    case 2:       *to = *from++;
    case 1:       *to = *from++;
            } while(--n > 0);
}

Skąd ta awaria? Ponieważ w normalnym przepływie kodu w normalnym bloku kodu oczekuje się przejścia do następnej instrukcji , tak jak można tego oczekiwać w ifbloku kodu.

if (a == b)
{
    Foo();
    /* "Fall-through" to Bar expected here. */
    Bar();
}

switch (a)
{
    case 1: 
        Foo();
        /* Automatic break would violate expected code execution semantics. */
    case 2:
        Bar();
}

Domyślam się, że powodem tego była łatwość wdrożenia. Nie potrzebujesz specjalnego kodu do parsowania i kompilowania switchbloku, dbając o specjalne reguły. Po prostu parsujesz go jak każdy inny kod i musisz tylko dbać o etykiety i wybór skoku.

Ciekawym pytaniem uzupełniającym z tego wszystkiego jest to, czy następujące zagnieżdżone instrukcje wypisują „Gotowe”. albo nie.

int a = 10;

switch (a)
{
    switch (a)
    {
        case 10: printf("Done.\n");
    }
}

Norma C dba o to (6.8.4.2.4):

Etykieta lub domyślna etykieta jest dostępna tylko w najbliższej załączającej instrukcji switch.

Bezpieczne
źródło
1

Kilka osób wspomniało już o dopasowaniu wielu warunków, co jest bardzo cenne od czasu do czasu. Jednak zdolność dopasowania wielu warunków niekoniecznie wymaga, abyś robił dokładnie to samo z każdym pasującym warunkiem. Rozważ następujące:

switch (someCase)
{
    case 1:
    case 2:
        doSomething1And2();
        break;

    case 3:
        doSomething3();

    case 4:
        doSomething3And4();
        break;

    default:
        throw new Error("Invalid Case");
}

Istnieją dwa różne sposoby dopasowania zestawów wielu warunków. W przypadku warunków 1 i 2 po prostu przechodzą do dokładnie tego samego wykresu kodu i robią dokładnie to samo. Jednak z warunkami 3 i 4, mimo że oba kończą się połączeniem doSomething3And4(), tylko 3 połączenia doSomething3().

Panzercrisis
źródło
Późno na imprezę, ale absolutnie nie jest to „1 i 2”, jak sugeruje nazwa funkcji: istnieją trzy różne stany, które mogą prowadzić do uruchomienia kodu case 2, więc jeśli chcemy być poprawni (i robimy) prawdziwe nazwa funkcji musiałaby być doSomethingBecause_1_OR_2_OR_1AND2()lub coś takiego (chociaż poprawny kod, w którym niezmienny pasuje do dwóch różnych przypadków, a mimo to dobrze napisany kod, praktycznie nie istnieje, więc przynajmniej tak powinno być doSomethingBecause1or2())
Mike 'Pomax' Kamermans
Innymi słowy doSomething[ThatShouldBeDoneInTheCaseOf]1And[InTheCaseOf]2(). Nie odnosi się to do logiki i / lub.
Panzercrisis,
Wiem, że tak nie jest, ale biorąc pod uwagę, dlaczego ludzie mylą się z tego rodzaju kodem, powinien bezwzględnie wyjaśnić, co to jest „i”, ponieważ polegając na kimś, kto zgadnie „naturalny i” kontra „logiczny koniec” w odpowiedzi, która tylko działa gdy dokładnie wyjaśniony, potrzebuje więcej wyjaśnień w samej odpowiedzi lub przepisania nazwy funkcji, aby usunąć tę dwuznaczność =)
Mike 'Pomax' Kamermans
1

Aby odpowiedzieć na dwa twoje pytania.

Dlaczego C potrzebuje przerw?

Sprowadza się do korzeni Cs jako „przenośnego asemblera”. Gdzie taki kod psudo był powszechny: -

    targets=(addr1,addr2,addr3);
    opt = 1  ## or 0 or 2
switch:
    br targets[opt]  ## go to addr2 
addr1:
    do 1stuff
    br switchend
addr2:
    do 2stuff
    br switchend
addr3
    do 3stuff
switchend:
    ......

instrukcja switch została zaprojektowana w celu zapewnienia podobnej funkcjonalności na wyższym poziomie.

Czy kiedykolwiek mamy przełączniki bez przerw?

Tak, jest to dość powszechne i istnieje kilka przypadków użycia;

Po pierwsze, możesz chcieć podjąć to samo działanie w kilku przypadkach. Robimy to, układając skrzynki jedna na drugiej:

case 6:
case 9:
    // six or nine code

Innym przypadkiem użycia powszechnym w automatach stanów jest to, że po przetworzeniu jednego stanu natychmiast chcemy wprowadzić i przetworzyć inny stan:

case 9:
    crashed()
    newstate=10;
case 10:
    claim_damage();
    break;
James Anderson
źródło
-2

Instrukcja break jest instrukcją skokową, która pozwala użytkownikowi wyjść z najbliższego załączającego przełącznika (w twoim przypadku), podczas gdy, do, for lub foreach. to takie proste.

jaspis
źródło
-3

Myślę, że przy wszystkich powyższych tekstach - głównym rezultatem tego wątku jest to, że jeśli chcesz zaprojektować nowy język - domyślnie powinno być tak, że nie musisz dodawać breakinstrukcji, a kompilator potraktuje to tak, jakbyś to zrobił.

Jeśli chcesz ten rzadki przypadek, w którym chcesz przejść do następnej sprawy - po prostu podaj continueoświadczenie.

Można to poprawić, tak że tylko jeśli użyjesz nawiasów klamrowych wewnątrz obudowy, nie będzie to kontynuowane, więc przykład z powyższymi miesiącami kilku przypadków wykonujących ten sam dokładny kod - zawsze działałby zgodnie z oczekiwaniami bez potrzeby continue.

etanowie
źródło
2
Nie jestem pewien, czy rozumiem twoją sugestię dotyczącą oświadczenia kontynuacji. Kontynuuj ma zupełnie inne znaczenie w C i C ++, a jeśli nowy język byłby podobny do C, ale czasami używał słowa kluczowego jak w C, a czasem w ten alternatywny sposób, myślę, że zamieszanie by zapanowało. Chociaż mogą wystąpić pewne problemy z przejściem z jednego bloku oznaczonego do drugiego, podoba mi się stosowanie wielu przypadków z taką samą obsługą ze wspólnym blokiem kodu.
DeveloperDon
C ma długą tradycję używania tych samych słów kluczowych do wielu celów. Pomyśl *, &, staticitp
idrougge