Istruzione switch fallthrough in C #?


365

Istruzione switch falltrough è uno dei miei principali motivi personali per amare switchvs. if/else ifcostrutti. Un esempio è qui:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Le persone intelligenti si stanno facendo da parte perché le string[]s dovrebbero essere dichiarate al di fuori della funzione: beh, lo sono, questo è solo un esempio.

Il compilatore ha esito negativo con il seguente errore:

Il controllo non può passare da un'etichetta del caso ("caso 3:") a un altro
Il controllo non può passare da un'etichetta di caso ('caso 2:') a un altro

Perché? E c'è un modo per ottenere questo tipo di comportamento senza tre ifs?

Risposte:


648

(Copia / incolla di una risposta che ho fornito altrove )

Il superamento di switch- cases può essere ottenuto non avendo codice in un case(vedi case 0), o usando i moduli speciali goto case(vedi case 1) o goto default(vedi case 2):

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}

121
Penso che, in questo caso particolare, goto non sia considerato dannoso.
Thomas Owens,

78
Accidenti - Ho programmato con C # sin dai primi giorni della 1.0, e non l'ho mai visto fino ad ora. Va solo a mostrare, impari cose nuove ogni giorno.
Erik Forbes,

37
Va tutto bene, Erik. L'unica ragione / I / sapevo a riguardo è che sono un nerd della teoria dei compilatori che legge le specifiche ECMA-334 con una lente d'ingrandimento.
Alex Lyman,

13
@Dancrumb: Al momento in cui la funzione era stata scritta, C # non aveva ancora aggiunto parole chiave "soft" (come 'yield', 'var', 'from' e 'select'), quindi avevano tre opzioni reali: 1 ) rendere 'fallthrough' una parola chiave difficile (non è possibile utilizzarlo come nome di una variabile), 2) scrivere il codice necessario per supportare tale parola chiave, 3) utilizzare parole chiave già riservate. Il numero 1 era un grosso problema per quei codici di porting; # 2 è stato un compito di ingegneria abbastanza grande, da quello che ho capito; e l'opzione con cui sono andati, # 3 ha avuto un vantaggio secondario: altri sviluppatori che leggevano il codice dopo che il fatto poteva imparare la funzionalità dal concetto base di goto
Alex Lyman,

10
Tutto questo parla di parole chiave nuove / speciali per un esplicito fallimento. Non potevano semplicemente usare la parola chiave "continua"? In altre parole. Uscire dall'interruttore o passare al caso successivo (fall through).
Tal Even-Tov,

44

Il "perché" è evitare cadute accidentali, per le quali sono grato. Questa è una fonte non insolita di bug in C e Java.

La soluzione alternativa consiste nell'utilizzare goto, ad es

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

Il design generale di switch / case è un po 'sfortunato dal mio punto di vista. Si è bloccato troppo vicino a C - ci sono alcune modifiche utili che potrebbero essere fatte in termini di scoping ecc. Probabilmente sarebbe utile un interruttore più intelligente che potrebbe fare corrispondenze di pattern ecc., Ma questo sta davvero cambiando da interruttore a "controllare una sequenza di condizioni" - a quel punto sarebbe forse richiesto un nome diverso.


1
Questa è la differenza tra switch e if / elseif nella mia mente. Switch serve per controllare vari stati di una singola variabile, mentre if / elseif può essere usato per controllare qualsiasi numero di cose connesse, ma non necessariamente una singola, o la stessa variabile.
Matthew Scharley,

2
Se fosse per impedire la caduta accidentale, penso che un avvertimento del compilatore sarebbe stato migliore. Proprio come se ne avesse uno se la tua dichiarazione if ha un incarico:if (result = true) { }
Tal Even-Tov,

3
@ TalEven-Tov: gli avvisi del compilatore dovrebbero davvero essere per i casi in cui è quasi sempre possibile correggere il codice per essere migliore. Personalmente preferirei una rottura implicita, quindi non sarebbe un problema per cominciare, ma è una questione diversa.
Jon Skeet,

26

