Evitare il voodoo `goto`?


47

Ho una switchstruttura che ha diversi casi da gestire. L'operazione switchsu un enumche pone il problema del codice duplicato attraverso valori combinati:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

Attualmente la switchstruttura gestisce ciascun valore separatamente:

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

Ti viene l'idea lì. Attualmente ho questo suddiviso in una ifstruttura sovrapposta per gestire invece le combinazioni con una sola riga:

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

Questo pone il problema di valutazioni logiche incredibilmente lunghe che sono confuse da leggere e difficili da mantenere. Dopo averlo rifatto, ho iniziato a pensare alle alternative e ho pensato all'idea di una switchstruttura con fall-through tra i casi.

Devo usare a gotoin quel caso poiché C#non consente il fall-through. Tuttavia, impedisce le catene logiche incredibilmente lunghe anche se salta nella switchstruttura e porta ancora alla duplicazione del codice.

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

Questa non è ancora una soluzione abbastanza pulita e c'è uno stigma associato alla gotoparola chiave che vorrei evitare. Sono sicuro che ci deve essere un modo migliore per ripulirlo.


La mia domanda

Esiste un modo migliore per gestire questo caso specifico senza compromettere la leggibilità e la manutenibilità?


28
sembra che tu voglia avere un grande dibattito. Ma avrai bisogno di un esempio migliore di un buon caso per usare goto. flag enum risolve ordinatamente questo, anche se il tuo esempio if statement non era accettabile e mi sembra perfetto
Ewan

5
@Ewan Nessuno usa il goto. Quando è stata l'ultima volta che hai guardato il codice sorgente del kernel Linux?
John Douma,

6
Usi gotoquando la struttura di alto livello non esiste nella tua lingua. A volte vorrei che ci fosse un fallthru; parola chiave per sbarazzarsi di quel particolare uso di, gotoma vabbè.
Giosuè il

25
In termini generali, se senti la necessità di usare un gotolinguaggio di alto livello come C #, probabilmente hai trascurato molte altre (e migliori) alternative di progettazione e / o implementazione. Altamente scoraggiato.
code_dredd

11
L'uso di "goto case" in C # è diverso dall'uso di un "goto" più generale, perché è il modo in cui si ottiene una caduta esplicita.
Hammerite,

Risposte:


175

Trovo il codice difficile da leggere con le gotodichiarazioni. Consiglierei di strutturare il tuo in modo enumdiverso. Ad esempio, se il tuo enumera un campo di bit in cui ogni bit rappresentava una delle scelte, potrebbe apparire così:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

L'attributo Flags indica al compilatore che stai impostando valori che non si sovrappongono. Il codice che chiama questo codice potrebbe impostare il bit appropriato. Potresti quindi fare qualcosa del genere per chiarire cosa sta succedendo:

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

Ciò richiede il codice impostato myEnumper impostare correttamente i campi di bit e contrassegnato con l'attributo Flags. Ma puoi farlo modificando i valori degli enumerati nel tuo esempio in:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

