Per aggiungere alle risposte qui, penso che valga la pena considerare la domanda opposta in combinato disposto con questo, vale a dire. perché C ha permesso il fall-through in primo luogo?
Qualsiasi linguaggio di programmazione ovviamente ha due obiettivi:
- Fornire istruzioni al computer.
- Lascia un registro delle intenzioni del programmatore.
La creazione di qualsiasi linguaggio di programmazione è quindi un equilibrio tra come servire al meglio questi due obiettivi. Da un lato, più è facile trasformarsi in istruzioni per computer (che si tratti di codice macchina, bytecode come IL o istruzioni interpretate in fase di esecuzione), quindi più tale processo di compilazione o interpretazione sarà efficiente, affidabile e compatto in uscita. Portato all'estremo, questo obiettivo si traduce nella nostra semplice scrittura in assembly, IL, o anche in codici operativi non elaborati, perché la compilazione più semplice è dove non esiste alcuna compilazione.
Al contrario, più la lingua esprime l'intenzione del programmatore, piuttosto che i mezzi presi a tal fine, più comprensibile è il programma sia durante la scrittura che durante la manutenzione.
Ora, switch
avrebbe sempre potuto essere compilato convertendolo in una catena equivalente di if-else
blocchi o simile, ma è stato progettato per consentire la compilazione in un particolare modello di assieme comune in cui si prende un valore, calcola un offset da esso (sia guardando una tabella indicizzato da un hash perfetto del valore o dall'aritmetica effettiva sul valore *). Vale la pena notare a questo punto che oggi la compilazione C # a volte si trasforma switch
in equivalenteif-else
e talvolta utilizza un approccio di salto basato sull'hash (e allo stesso modo con C, C ++ e altri linguaggi con sintassi comparabile).
In questo caso ci sono due buoni motivi per consentire il fall-through:
Succede comunque in modo naturale: se si crea una tabella di salto in un set di istruzioni e uno dei lotti precedenti di istruzioni non contiene una sorta di salto o ritorno, l'esecuzione passerà naturalmente al batch successivo. Consentire il fall-through era ciò che sarebbe "appena accaduto" se si trasformasse la switch
C in un jump-table, usando il codice macchina.
I programmatori che scrivevano in assembly erano già usati per l'equivalente: quando scrivevano una tabella di salto a mano in assembly, dovevano considerare se un determinato blocco di codice sarebbe terminato con un ritorno, un salto fuori dalla tabella o semplicemente continuare al blocco successivo. Come tale, avere il programmatore aggiungere un esplicito break
quando necessario era "naturale" anche per il programmatore.
All'epoca, quindi, era un ragionevole tentativo di bilanciare i due obiettivi di un linguaggio informatico in relazione sia al codice macchina prodotto, sia all'espressività del codice sorgente.
Quattro decenni dopo, però, le cose non sono più le stesse, per alcuni motivi:
- I programmatori in C oggi possono avere poca o nessuna esperienza di assemblaggio. I programmatori in molti altri linguaggi in stile C hanno anche meno probabilità di (specialmente Javascript!). Qualsiasi concetto di "cosa le persone sono abituate dall'assemblaggio" non è più pertinente.
- I miglioramenti nelle ottimizzazioni significano che la probabilità di
switch
essere trasformato inif-else
perché ritenuta l'approccio probabilmente più efficiente, oppure trasformata in una variante particolarmente esoterica dell'approccio con tabella di salto, è maggiore. La mappatura tra gli approcci di livello superiore e inferiore non è così forte come una volta.
- L'esperienza ha dimostrato che il fall-through tende a essere il caso di minoranza piuttosto che la norma (uno studio del compilatore di Sun ha rilevato che il 3% dei
switch
blocchi utilizzava un fall-through diverso da più etichette nello stesso blocco e si pensava che l'uso- il caso qui significava che questo 3% era in realtà molto più alto del normale). Quindi la lingua studiata rende l'insolito più facilmente accessibile rispetto al comune.
- L'esperienza ha dimostrato che il fall-through tende ad essere la fonte di problemi sia nei casi in cui viene effettuato accidentalmente, sia nei casi in cui un fall-through corretto viene perso da qualcuno che mantiene il codice. Quest'ultima è una sottile aggiunta ai bug associati al fall-through, perché anche se il tuo codice è perfettamente privo di bug, il fall-through può comunque causare problemi.
In relazione a questi ultimi due punti, considera la seguente citazione dall'edizione corrente di K&R:
Passare da un caso all'altro non è robusto, essendo soggetto a disintegrazione quando il programma viene modificato. Con l'eccezione di più etichette per un singolo calcolo, i fall-through dovrebbero essere usati con parsimonia e commentati.
In buona forma, fai una pausa dopo l'ultimo caso (il default qui) anche se logicamente non è necessario. Un giorno, quando un altro caso verrà aggiunto alla fine, questo pezzetto di programmazione difensiva ti salverà.
Quindi, dalla bocca del cavallo, la caduta in C è problematica. È buona norma documentare sempre errori con i commenti, che è un'applicazione del principio generale secondo cui si dovrebbe documentare dove si fa qualcosa di insolito, perché è quello che farà scattare l'esame successivo del codice e / o rendere il codice simile ha un bug per principianti quando è effettivamente corretto.
E a pensarci bene, codice come questo:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Sta aggiungendo qualcosa per rendere esplicito il fall-through nel codice, semplicemente non è qualcosa che può essere rilevato (o la cui assenza può essere rilevata) dal compilatore.
Come tale, il fatto che on debba essere esplicito con fall-through in C # non aggiunge alcuna penalità alle persone che hanno scritto bene in altri linguaggi in stile C, poiché sarebbero già esplicite nei loro fall-through. †
Infine, l'uso di goto
qui è già una norma da C e altri linguaggi simili:
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();
}
In questo tipo di casi in cui vogliamo che un blocco sia incluso nel codice eseguito per un valore diverso da quello che porta uno al blocco precedente, allora dobbiamo già usarlo goto
. (Naturalmente, ci sono mezzi e modi per evitarlo con diversi condizionali, ma questo è vero per tutto ciò che riguarda questa domanda). Come tale, C # si è basato sul modo già normale di affrontare una situazione in cui vogliamo colpire più di un blocco di codice in a switch
, e lo ha appena generalizzato per coprire anche il fall-through. Ha anche reso entrambi i casi più convenienti e autocompattanti, poiché dobbiamo aggiungere una nuova etichetta in C ma possiamo usare case
come etichetta in C #. In C # possiamo sbarazzarci below_six
dell'etichetta e usarlagoto case 5
che è più chiaro su ciò che stiamo facendo. (Dovremmo anche aggiungerebreak
per il default
, che ho lasciato solo per rendere chiaramente il codice C sopra non il codice C #).
In sintesi quindi:
- C # non si riferisce più all'output del compilatore non ottimizzato direttamente come ha fatto il codice C 40 anni fa (né C in questi giorni), il che rende irrilevante una delle ispirazioni del fall-through.
- C # rimane compatibile con C non solo avendo implicito
break
, per un più facile apprendimento della lingua da parte di chi ha familiarità con lingue simili e un porting più semplice.
- C # rimuove una possibile fonte di bug o codice incompreso che è stato ben documentato come causa di problemi negli ultimi quattro decenni.
- C # rende le best practice esistenti con C (fall fall document) applicabile dal compilatore.
- C # rende il caso insolito quello con un codice più esplicito, il solito caso quello con il codice che scrive automaticamente.
- C # usa lo stesso
goto
approccio per colpire lo stesso blocco da case
etichette diverse come viene usato in C. Lo generalizza solo in altri casi.
- C # rende l'
goto
approccio basato su tale approccio più conveniente e più chiaro di quanto non lo sia in C, consentendo case
alle dichiarazioni di fungere da etichette.
Tutto sommato, una decisione di progettazione abbastanza ragionevole
* Alcune forme di BASIC consentirebbero di fare cose del genere GOTO (x AND 7) * 50 + 240
mentre fragili e quindi un caso particolarmente persuasivo per il bando goto
, servono a mostrare un equivalente di lingua superiore del modo in cui il codice di livello inferiore può fare un salto basato su aritmetica su un valore, che è molto più ragionevole quando è il risultato della compilazione piuttosto che qualcosa che deve essere mantenuto manualmente. Le implementazioni del dispositivo Duff in particolare si prestano bene al codice macchina equivalente o IL perché ogni blocco di istruzioni avrà spesso la stessa lunghezza senza bisogno di aggiungere nop
riempitivi.
† Il dispositivo di Duff viene di nuovo qui, come un'eccezione ragionevole. Il fatto che con questo e simili schemi vi sia una ripetizione di operazioni serve a rendere l'uso del fall-through relativamente chiaro anche senza un commento esplicito in tal senso.