Passare al fallthrough è storicamente una delle principali fonti di bug nei software moderni. Il progettista della lingua ha deciso di rendere obbligatorio saltare alla fine del caso, a meno che non si stia passando automaticamente al caso successivo senza elaborarlo.

switch(value)
{
    case 1:// this is still legal
    case 2:
}

23
Non capisco mai perché non sia "case 1, 2:"
BCS

11
@David Pfeffer: Sì, lo è, e lo è anche case 1, 2:nelle lingue che lo consentono. Quello che non capirò mai è il motivo per cui qualsiasi linguaggio moderno non sceglierebbe di permetterlo.
BCS,

@BCS con l'istruzione goto, più opzioni separate da virgola potrebbero essere difficili da gestire?
penguat,

@pengut: potrebbe essere più preciso dire che case 1, 2:è una singola etichetta ma con più nomi. - FWIW, penso che la maggior parte delle lingue che vietano di passare non indichino casi particolari con l'indio "etichette di casi consecutivi", ma piuttosto trattano le etichette di casi come un'annotazione sull'istruzione successiva e richiedono l'ultima istruzione prima di un'istruzione etichettata con (una o di più) le etichette delle custodie per essere un salto.
BCS,

22

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:

  1. Fornire istruzioni al computer.
  2. 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, switchavrebbe sempre potuto essere compilato convertendolo in una catena equivalente di if-elseblocchi 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 switchin 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:

  1. 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 switchC in un jump-table, usando il codice macchina.

  2. 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 breakquando 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:

  1. 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.
  2. I miglioramenti nelle ottimizzazioni significano che la probabilità di switchessere 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.
  3. 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 switchblocchi 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.
  4. 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 gotoqui è 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 casecome etichetta in C #. In C # possiamo sbarazzarci below_sixdell'etichetta e usarlagoto case 5 che è più chiaro su ciò che stiamo facendo. (Dovremmo anche aggiungerebreakper il default, che ho lasciato solo per rendere chiaramente il codice C sopra non il codice C #).

In sintesi quindi:

  1. 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.
  2. 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.
  3. C # rimuove una possibile fonte di bug o codice incompreso che è stato ben documentato come causa di problemi negli ultimi quattro decenni.
  4. C # rende le best practice esistenti con C (fall fall document) applicabile dal compilatore.
  5. C # rende il caso insolito quello con un codice più esplicito, il solito caso quello con il codice che scrive automaticamente.
  6. C # usa lo stesso gotoapproccio per colpire lo stesso blocco da caseetichette diverse come viene usato in C. Lo generalizza solo in altri casi.
  7. C # rende l' gotoapproccio basato su tale approccio più conveniente e più chiaro di quanto non lo sia in C, consentendo casealle 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 + 240mentre 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 nopriempitivi.

† 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.


17

Puoi "andare all'etichetta della custodia" http://www.blackwasp.co.uk/CSharpGoto.aspx

L'istruzione goto è un semplice comando che trasferisce incondizionatamente il controllo del programma su un'altra istruzione. Il comando viene spesso criticato con alcuni sviluppatori che sostengono la sua rimozione da tutti i linguaggi di programmazione di alto livello perché può portare al codice spaghetti . Ciò si verifica quando ci sono così tante dichiarazioni goto o istruzioni jump simili che il codice diventa difficile da leggere e mantenere. Tuttavia, ci sono programmatori che sottolineano che l'istruzione goto, se usata con attenzione, fornisce una soluzione elegante ad alcuni problemi ...


8

Hanno lasciato fuori questo comportamento dalla progettazione per evitare che non fosse usato dalla volontà ma causasse problemi.

Può essere usato solo se non ci sono istruzioni nella parte del caso, come:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}

4

Hanno cambiato il comportamento dell'istruzione switch (da C / Java / C ++) per c #. Immagino che il ragionamento fosse che le persone si sono dimenticate della caduta e che sono stati causati errori. Un libro che ho letto diceva di usare goto per simulare, ma per me non sembra una buona soluzione.


2
C # supporta goto, ma non fallthrough? Wow. E non sono solo quelli. C # è l'unico linguaggio che conosco che si comporta in questo modo.
Matthew Scharley,