Quando scrivi un numero nel modulo 0bxxxx, lo specifichi in binario. Quindi puoi vedere che abbiamo impostato i bit 1, 2 o 3 (beh, tecnicamente 0, 1 o 2, ma hai l'idea). È inoltre possibile assegnare un nome alle combinazioni utilizzando un OR bit a bit se le combinazioni possono essere impostate frequentemente insieme.


5
Esso appare così ! Ma deve essere C # 7.0 o successivo, immagino.
user1118321,

31
In ogni caso, si potrebbe anche fare in questo modo: public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Non è necessario insistere su C # 7 +.
Deduplicatore il

8
Puoi usare Enum.HasFlag
IMil

13
L' [Flags]attributo non segnala nulla al compilatore. Questo è il motivo per cui devi ancora dichiarare esplicitamente i valori enum come poteri di 2.
Joe Sewell,

19
@UKMonkey Non sono d'accordo con veemenza. HasFlagè un termine descrittivo ed esplicito per l'operazione in corso e ne estrae l'implementazione dalla funzionalità. Usare &perché è comune in altre lingue non ha più senso che usare un bytetipo invece di dichiarare un enum.
BJ Myers il

136

IMO la radice del problema è che questo pezzo di codice non dovrebbe nemmeno esistere.

Apparentemente hai tre condizioni indipendenti e tre azioni indipendenti da intraprendere se tali condizioni sono vere. Quindi perché tutto ciò viene incanalato in un pezzo di codice che ha bisogno di tre flag booleani per dirgli cosa fare (indipendentemente dal fatto che li offuschi o meno in un enum) e poi fa una combinazione di tre cose indipendenti? Il principio della responsabilità unica sembra avere una giornata libera qui.

Metti le chiamate alle tre funzioni a cui appartengono (ovvero dove scopri la necessità di eseguire le azioni) e consegna il codice in questi esempi nel cestino.

Se ci fossero dieci flag e azioni non tre, estenderesti questo tipo di codice per gestire 1024 combinazioni diverse? Spero di no! Se 1024 è troppi, anche 8 è troppi, per lo stesso motivo.


10
È un dato di fatto, ci sono molte API ben progettate che hanno tutti i tipi di flag, opzioni e altri parametri assortiti. Alcuni possono essere trasmessi, alcuni hanno effetti diretti, ma altri possono essere potenzialmente ignorati, senza interrompere SRP. Ad ogni modo, la funzione potrebbe persino decodificare alcuni input esterni, chi lo sa? Inoltre, la reductio ad absurdum porta spesso a risultati assurdi, soprattutto se il punto di partenza non è nemmeno così solido.
Deduplicatore il

9
Ha votato questa risposta. Se vuoi solo discutere di GOTO, questa è una domanda diversa da quella che hai posto. Sulla base delle prove presentate, hai tre requisiti disgiunti, che non dovrebbero essere aggregati. Da un punto di vista SE, sostengo che alephzero è la risposta corretta.

8
@Deduplicator Uno sguardo a questo miscuglio di alcune ma non tutte le combinazioni di 1,2 e 3 mostra che questa non è una "API ben progettata".
user949300

4
Questo così tanto. Le altre risposte non migliorano davvero su ciò che è nella domanda. Questo codice necessita di una riscrittura. Non c'è niente di sbagliato nello scrivere più funzioni.
Chiedere scusa e ripristinare Monica

3
Adoro questa risposta, in particolare "Se 1024 è troppi, anche 8 sono troppi". Ma è essenzialmente "usa il polimorfismo", no? E la mia (più debole) risposta sta ottenendo molte schifezze per averlo detto. Posso assumere la tua persona PR? :-)
user949300

29

Non usare mai goto è uno dei concetti di "bugie per bambini" dell'informatica. È il consiglio giusto il 99% delle volte, e i tempi non lo sono sono così rari e specializzati che è molto meglio per tutti se viene spiegato ai nuovi programmatori come "non usarli".

Quindi quando dovrebbero essere usati? Ci sono alcuni scenari , ma quello di base che sembra colpire è: quando si codifica una macchina a stati . Se non esiste un'espressione più organizzata e strutturata migliore del tuo algoritmo di una macchina a stati, allora la sua espressione naturale nel codice coinvolge rami non strutturati, e non c'è molto che si possa fare su ciò che probabilmente non rende la struttura del la macchina a stati stessa è più oscura piuttosto che minore.

Gli autori di compilatori lo sanno, motivo per cui il codice sorgente per la maggior parte dei compilatori che implementano i parser LALR * contiene goto. Tuttavia, pochissime persone codificheranno effettivamente i propri analizzatori e parser lessicali.

* - IIRC, è possibile implementare le grammatiche LALL interamente discendenti ricorsive senza ricorrere a tabelle di salto o altre istruzioni di controllo non strutturate, quindi se sei veramente anti-goto, questa è una via d'uscita.


Ora la domanda successiva è "Questo esempio è uno di quei casi?"

