Interruzione dell’affermazione in C #?

L’affermazione di switch switch è uno dei miei principali motivi personali per amare switch vs. if/else if constructs. Un esempio è in ordine 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 stanno facendo i cretini perché la string[] deve essere dichiarata al di fuori della funzione: beh, lo sono, questo è solo un esempio.

Il compilatore non riesce con il seguente errore:

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

Perché? E c’è un modo per ottenere questo tipo di comportamento senza avere tre if s?

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

Falling through switchcase s può essere ottenuto non avendo alcun codice in un case (vedi case 0 ), o usando il goto case speciale 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; } 

Il “perché” è quello di evitare il ricaduta accidentale, di cui sono grato. Questa è una fonte non comune di bug in C e Java.

La soluzione alternativa è usare 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 a mio avviso. Si è avvicinato troppo a C – ci sono alcune modifiche utili che potrebbero essere fatte in termini di scope ecc. Probabilmente un interruttore più intelligente che potrebbe fare la corrispondenza dei modelli, ecc. Sarebbe utile, ma in realtà sta cambiando dal passaggio a “controllare una sequenza di condizioni” – a quel punto potrebbe essere richiesto un nome diverso.

L’interruttore di caduta è storicamente una delle principali fonti di bug nei moderni software. Il progettista del linguaggio ha deciso di rendere obbligatorio saltare alla fine del caso, a meno che non si passi automaticamente al caso successivo direttamente senza elaborazione.

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

Per aggiungere alle risposte qui, penso che valga la pena considerare la domanda opposta in congiunzione con questo, vale a dire. perché C ha permesso di 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 il modo migliore per servire questi due obiettivi. Da un lato, più facile è trasformare in istruzioni informatiche (che siano codice macchina, bytecode come IL, o le istruzioni siano interpretate in esecuzione) quindi più capace quel processo di compilazione o interpretazione sarà efficiente, affidabile e compatto in uscita. Portato all’estremo, questo objective si traduce nella nostra semplice scrittura in assembly, IL, o anche codici operativi grezzi, perché la compilazione più semplice è dove non esiste alcuna compilazione.

Viceversa, più il linguaggio esprime l’intenzione del programmatore, piuttosto che i mezzi utilizzati a tal fine, più comprensibile è il programma sia durante la scrittura che durante la manutenzione.

Ora, lo switch potrebbe sempre essere stato compilato convertendolo nella catena equivalente di if-else blocks o simili, ma è stato progettato per consentire la compilazione in un particolare modello di assembly comune in cui uno prende un valore, ne calcola uno sfalsato (sia per cercare una tabella indicizzata da un perfetto hash del valore, o dalla reale aritmetica sul valore *). Vale la pena notare a questo punto che oggi la compilazione C # a volte converte l’ switch in if-else e talvolta utilizza un approccio di salto basato su hash (e allo stesso modo con C, C ++ e altri linguaggi con syntax simile).

In questo caso ci sono due buoni motivi per consentire il fall-through:

  1. Succede comunque naturalmente: se costruisci una tabella di salto in una serie di istruzioni e uno dei primi batch di istruzioni non contiene alcuna sorta di salto o ritorno, l’esecuzione procederà naturalmente al prossimo lotto. Consentire il fall-through era ciò che “succedeva” se si girava il switch -utilizzando C in jump-table-usando il codice macchina.

  2. I coder che hanno scritto in assembly erano già stati utilizzati nell’equivalente: quando scrivevano un jump table a mano in assembly, dovevano considerare se un dato blocco di codice sarebbe terminato con un ritorno, un salto fuori dal tavolo o semplicemente continuare su al prossimo blocco. In quanto tale, avere il coder aggiungere un’interruzione esplicita quando necessario era “naturale” anche per il programmatore.

A quel tempo, quindi, era un ragionevole tentativo di bilanciare i due obiettivi di un linguaggio informatico in relazione sia al codice macchina prodotto che all’espressività del codice sorgente.

Tuttavia, dopo quarant’anni, le cose non sono proprio le stesse, per alcune ragioni:

  1. I programmatori in C oggi potrebbero avere poca o nessuna esperienza di assemblaggio. I coder in molti altri linguaggi in stile C sono anche meno probabili (specialmente Javascript!). Ogni concetto di “ciò che le persone sono abituate a fare da assemblea” non è più rilevante.
  2. Miglioramenti delle ottimizzazioni significano che la probabilità che lo switch sia trasformato in if-else perché è stato ritenuto l’approccio più efficiente, o che è diventato una variante particolarmente esoterica dell’approccio jump-table è più alto. La mapping tra gli approcci di livello superiore e inferiore non è forte come una volta.
  3. L’esperienza ha dimostrato che il fall-through tende ad essere il caso di minoranza piuttosto che la norma (uno studio del compilatore di Sun ha rilevato che il 3% dei blocchi di switch utilizzava un fall-through diverso da più etichette sullo stesso blocco e si pensava che l’uso -case qui significava che questo 3% era in effetti molto più alto del normale). Quindi la lingua così studiata rende l’insolito più facile da soddisfare rispetto al comune.
  4. L’esperienza ha dimostrato che il fall-through tende ad essere la fonte di problemi sia nei casi in cui è accidentalmente fatto, sia nei casi in cui il fall-through corretto viene ignorato 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 tuo fall-through può ancora causare problemi.

In relazione a questi ultimi due punti, considera la seguente citazione dall’edizione corrente di K & R:

Cadere da un caso all’altro non è robusto, essendo incline alla 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.

Per una buona forma, fai una pausa dopo l’ultimo caso (il default qui) anche se logicamente non è necessario. Un giorno in cui un altro caso verrà aggiunto alla fine, questo bit di programmazione difensiva ti salverà.