All'inizio non mi piaceva esattamente, ma "fall-thru" è davvero una ricetta per il disastro (specialmente tra i programmatori junior.) Come molti hanno sottolineato, C # consente ancora fall-thru per le righe vuote (che è la maggior parte dei casi.) "Kenny" ha pubblicato un link che evidenzia l'elegante utilizzo di Goto con la custodia.
Pretzel,

Non è un grosso problema secondo me. Il 99% delle volte non voglio perdere tempo e in passato sono stato bruciato da bug.
Ken,

1
"questa non suona come una buona soluzione per me" - mi dispiace sentirla su di te, perché è quello che goto caseserve. Il suo vantaggio rispetto al fallthrough è che è esplicito. Il fatto che alcune persone qui si oppongano goto casedimostra semplicemente di essere stati indottrinati contro "goto" senza alcuna comprensione del problema e che non sono in grado di pensare da soli. Quando Dijkstra scrisse "GOTO considerato dannoso", si stava rivolgendo a lingue che non avevano altri mezzi per modificare il flusso di controllo.
Jim Balter,

@JimBalter e poi quante persone citano Dijkstra citando Knuth che "l'ottimizzazione prematura è la radice di tutti i mali" sebbene quella citazione fosse quando Knuth stava esplicitamente scrivendo su quanto utile gotopuò essere quando si ottimizza il codice?
Jon Hanna,

1

È possibile ottenere fall come c ++ con la parola chiave goto.

EX:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}

9
Se solo qualcuno l'avesse pubblicato due anni prima!

0

È richiesta un'istruzione jump come un'interruzione dopo ogni blocco di maiuscole, compreso l'ultimo blocco, che si tratti di un'istruzione di caso o di un'istruzione di default. Con un'eccezione (a differenza dell'istruzione switch C ++), C # non supporta una caduta implicita da un'etichetta del caso a un'altra. L'unica eccezione è se un'istruzione case non ha codice.

- Documentazione C # switch ()


1
Mi rendo conto che questo comportamento è documentato, voglio sapere PERCHÉ è così e qualsiasi alternativa per ottenere il vecchio comportamento.
Matthew Scharley,

0

Dopo ogni istruzione case è necessaria un'istruzione break o goto anche se si tratta di un caso predefinito.


2
Se solo qualcuno l'avesse pubblicato due anni prima!

1
@Poldie è stato divertente la prima volta ... Shilpa non hai bisogno di una pausa o di andare per ogni caso, solo per ogni caso con il suo codice. Puoi avere più casi che condividono il codice bene.
Maverick,

0

Solo una breve nota per aggiungere che il compilatore per Xamarin ha effettivamente sbagliato e consente di fallire. Presumibilmente è stato corretto, ma non è stato rilasciato. L'ho scoperto in un codice che in realtà stava fallendo e il compilatore non si è lamentato.


0

dice switch (riferimento C #)

C # richiede la fine delle sezioni di commutazione, compresa quella finale,

Quindi devi anche aggiungere un break;alla tua defaultsezione, altrimenti ci sarà ancora un errore del compilatore.


Grazie, questo mi ha aiutato;)
CareTaker22

-1

C # non supporta fall fall con istruzioni switch / case. Non so perché, ma non c'è davvero supporto per questo. collegamento


Questo è così sbagliato. Vedi le altre risposte ... l'effetto di una caduta implicita può essere ottenuto con un esplicito goto case . Questa è stata una scelta di design intenzionale e saggia da parte dei designer C #.
Jim Balter,

-11

Hai dimenticato di aggiungere "break;" nell'istruzione case 3. Nel caso 2 l'hai scritta nel blocco if. Quindi prova questo:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}

2
Questo produce un output enormemente sbagliato. Ho lasciato fuori le dichiarazioni di switch dal design. La domanda è perché il compilatore C # vede questo come un errore quando quasi nessun altro linguaggio ha questa restrizione.
Matthew Scharley,

5
Che incredibile incomprensione. E hai avuto 5 anni per eliminarlo e non l'hai ancora fatto? Mindboggling.
Jim Balter,
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.