Quello che vedo guardando oltre è che hai tre diversi stati successivi possibili a seconda dell'elaborazione dello stato corrente. Dal momento che uno di questi ("default") è solo una riga di codice, tecnicamente potresti sbarazzartene affrontando quella riga di codice alla fine degli stati a cui si applica. Ciò ti porterebbe a 2 possibili stati successivi.

Uno dei rimanenti ("Tre") è ramificato solo da un punto che posso vedere. Quindi potresti liberartene allo stesso modo. Finiresti con un codice simile al seguente:

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

Tuttavia, anche questo è stato un esempio di giocattolo che hai fornito. Nel caso in cui "default" contenga una quantità non banale di codice, "tre" viene trasferito da più stati o (cosa ancora più importante) è probabile che un'ulteriore manutenzione possa aggiungere o complicare gli stati , onestamente staresti meglio usando goto (e forse anche sbarazzandosi della struttura del caso enumatico che nasconde la natura delle macchine a stati, a meno che non ci sia qualche buona ragione per cui deve rimanere).


2
@PerpetualJ - Beh, sono sicuramente felice che tu abbia trovato qualcosa di più pulito. Potrebbe essere comunque interessante, se questa è davvero la tua situazione, provarlo con i goto (nessun caso o enum. Solo blocchi etichettati e goto) e vedere come si confrontano in leggibilità. Detto questo, l'opzione "goto" non deve solo essere più leggibile, ma abbastanza più leggibile per evitare argomenti e tentativi di "correzione" di altri sviluppatori, quindi sembra che tu abbia trovato l'opzione migliore.
TED

4
Non credo che saresti "meglio usare gotos" se "è probabile che un'ulteriore manutenzione possa aggiungere o complicare gli stati". Stai davvero sostenendo che il codice spaghetti è più facile da mantenere rispetto al codice strutturato? Questo va contro ~ 40 anni di pratica.
user949300

5
@ user949300 - Non esercitarsi contro la costruzione di macchine a stati, no. Puoi semplicemente leggere il mio commento precedente su questa risposta per un esempio reale. Se provi a usare i fall-over del caso C, che impongono una struttura artificiale (il loro ordine lineare) su un algoritmo di macchina a stati che non ha intrinsecamente tale restrizione, ti ritroverai con complessità geometricamente crescente ogni volta che dovrai riorganizzare tutti i tuoi ordini per accogliere un nuovo stato. Se si codificano solo stati e transizioni, come richiede l'algoritmo, ciò non accade. I nuovi stati sono banali da aggiungere.
TED

8
@ user949300 - Ancora una volta, la compilazione di compilatori "~ 40 anni di pratica" mostra che le goto sono in realtà il modo migliore per parti di quel dominio problematico, motivo per cui se guardi il codice sorgente del compilatore, troverai quasi sempre goto.
TED

3
@ user949300 Il codice spaghetti non viene visualizzato solo intrinsecamente quando si utilizzano funzioni o convenzioni specifiche. Può apparire in qualsiasi lingua, funzione o convenzione in qualsiasi momento. La causa sta usando detto linguaggio, funzione o convenzione in un'applicazione per cui non era prevista e facendola piegare all'indietro per farcela. Usa sempre lo strumento giusto per il lavoro e sì, a volte questo significa usare goto.
Abion47

26

La migliore risposta è usare il polimorfismo .

Un'altra risposta, che, IMO, rende le cose più chiare e discutibilmente più brevi :

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto è probabilmente la mia 58esima scelta qui ...


5
+1 per suggerire il polimorfismo, anche se idealmente ciò potrebbe semplificare il codice del client fino a "Call ();", usando tell not ask - per spingere la logica decisionale lontano dal client consumante e nel meccanismo e nella gerarchia delle classi.
Erik Eidt,

12
Proclamare qualcosa che la vista migliore non vista è certamente molto coraggioso. Ma senza ulteriori informazioni, suggerisco un po 'di scetticismo e di mantenere una mente aperta. Forse soffre di esplosione combinatoria?
Deduplicatore il