Quindi, dalla bocca del cavallo, la caduta in C è problematica. È considerata una buona pratica documentare sempre i commenti con i commenti, che è un’applicazione del principio generale che si dovrebbe documentare dove si fa qualcosa di insolito, perché è quello che farà scappare più tardi l’esame del codice e / o renderà il proprio codice simile ha il bug di un novizio in esso quando è in realtà corretto.

E quando ci pensi, codice come questo:

 switch(x) { case 1: foo(); /* FALLTHRU */ case 2: bar(); break; } 

Aggiungere qualcosa per rendere esplicito il fall-through nel codice, non è qualcosa che può essere rilevato (o la cui assenza può essere rilevata) dal compilatore.

In quanto tale, il fatto che debba essere esplicito con ricaduta in C # non aggiunge alcuna penalità alle persone che hanno scritto bene in altri linguaggi in stile C, poiché sarebbero già espliciti nei loro fall-through. †

Infine, l’uso di goto qui è già una norma di C e di altri linguaggi di questo tipo:

 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 venga incluso nel codice eseguito per un valore diverso da quello che porta uno al blocco precedente, dobbiamo già utilizzare 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 basa sul già normale modo di affrontare una situazione in cui vogliamo colpire più di un blocco di codice in un switch , e semplicemente lo generalizza per coprire anche il fall-through. Ha anche reso entrambi i casi più convenienti e autodocumentanti, dal momento che dobbiamo aggiungere una nuova etichetta in C, ma possiamo usare la case come etichetta in C #. In C # possiamo sbarazzarci dell’etichetta below_six e usare goto case 5 che è più chiaro di ciò che stiamo facendo. (Dovremmo anche aggiungere break per il default , che ho omesso 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 il codice C ha fatto 40 anni fa (e nemmeno C in questi giorni), il che rende irrilevante una delle ispirazioni del fall-through.
  2. C # rimane compatibile con C non solo avendo break implicita, per un apprendimento più facile della lingua da parte di coloro che hanno familiarità con linguaggi simili e un porting più semplice.
  3. C # rimuove una ansible fonte di bug o codice frainteso che è stato ben documentato come causa di problemi negli ultimi quattro decenni.
  4. C # rende la best practice esistente con C (document fall through) applicabile dal compilatore.
  5. C # rende il caso insolito quello con il codice più esplicito, nel caso consueto quello con il codice uno scrive automaticamente.
  6. C # usa lo stesso approccio goto per colpire lo stesso blocco da diverse case come è usato in C. Lo generalizza solo in altri casi.
  7. C # rende quell’approccio basato sul goto più conveniente e più chiaro di quanto lo sia in C, consentendo alle istruzioni case di agire come etichette.

Tutto sumto, una decisione di progettazione piuttosto ragionevole


* Alcune forms di BASIC consentirebbero di fare cose del calibro di GOTO (x AND 7) * 50 + 240 che, per quanto fragile e quindi un caso particolarmente persuasivo per vietare il goto , serve a mostrare un equivalente di lingua superiore del tipo di modo quel codice di livello inferiore può eseguire un salto basato sull’aritmetica su un valore, che è molto più ragionevole quando è il risultato della compilazione piuttosto che qualcosa che deve essere mantenuto manualmente. In particolare, le implementazioni del dispositivo Duff si prestano bene al codice macchina o IL equivalente poiché ogni blocco di istruzioni sarà spesso della stessa lunghezza senza l’aggiunta di riempitivi nop .

† Il dispositivo di Duff arriva di nuovo qui, come ragionevole eccezione. Il fatto che con questo e altri modelli ci sia una ripetizione delle operazioni serve a rendere l’uso del fall-through relativamente chiaro anche senza un commento esplicito a tale effetto.

Puoi ‘andare all’etichetta del caso’ http://www.blackwasp.co.uk/CSharpGoto.aspx

L’istruzione goto è un semplice comando che trasferisce incondizionatamente il controllo del programma a 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 a codice spaghetti . Ciò si verifica quando ci sono così tante istruzioni goto o dichiarazioni di salto 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 …

Hanno omesso questo comportamento in base alla progettazione per evitare quando non è stato utilizzato dalla volontà ma ha causato 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; } 

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 gli errori sono stati causati. Un libro che ho letto dice di usare goto per simulare, ma non mi sembra una buona soluzione.

Dopo ogni dichiarazione di caso richiedono un’istruzione break o goto anche se è un caso predefinito.

Un’istruzione di salto come un’interruzione è richiesta dopo ogni blocco di casi, incluso l’ultimo blocco, sia che si tratti di un’istruzione case o di un’istruzione predefinita. Con una eccezione, (a differenza dell’istruzione switch C ++), C # non supporta un’inclusione implicita da un’etichetta del caso a un’altra. L’unica eccezione è se un’istruzione case non ha codice.

– Documentazione C # switch ()

C # non supporta le istruzioni switch / case. Non so perché, ma non c’è davvero alcun supporto per questo. collegamento

È ansible ottenere fallire come c ++ dalla 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; } 

Solo una breve nota per aggiungere che il compilatore per Xamarin in realtà ha sbagliato e consente il fallthrough. È stato probabilmente riparato, ma non è stato rilasciato. Scoperto questo in qualche codice che stava effettivamente cadendo e il compilatore non si lamentava.

switch (C # Reference) dice

C # richiede la fine delle sezioni di switch, inclusa quella finale,

Quindi devi anche aggiungere una break; alla tua sezione di default , altrimenti ci sarà ancora un errore del compilatore.

Hai dimenticato di aggiungere la “pausa” dichiarazione nel caso 3. Nel caso 2 lo hai scritto 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"); }