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:
- Prześlij instrukcje do komputera.
- 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 switch
zawsze 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 switch
się 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ę:
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.
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:
- 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.
- Ulepszenia optymalizacji oznaczają, że prawdopodobieństwo
switch
, że zostanie ono zmienione, if-else
ponieważ 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ś.
- Doświadczenie pokazuje, że przewaga jest raczej przypadkiem mniejszości niż normą (badanie kompilatora firmy Sun wykazało, że 3%
switch
blokó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.
- 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 goto
tutaj 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 case
jako etykiety w C #. W języku C # możemy pozbyć się below_six
etykiety i używania, goto case 5
co 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:
- 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.
- 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.
- 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.
- C # umożliwia egzekwowanie istniejących najlepszych praktyk z C (przechodzenie dokumentu) przez kompilator.
- C # sprawia, że nietypowy przypadek jest tym z bardziej wyraźnym kodem, zwykły przypadek z kodem, który po prostu zapisuje automatycznie.
- C # używa tego samego
goto
podejścia do trafiania w ten sam blok z różnych case
etykiet, co jest używane w C. To po prostu uogólnia go na inne przypadki.
- C # sprawia, że
goto
oparte 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 + 240
któ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 nop
wypeł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.