Perché l'istruzione switch è stata progettata per richiedere un'interruzione?


139

Dato un semplice switch switch

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

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

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

L'assenza di una dichiarazione di rottura nel caso 2, implica che l'esecuzione continuerà all'interno del codice per il caso 3. Questo non è un incidente; è stato progettato in questo modo. Perché sono state prese queste decisioni? Quale vantaggio offre questo rispetto alla presenza di una semantica di interruzione automatica per i blocchi? Qual era la logica?

Risposte:


150

Molte risposte sembrano concentrarsi sulla capacità di fallire come motivo per richiedere l' breakaffermazione.

Credo che sia stato semplicemente un errore, in gran parte dovuto al fatto che quando C è stato progettato non c'era molta esperienza su come questi costrutti sarebbero stati usati.

Peter Van der Linden illustra il caso nel suo libro "Expert C Programming":

Abbiamo analizzato le fonti del compilatore Sun C per vedere con quale frequenza veniva utilizzato il fall fall default. Il front-end del compilatore Sun ANSI C ha 244 istruzioni switch, ognuna delle quali ha una media di sette casi. La caduta avviene solo nel 3% di tutti questi casi.

In altre parole, il normale comportamento dell'interruttore è errato nel 97% dei casi. Non è solo in un compilatore - al contrario, dove fall through è stato utilizzato in questa analisi è stato spesso per situazioni che si verificano più frequentemente in un compilatore rispetto ad altri software, ad esempio, quando si compilano operatori che possono avere uno o due operandi :

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

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

Il case fall through è così ampiamente riconosciuto come un difetto che esiste persino una convenzione di commento speciale, mostrata sopra, che dice alla lanugine "questo è davvero uno di quei 3% dei casi in cui si desiderava il fall through".

Penso che sia stata una buona idea richiedere a C # un'istruzione esplicita per il salto alla fine di ogni blocco maiuscolo (pur consentendo l'accatastamento di più etichette maiuscole, purché vi sia un solo blocco di istruzioni). In C # puoi comunque far cadere un caso in un altro - devi solo rendere esplicito il fall thru saltando al caso successivo usando a goto.

Peccato che Java non abbia colto l'occasione per uscire dalla semantica C.


In effetti, penso che siano andati per semplicità di implementazione. Alcune altre lingue supportano casi più sofisticati (intervalli, valori multipli, stringhe ...) a costo, forse, di efficienza.
PhiLho,

Java probabilmente non voleva rompere le abitudini e diffondere confusione. Per un comportamento diverso, dovrebbero usare una semantica diversa. I designer Java hanno comunque perso una serie di opportunità per uscire da C.
PhiLho,

@PhiLho - Penso che tu sia probabilmente il più vicino alla verità con "semplicità di implementazione".
Michael Burr,

Se stai utilizzando salti espliciti tramite GOTO, non è altrettanto produttivo utilizzare solo una serie di istruzioni IF?
DevinB,

3
persino Pascal implementa il loro interruttore senza interruzioni. come potrebbe il programmatore del compilatore C non pensarci @@
Chan Le,

30

In molti modi c è solo un'interfaccia pulita per gli idiomi di assemblaggio standard. Quando si scrive il controllo del flusso guidato dalla tabella di salto, il programmatore ha la possibilità di saltare o saltare fuori dalla "struttura di controllo" e un salto richiede un'istruzione esplicita.

Quindi, c fa la stessa cosa ...


1
So che molte persone lo dicono, ma penso che non sia il quadro completo; C è spesso anche una brutta interfaccia per assemblare gli antipattern. 1. Brutto: dichiarazioni invece di espressioni. "La dichiarazione riflette l'uso." Glorificato copia-incolla, come il meccanismo per la modularità e portabilità. Digita il sistema in modo troppo complicato; incoraggia i bug. NULL. (Continua nel prossimo commento.)
Harrison,

2. Antipattern: non fornisce unioni "pulite" etichettate, uno dei due modi di dire fondamentali di rappresentazione dei dati di assembly. (Knuth, vol 1, ch 2) Invece, "sindacati senza tag", un hack non idiomatico. Questa scelta ha ostacolato il modo in cui le persone pensano ai dati per decenni. E le NULstringhe terminate sono la peggior idea di sempre.
Harrison,

@HarrisonKlaperman: nessun metodo di memorizzazione delle stringhe è perfetto. La stragrande maggioranza dei problemi associati alle stringhe con terminazione null non si verificherebbe se le routine che accettavano le stringhe accettassero anche un parametro di lunghezza massima e si verificherebbero con le routine che memorizzano stringhe con marcatura di lunghezza in buffer di dimensioni fisse senza accettare un parametro di dimensioni buffer . Il design dell'istruzione switch in cui i casi sono semplicemente etichette può sembrare strano agli occhi moderni, ma non è peggio del design del DOloop Fortran .
supercat

Se decido di scrivere una tabella di salto in assembly, prenderò il valore del caso e lo trasformerò magicamente nel pedice della tabella di salto, salterò in quella posizione ed eseguirò il codice. Dopodiché non passerò al caso successivo. Salto a un indirizzo uniforme che è l'uscita di tutti i casi. L'idea che vorrei saltare o cadere nel corpo del prossimo caso è sciocca. Non esiste un caso d'uso per quel modello.
EvilTeach

