Dlaczego instrukcja Switch została zaprojektowana tak, aby wymagała przerwy?

139

Biorąc pod uwagę prostą instrukcję przełącznika

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

Brak instrukcji break w przypadku 2 oznacza, że ​​wykonanie będzie kontynuowane wewnątrz kodu dla przypadku 3. To nie jest przypadek; został zaprojektowany w ten sposób. Dlaczego podjęto takie decyzje? Jakie to daje korzyści w porównaniu z automatyczną semantyczną przerwą dla bloków? Jaki był powód?

EvilTeach
źródło

Odpowiedzi:

150

Wiele odpowiedzi wydaje się skupiać się na zdolności do upadku jako na przyczynie żądania breakstwierdzenia.

Uważam, że był to po prostu błąd, głównie dlatego, że gdy projektowano C, nie było tak dużego doświadczenia w zakresie wykorzystania tych konstrukcji.

Peter Van der Linden przedstawia taki przypadek w swojej książce „Expert C Programming”:

Przeanalizowaliśmy źródła kompilatora Sun C, aby zobaczyć, jak często używany był domyślny błąd. Interfejs kompilatora Sun ANSI C ma 244 instrukcje przełączające, z których każda ma średnio siedem przypadków. Upadek występuje tylko w 3% wszystkich tych przypadków.

Innymi słowy, normalne zachowanie przełącznika jest nieprawidłowe w 97% przypadków. Nie dotyczy to tylko kompilatora - wręcz przeciwnie, tam, gdzie fall through używano w tej analizie, często dotyczyło to sytuacji, które występują częściej w kompilatorze niż w innym oprogramowaniu, na przykład podczas kompilowania operatorów, które mogą mieć jeden lub dwa operandy :

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

Upadek przypadku jest tak powszechnie uznawany za wadę, że istnieje nawet specjalna konwencja komentarzy, pokazana powyżej, która mówi lintowi, że „jest to naprawdę jeden z tych 3% przypadków, w których upadek był pożądany”.

Myślę, że dobrym pomysłem dla C # było wymaganie jawnej instrukcji skoku na końcu każdego bloku przypadku (przy jednoczesnym umożliwieniu układania wielu etykiet przypadków - o ile istnieje tylko jeden blok instrukcji). W języku C # nadal możesz mieć jeden przypadek przechodzić do drugiego - wystarczy, że przejdziesz bezpośrednio przez przejście do następnego przypadku przy użyciu pliku goto.

Szkoda, że ​​Java nie skorzystała z okazji, aby zerwać z semantyką C.

Michael Burr
źródło
Rzeczywiście, myślę, że postawili na prostotę implementacji. Niektóre inne języki obsługiwały bardziej wyrafinowane przypadki (zakresy, wiele wartości, łańcuchy ...), być może kosztem wydajności.
PhiLho
Java prawdopodobnie nie chciała zerwać z nawykami i szerzyć zamieszania. W przypadku innego zachowania musieliby użyć innej semantyki. Projektanci Java i tak stracili wiele okazji do oderwania się od C.
PhiLho
@PhiLho - myślę, że prawdopodobnie najbliżej prawdy jest „prostota wykonania”.
Michael Burr
Jeśli używasz jawnych skoków za pośrednictwem GOTO, czy nie jest tak samo produktywne, aby po prostu użyć serii instrukcji IF?
DevinB
3
nawet Pascal wdraża swój przełącznik bez przerwy. jak mógł C-kompilator-koder o tym nie myśleć @@
Chan Le
30

Na wiele sposobów c jest po prostu przejrzystym interfejsem do standardowych idiomów asemblera. Pisząc sterowanie przepływem sterowane tablicą skoków, programista ma do wyboru wypadnięcie lub wyskoczenie ze „struktury sterowania”, a wyskoczenie wymaga wyraźnej instrukcji.

Więc c robi to samo ...