Wow, penso che questa sia la prima volta che vengo mai votato per aver suggerito il polimorfismo. :-)
user949300

25
Alcune persone, di fronte a un problema, dicono "Userò solo il polimorfismo". Ora hanno due problemi e il polimorfismo.
Lightness Races con Monica il

8
In realtà ho fatto la tesi del mio Maestro sull'uso del polimorfismo di runtime per sbarazzarmi delle goto nelle macchine a stato dei compilatori. Questo è abbastanza fattibile, ma da allora ho scoperto che non vale la pena nella maggior parte dei casi. Richiede una tonnellata di codice di installazione OO, che ovviamente può finire con dei bug (e che questa risposta omette).
TED

10

Perché non questo:

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

OK, sono d'accordo, questo è un trucco (non sono uno sviluppatore C # a proposito, quindi mi scusi per il codice), ma dal punto di vista dell'efficienza questo è necessario? L'uso di enums come indice di array è C # valido.


3
Sì. Un altro esempio di sostituzione del codice con dati (che di solito è desiderabile, per velocità, struttura e manutenibilità).
Peter - Ripristina Monica il

2
Questa è un'ottima idea per l'ultima edizione della domanda, senza dubbio. E può essere usato per passare dal primo enum (un denso intervallo di valori) alle bandiere di azioni più o meno indipendenti che devono essere fatte in generale.
Deduplicatore il

Non ho accesso a un compilatore C #, ma forse il codice può essere reso più sicuro utilizzando questo tipo di codice int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };:?
Laurent Grégoire,

7

Se non puoi o non vuoi usare le bandiere, usa una funzione ricorsiva della coda. Nella modalità di rilascio a 64 bit, il compilatore produrrà un codice molto simile alla tua gotoaffermazione. Semplicemente non devi occupartene.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}

Questa è una soluzione unica! +1 Non penso di doverlo fare in questo modo, ma ottimo per i futuri lettori!
PerpetualJ

2

La soluzione accettata va bene ed è una soluzione concreta al tuo problema. Tuttavia, vorrei proporre una soluzione alternativa, più astratta.

Nella mia esperienza, l'uso degli enum per definire il flusso della logica è un odore di codice in quanto spesso è un segno di un design di classe scadente.

Mi sono imbattuto in un esempio reale di ciò che sta accadendo nel codice su cui ho lavorato l'anno scorso. Lo sviluppatore originale aveva creato una singola classe che eseguiva sia la logica di importazione che di esportazione e passava tra i due sulla base di un enum. Ora il codice era simile e aveva un po 'di codice duplicato, ma era abbastanza diverso che farlo rendeva il codice significativamente più difficile da leggere e praticamente impossibile da testare. Ho finito per refactoring in due classi separate, che hanno semplificato entrambi e in realtà mi hanno permesso di individuare ed eliminare un numero di bug non segnalati.

Ancora una volta, devo dire che l'uso degli enum per controllare il flusso della logica è spesso un problema di progettazione. Nel caso generale, Enums dovrebbe essere usato principalmente per fornire valori sicuri per il tipo e adatti ai consumatori in cui i valori possibili sono chiaramente definiti. Sono meglio utilizzati come proprietà (ad esempio, come ID colonna in una tabella) che come meccanismo di controllo logico.

Consideriamo il problema presentato nella domanda. Non conosco davvero il contesto qui, o cosa rappresenta questo enum. Sta pescando carte? Disegnando immagini? Prelievo di sangue? L'ordine è importante? Inoltre non so quanto sia importante la prestazione. Se le prestazioni o la memoria sono fondamentali, probabilmente questa soluzione non sarà quella desiderata.

In ogni caso, consideriamo l'enum:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

Ciò che abbiamo qui sono una serie di valori enum diversi che rappresentano concetti aziendali diversi.

Quello che potremmo usare invece sono le astrazioni per semplificare le cose.

Consideriamo la seguente interfaccia:

public interface IExample
{
  void Draw();
}

Possiamo quindi implementarlo come una classe astratta:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