2
Mentre al giorno d'oggi le persone sono più abituate a pulire e auto-proteggere idos e funzionalità linguistiche che impediscono a se stessi di sparare al piede, questa è una reminescenza da un'area in cui i byte erano costosi (la C iniziò prima del 1970). se il codice deve rientrare in 1024 byte, si verificherà un forte ricorso al riutilizzo di frammenti di codice. Riutilizzare il codice iniziando da punti di ingresso diversi che condividono la stessa estremità è un meccanismo per raggiungere questo obiettivo.
rpy,

23

Per implementare il dispositivo Duff, ovviamente:

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);
    }
}

3
Adoro il dispositivo di Duff. Così elegante e malvagio veloce.
dicroce,

7
sì, ma deve comparire ogni volta che c'è un'istruzione switch su SO?
billjamesdev,

Hai perso due parentesi graffe chiuse ;-).
Toon Krijthe

18

Se i casi sono stati progettati per rompersi in modo implicito, non si potrebbe avere fallimenti.

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

Se l'interruttore fosse progettato per rompersi implicitamente dopo ogni caso, non avresti scelta. La struttura della custodia è stata progettata per essere più flessibile.


12
Puoi immaginare un interruttore che si interrompe implicitamente, ma ha una parola chiave "fallthrough". Imbarazzante, ma praticabile.
dmckee --- ex gattino moderatore

Come sarebbe meglio? Immagino che un'istruzione case funzioni in questo modo più spesso di "blocco di codice per caso" ... questo è un blocco if / then / else.
billjamesdev,

Vorrei aggiungere che nella domanda, i contenitori di ambito {} in ogni blocco di casi si aggiungono alla confusione poiché sembra lo stile di un'istruzione "while".
Matthew Smith,

1
@Bill: sarebbe peggio, penso, ma affronterebbe il reclamo sollevato da Mike B: che fall-sebbene (a parte più casi lo stesso) è un evento raro e non dovrebbe essere il comportamento predefinito.
dmckee --- ex gattino moderatore

1
Ho lasciato fuori le parentesi graffe per brevità, con più dichiarazioni che dovrebbero essere lì. Sono d'accordo che il fallthrough dovrebbe essere usato solo quando i casi sono esattamente gli stessi. È fonte di confusione quando i casi si basano su casi precedenti usando fallthrough.
Bill the Lizard,

15

Le istruzioni case in istruzioni switch sono semplicemente etichette.

Quando si attiva un valore, l'istruzione switch fa essenzialmente un goto sull'etichetta con il valore corrispondente.

Ciò significa che l'interruzione è necessaria per evitare di passare al codice sotto l'etichetta successiva.

Per quanto riguarda il motivo per cui è stato implementato in questo modo - la natura fall-through di un'istruzione switch può essere utile in alcuni scenari. Per esempio:

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;

2
Ci sono due grandi problemi: 1) Dimenticare breakdove è necessario. 2) Se le istruzioni del caso cambiano il loro ordine, il fallthrough può comportare l'esecuzione del caso sbagliato. Quindi, trovo che la gestione di C # sia molto migliore (esplicita goto caseper fallthrough, ad eccezione delle etichette vuote del caso).
Daniel Rose,

@DanielRose: 1) ci sono anche modi per dimenticare breakin C # - uno più semplice è quando non vuoi casefare nulla ma dimentica di aggiungere un break(forse sei stato così coinvolto nel commento esplicativo, o sei stato richiamato su alcuni altro compito): l'esecuzione cadrà nel caseseguito. 2) incoraggiare goto caseè incoraggiare la codifica "spaghetti" non strutturata - si può finire con cicli accidentali ( case A: ... goto case B; case B: ... ; goto case A;), specialmente quando i casi sono separati nel file e difficili da grok in combinazione. In C ++, il fall-through è localizzato.
Tony Delroy,

8

Per consentire cose come:

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 */
}

Pensa alla caseparola chiave come gotoun'etichetta ed è molto più naturale.


8
Quello if(0)alla fine del primo caso è malvagio e dovrebbe essere usato solo se l'obiettivo è offuscare il codice.
Daniel Rose,

11
Tutta questa risposta è stata un esercizio per essere malvagi, credo. :-)
R .. GitHub FERMA AIUTANDO ICE il

3

Elimina la duplicazione del codice quando diversi casi devono eseguire lo stesso codice (o lo stesso codice in sequenza).

Dal momento che a livello di linguaggio assembly non importa se si interrompe tra di loro o meno non vi è alcun sovraccarico zero per fallire comunque casi, quindi perché non consentirli poiché offrono vantaggi significativi in ​​alcuni casi.


2

Mi è capitato di imbattermi in un caso di assegnazione di valori in vettori a strutture: doveva essere fatto in modo tale che se il vettore di dati fosse più corto del numero di membri di dati nella struttura, il resto dei membri rimarrebbe in il loro valore predefinito. In quel caso omettere breakera abbastanza utile.

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);
}

0

Come molti hanno specificato qui, è per consentire a un singolo blocco di codice di funzionare per più casi. Questo dovrebbe essere un evento più comune per le istruzioni switch rispetto al "blocco di codice per caso" specificato nell'esempio.

Se hai un blocco di codice per caso senza fall-through, forse dovresti considerare l'uso di un blocco if-elseif-else, poiché sembrerebbe più appropriato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.