dmckee --- kociak ex-moderator
źródło
1
Wiem, że wiele osób tak mówi, ale myślę, że nie jest to pełny obraz; C jest również często brzydkim interfejsem do tworzenia antywzorów. 1. Brzydki: oświadczenia zamiast wyrażeń. „Deklaracja odzwierciedla użycie”. Uwielbione kopiowania i wklejania jak z mechanizmu modułowości i przenośności. System typu sposób zbyt skomplikowane; zachęca do błędów. NULL. (Ciąg dalszy w następnym komentarzu.)
Harrison
2. Anty-wzorce: nie zapewnia "czystych" oznaczonych związków, jednego z dwóch podstawowych idiomów asemblacji reprezentacji danych. (Knuth, tom 1, rozdział 2) Zamiast „nieoznakowanych związków zawodowych”, nie idiomatyczny hack. Ten wybór od dziesięcioleci utrudnia myślenie o danych. I NULzakończone struny to chyba najgorszy pomysł na świecie.
Harrison
@HarrisonKlaperman: Żadna metoda przechowywania ciągów znaków nie jest idealna. Zdecydowana większość problemów związanych z ciągami znaków zakończonych wartością null nie wystąpiłaby, gdyby procedury, które akceptowały łańcuchy, również akceptowały parametr maksymalnej długości, i wystąpiłyby z procedurami, które przechowują ciągi znaków o określonej długości w buforach o stałej wielkości bez akceptowania parametru rozmiaru bufora . Projekt instrukcji przełącznika, w którym przypadki są po prostu etykietami, może wydawać się dziwny dla współczesnych oczu, ale nie jest gorszy niż projekt DOpętli Fortran .
supercat
Jeśli zdecyduję się napisać tablicę skoków w asemblerze, wezmę wartość wielkości liter i magicznie zamienię ją na indeks dolny tablicy skoków, przeskoczę do tej lokalizacji i wykonam kod. Po tym nie przejdę do następnej sprawy. Przeskoczę do jednolitego adresu, który jest wyjściem ze wszystkich spraw. Pomysł, żebym wskoczył lub wpadł w sedno następnej sprawy, jest głupi. Nie ma żadnego przypadku użycia dla tego wzorca.
EvilTeach
2
Chociaż w dzisiejszych czasach ludzie są bardziej przyzwyczajeni do czyszczenia i samoobrony ido i funkcji językowych, które zapobiegają strzelaniu w stopę, jest to reminescencja z obszaru, w którym bajty były drogie (C zaczęło się przed 1970 rokiem). jeśli twój kod musi mieścić się w 1024 bajtach, będziesz mieć duże trudności z ponownym użyciem fragmentów kodu. Ponowne użycie kodu poprzez rozpoczęcie w różnych punktach wejścia o tym samym końcu jest jednym z mechanizmów umożliwiających osiągnięcie tego.
rpy
23

Aby wdrożyć urządzenie Duffa, oczywiście:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    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);
    }
}
FlySwat
źródło
3
Uwielbiam urządzenie Duffa. Tak elegancka i szybka.
dicroce
7
tak, ale czy musi się pojawiać za każdym razem, gdy pojawia się instrukcja przełącznika na SO?
billjamesdev
Przegapiłeś dwa zamykające nawiasy klamrowe ;-).
Toon Krijthe
18

Gdyby sprawy miały załamać się niejawnie, to nie można by tego dokonać.

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

Gdyby przełącznik został zaprojektowany tak, aby wyłamać się niejawnie po każdym przypadku, nie miałbyś wyboru. Konstrukcja obudowy przełącznika została zaprojektowana w taki sposób, aby była bardziej elastyczna.

Bill the Lizard
źródło
12
Możesz wyobrazić sobie przełącznik, który niejawnie się psuje, ale ma słowo kluczowe „fallthrough”. Niezręczne, ale wykonalne.
dmckee --- ex-moderator kitten
Jak byłoby lepiej? Wyobrażałbym sobie, że instrukcja case działałaby w ten sposób częściej niż „blok kodu na przypadek” ... to jest blok if / then / else.
billjamesdev
Dodałbym, że w pytaniu załączniki zakresu {} w każdym bloku przypadków zwiększają zamieszanie, ponieważ wygląda jak instrukcja „while”.
Matthew Smith
1
@ Bill: Wydaje mi się, że byłoby gorzej, ale rozwiązałoby to skargę podniesioną przez Mike'a B: ten upadek (poza wieloma przypadkami to samo) jest rzadkim zdarzeniem i nie powinien być zachowaniem domyślnym.
dmckee --- ex-moderator kitten
1
Pominąłem nawiasy klamrowe dla zwięzłości, z wieloma stwierdzeniami, że powinny tam być. Zgadzam się, że fallthrough należy używać tylko wtedy, gdy przypadki są dokładnie takie same. Jest to mylące jak diabli, gdy przypadki opierają się na poprzednich przypadkach przy użyciu metody Fallthrough.
Bill the Lizard
15