Possiamo avere una classe concreta per rappresentare il Disegno Uno, due e tre (che per ragioni di argomento hanno una logica diversa). Questi potrebbero potenzialmente utilizzare la classe base sopra definita, ma suppongo che il concetto DrawOne sia diverso dal concetto rappresentato dall'enum:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

E ora abbiamo tre classi separate che possono essere composte per fornire la logica per le altre classi.

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

Questo approccio è molto più dettagliato. Ma ha dei vantaggi.

Considera la seguente classe, che contiene un bug:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

Quanto è più semplice individuare la chiamata drawThree.Draw () mancante? E se l'ordine è importante, anche l'ordine delle chiamate di sorteggio è molto facile da vedere e seguire.

Svantaggi di questo approccio:

  • Ognuna delle otto opzioni presentate richiede una classe separata
  • Questo utilizzerà più memoria
  • Questo renderà il tuo codice superficialmente più grande
  • A volte questo approccio non è possibile, anche se alcune variazioni potrebbero esserlo

Vantaggi di questo approccio:

  • Ognuna di queste classi è completamente testabile; perché
  • La complessità ciclomatica dei metodi di disegno è bassa (e in teoria potrei prendere in giro le classi DrawOne, DrawTwo o DrawThree se necessario)
  • I metodi di disegno sono comprensibili: uno sviluppatore non deve legare il cervello in nodi per capire cosa fa il metodo
  • I bug sono facili da individuare e difficili da scrivere
  • Le classi si compongono in classi di livello più alto, il che significa che definire una classe ThreeThreeThree è facile da fare

Considera questo (o un approccio simile) ogni volta che senti la necessità di avere un codice di controllo logico complesso scritto nelle istruzioni del caso. In futuro sarai felice di averlo fatto.


Questa è una risposta molto ben scritta e concordo pienamente con l'approccio. Nel mio caso particolare qualcosa di così dettagliato sarebbe eccessivo all'infinito considerando il codice insignificante dietro ciascuno di essi. Per metterlo in prospettiva, immagina che il codice stia semplicemente eseguendo una Console.WriteLine (n). Questo è praticamente l'equivalente di ciò che stava facendo il codice con cui stavo lavorando. Nel 90% di tutti gli altri casi, la tua soluzione è sicuramente la risposta migliore quando segui OOP.
PerpetualJ,

Sì, punto giusto, questo approccio non è utile in ogni situazione. Volevo metterlo qui perché è normale che gli sviluppatori si fissino su un approccio particolare per risolvere un problema e talvolta paga fare un passo indietro e cercare alternative.
Stephen,

Sono completamente d'accordo, è in realtà il motivo per cui sono finito qui perché stavo cercando di risolvere un problema di scalabilità con qualcosa che un altro sviluppatore ha scritto per abitudine.
PerpetualJ,

0

Se intendi utilizzare un interruttore qui, il tuo codice sarà effettivamente più veloce se gestisci ciascun caso separatamente

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

solo una singola operazione aritmetica viene eseguita in ciascun caso

inoltre, come altri hanno affermato se stai pensando di usare gotos, dovresti probabilmente ripensare il tuo algoritmo (anche se ammetterò che la mancanza del caso C # potrebbe essere una ragione per usare un goto). Vedi il famoso articolo di Edgar Dijkstra "Vai alla dichiarazione considerata dannosa"


3
Direi che questa è un'ottima idea per qualcuno che deve affrontare una questione più semplice, quindi grazie per averla pubblicata. Nel mio caso non è così semplice.
PerpetualJ

Inoltre, oggi non abbiamo esattamente i problemi che Edgar aveva al momento di scrivere il suo documento; la maggior parte delle persone al giorno d'oggi non hanno nemmeno un motivo valido per odiare il goto a parte questo rende il codice difficile da leggere. Bene, in tutta onestà, se viene abusato, allora sì, altrimenti è intrappolato nel metodo contenitivo, quindi non puoi davvero fare il codice spaghetti. Perché spendere sforzi per aggirare una caratteristica di una lingua? Direi che goto è arcaico e che dovresti pensare ad altre soluzioni nel 99% dei casi d'uso; ma se ne hai bisogno, ecco a cosa serve.
PerpetualJ

Questo non si ridimensionerà bene quando ci sono molti booleani.
user949300

0

Per il tuo esempio particolare, poiché tutto ciò che volevi veramente dall'enumerazione era un indicatore do / do-not per ciascuno dei passaggi, la soluzione che riscrive le tue tre ifaffermazioni è preferibile a una switched è positivo che tu l'abbia resa la risposta accettata .

Ma se avessi una logica più complessa che non ha funzionato in modo così pulito, allora trovo ancora confusa la gotos nella switchdichiarazione. Preferirei vedere qualcosa del genere:

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

Questo non è perfetto ma penso che sia meglio così che con la gotos. Se le sequenze di eventi sono così lunghe e si duplicano così tanto che non ha davvero senso precisare la sequenza completa per ciascun caso, preferirei una subroutine piuttosto che gotoper ridurre la duplicazione del codice.


0

Non sono sicuro che qualcuno abbia davvero un motivo per odiare la parola chiave goto in questi giorni. È decisamente arcaico e non è necessario nel 99% dei casi d'uso, ma è una caratteristica del linguaggio per un motivo.

Il motivo per odiare la gotoparola chiave è come il codice

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Oops! Questo chiaramente non funzionerà. La messagevariabile non è definita in quel percorso di codice. Quindi C # non lo passerà. Ma potrebbe essere implicito. Tener conto di

object.setMessage("Hello World!");

label:
object.display();

E supponiamo che displayquindi abbia la WriteLinedichiarazione in esso.

Questo tipo di bug può essere difficile da trovare perché gotooscura il percorso del codice.

Questo è un esempio semplificato. Supponiamo che un esempio reale non sia altrettanto ovvio. Potrebbero esserci cinquanta righe di codice tra label:e l'uso di message.

Il linguaggio può aiutare a risolvere questo problema limitando il modo in cui gotopuò essere utilizzato, scendendo solo dai blocchi. Ma il C # gotonon è limitato così. Può saltare il codice. Inoltre, se hai intenzione di limitare goto, è altrettanto bene cambiare il nome. Altre lingue usano breakper scendere da blocchi, con un numero (di blocchi da uscire) o un'etichetta (su uno dei blocchi).

Il concetto di gotoè un'istruzione di linguaggio macchina di basso livello. Ma l'intero motivo per cui abbiamo linguaggi di livello superiore è che siamo limitati alle astrazioni di livello superiore, ad esempio portata variabile.

Detto questo, se usi C # gotoall'interno di switchun'istruzione per saltare da un caso all'altro, è ragionevolmente innocuo. Ogni caso è già un punto di ingresso. Penso ancora che chiamarlo gotosia sciocco in quella situazione, poiché unisce questo uso innocuo di gotoforme più pericolose. Preferirei piuttosto che usassero qualcosa del genere continueper quello. Ma per qualche ragione, si sono dimenticati di chiedermelo prima di scrivere la lingua.


2
Nella C#lingua non è possibile saltare le dichiarazioni delle variabili. Per prova, avvia un'applicazione console e inserisci il codice che hai fornito nel tuo post. Verrà visualizzato un errore del compilatore nell'etichetta in cui si afferma che l'errore è l' uso della variabile locale non assegnata "messaggio" . Nelle lingue in cui ciò è consentito è una preoccupazione valida, ma non nel linguaggio C #.
PerpetualJ

0

Quando hai così tante scelte (e anche di più, come dici tu), allora forse non si tratta di codice ma di dati.

Creare un dizionario che associa i valori enum alle azioni, espressi come funzioni o come un tipo enum più semplice che rappresenta le azioni. Quindi il tuo codice può essere ridotto a una semplice ricerca nel dizionario, seguita dalla chiamata del valore della funzione o da un interruttore sulle scelte semplificate.


-7

Usa un loop e la differenza tra break e continue.

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
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.