Instrukcje case w instrukcjach switch są po prostu etykietami.

Po przełączeniu na wartości, instrukcja switch zasadniczo robi goto do etykiety o wartości dopasowania.

Oznacza to, że przerwa jest konieczna, aby uniknąć przejścia do kodu pod następną etykietą.

Jeśli chodzi o powód, dla którego została zaimplementowana w ten sposób - upadkowy charakter instrukcji przełącznika może być przydatny w niektórych scenariuszach. Na przykład:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;
LeopardSkinPillBoxHat
źródło
2
Istnieją dwa duże problemy: 1) Zapominanie o tym, breakgdzie jest to potrzebne. 2) Jeśli kolejność instrukcji przypadków zostanie zmieniona, przejście może spowodować uruchomienie niewłaściwej sprawy. Dlatego uważam, że obsługa języka C # jest znacznie lepsza (jawna goto casedla fallthrough, z wyjątkiem pustych etykiet wielkości liter).
Daniel Rose
@DanielRose: 1) są sposoby, aby zapomnieć również breakw C # - najprostszy jest to, gdy nie chcesz casenic robić, ale zapomnisz dodać break(być może tak bardzo pochłonął cię komentarz wyjaśniający, albo ktoś cię wezwał inne zadanie): wykonanie przejdzie do caseponiższego. 2) zachęcające goto casejest zachęcanie do nieustrukturyzowanego kodowania „spaghetti” - możesz skończyć z przypadkowymi cyklami ( case A: ... goto case B; case B: ... ; goto case A;), zwłaszcza gdy przypadki są oddzielone w pliku i trudne do zrozumienia w kombinacji. W C ++ lokalizowane jest przejście przez.
Tony Delroy
8

Aby zezwolić na takie rzeczy, jak:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

Pomyśl o casesłowie kluczowym jak o gotoetykiecie i jest to o wiele bardziej naturalne.

R .. GitHub PRZESTAŃ POMÓC LODOWI
źródło
8
To if(0)pod koniec pierwszego przypadku jest złe i powinno być używane tylko wtedy, gdy celem jest zaciemnienie kodu.
Daniel Rose
11
Myślę, że cała ta odpowiedź była ćwiczeniem bycia złym. :-)
R .. GitHub STOP HELPING ICE
3

Eliminuje powielanie kodu, gdy kilka przypadków musi wykonać ten sam kod (lub ten sam kod w kolejności).

Ponieważ na poziomie języka asemblerowego nie ma znaczenia, czy przerwiesz każdy z nich, czy też nie, i tak nie ma żadnego narzutu za upadek przypadków, więc dlaczego nie pozwolić im, skoro oferują one znaczące korzyści w niektórych przypadkach.

Greg Rogers
źródło
2

Zdarzyło mi się natknąć się na przypadek przypisywania wartości w wektorach do struktur: trzeba było to zrobić w taki sposób, że jeśli wektor danych byłby krótszy niż liczba członków danych w strukturze, reszta członków pozostałaby w ich wartość domyślna. W takim przypadku pomijanie breakbyło całkiem przydatne.

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}
Martti K.
źródło
0

Jak wiele osób tutaj wskazało, ma to pozwolić na działanie pojedynczego bloku kodu w wielu przypadkach. To powinno być częstsze występowanie do swoich sprawozdań przełączników niż „bloku kodu na wszelki wypadek” można określić w swoim przykładzie.

Jeśli masz blok kodu na przypadek bez upadku, być może powinieneś rozważyć użycie bloku if-elseif-else, ponieważ wydaje się to bardziej odpowiednie.

billjamesdev
źródło