Le istruzioni 'switch' sono generalmente utilizzate in modo errato? [chiuso]

3

Vedo la maggior parte degli sviluppatori che utilizzano switch istruzioni con break s in ogni case . Questo uso di dichiarazioni switch è inappropriato in quanto sarebbe sufficiente un semplice insieme di istruzioni if-else ?

Va bene per me pensare che l'unica ragione per cui tu usi un switch è perché due o più case s sono correlati, quindi vuoi intenzionalmente che alcune istruzioni "falliscano"?

Un esempio in cui penso che sia OK usare un switch è il caso in cui si desidera aggiornare lo schema di un database; quindi, se stai aggiornando dalla versione 1 alla versione 4, vuoi assicurarti che lo schema passi prima attraverso le versioni 2 e 3:

private void updateDatabase(int oldVersion){
   switch(oldVersion){
      case 1:
         updateToVersion2();
      case 2:
         updateToVersion3();
      case 3:
         updateToVersion4();
   }
}

Un esempio NON OK di switch è dove lo uso in un videogioco per spostare un personaggio in base al pulsante premuto. Dovrei break ogni case in modo che quando si preme LEFT il carattere non si muova in tutte le direzioni:

private void onButtonPressed(Button button){
   switch(button){
      case LEFT:
         moveLeft();
         break;
      case RIGHT:
         moveRight();
         break;
      case UP:
         moveUp();
         break;
      case DOWN:
         moveDown();
         break;
    }
 }
    
posta AxiomaticNexus 14.01.2015 - 02:15
fonte

4 risposte

4

No,% dichiarazioni diswitch non vengono generalmente utilizzate in modo errato.

Sono per lo più utilizzati per l'uso previsto: enumerazione delle alternative di azione per un insieme minimo di valori di input possibili. È più leggibile quindi una lunga catena if / else, i compilatori possono emettere un codice molto efficiente se i valori verificati sono ragionevolmente contigui, ed è più facile scrivere compilatori di controllo degli errori che ti avvisano quando dimentichi un'alternativa. Tutto questo va bene. (Usare switch quando l'ereditarietà e la spedizione dinamica sarebbero preferibili è un errore comune tra i principianti che non capiscono molto bene OOP, ma questo è un problema di livello superiore.)

Tuttavia, switch è definito male nella maggior parte delle lingue.

Dato che il caso d'uso più comune è di fare qualcosa di diverso per ogni caso, è fastidioso e ingombrante dover scrivere break dopo ogni caso che non è un fall-through. L'istruzione switch funzionerebbe molto meglio se break fosse il default dopo un case body, e dovevi ordinare fall-through tramite una parola chiave fallthrough esplicita (a meno che due case s siano direttamente adiacenti). Ciò eliminerebbe molte linee inutili e molte, molte persone non avrebbero commesso errori sottili dimenticando un break .

Tuttavia, è decisamente troppo tardi ora per fare qualcosa al riguardo; C fa in questo modo, e nessuno dei suoi discendenti ha avuto il coraggio di cambiare qualcosa al riguardo, quindi per il momento, switch probabilmente rimarrà così com'è.

    
risposta data 14.01.2015 - 08:22
fonte
9

Poiché if/else è simile a switch e spesso intercambiabile, la tua confusione è comprensibile.

Alcune lingue, come Python, non hanno nemmeno un'istruzione switch . Non sorprendentemente, quando si utilizza Google per "python switch", il primo risultato punta a un'alternativa: una mappa. Non un if/else , ma una mappa.

Mentre puoi usare if/else ogni volta invece che switch , dovresti anche capire che i tuoi colleghi non sono sbagliati quando usano switch dichiarazione. O è stato semplicemente detto da un insegnante o da un mentore che dovrebbero usare switch quando ci sono più di due rami, o semplicemente lo trovano più elegante o leggibile , entrambi i termini sono perfettamente soggettivi.

Il tuo primo esempio, a proposito, mi infastidisce molto. Se fossi un recensore del tuo codice, lo segnalerei immediatamente, perché è facile ignorare la mancanza di dichiarazioni break , quindi sembra come se oldVersion è 2 , solo updateToVersion3() verrà eseguito, ma non updateToVersion4() . Evita di scrivere codice soggetto a errori. Ad esempio, tale sintassi è proibita in C # proprio per evitare errori che sono così facilmente evitabili.

Il tuo secondo esempio è un caso in cui switch va perfettamente bene . Anche se utilizzerei invece una mappa (funzioni di mappatura, o anche espressioni lambda, e non i loro risultati) o ereditarietà, passerei alla dichiarazione switch se so che il mio codice verrà letto dai principianti che potrebbero non capire quei due approcci.

Se le linee guida di stile del tuo codice base lo consentono, il tuo secondo esempio potrebbe anche essere scritto in questo modo:

switch (button){
    case LEFT: moveLeft(); break;
    case RIGHT: moveRight(); break;
    case UP: moveUp(); break;
    case DOWN: moveDown(); break;
}

o così:

switch (button){
    case LEFT:  moveLeft();  break;
    case RIGHT: moveRight(); break;
    case UP:    moveUp();    break;
    case DOWN:  moveDown();  break;
}

rendendo il codice più breve e più leggibile (anche in questo caso, la leggibilità è soggettiva) rispetto a una variante if/elif/else .

Un aspetto importante evidenziato di J Trana è che in questo secondo esempio, if/elif/else potrebbe essere soggetto a errori. Non riesco a trovare facilmente un esempio più esplicativo in cui è possibile migrare a switch , ma la seguente parte di codice mostra l'idea. Sei in grado di capire immediatamente perché il codice seguente a volte genera un'eccezione in fase di esecuzione (immagina che siano le sei del pomeriggio e hai passato tre ore a ispezionare il codice)?

if (!fs.FileExists(fileFullPath)) {
    debug("The file appears to be missing. Check if the location is set in the options.");
}
if (fs.ReadFile(fileFullPath).Contains(textToFind)) {
    found = true;
    debug("The file contains the expected text.");
}
else {
    debug("The text was not found.");
}

Anche in questo caso, in C #, la guida di stile ufficiale ti impedirà di scrivere questa parte di codice soggetta a errori: sarà necessario aggiungere un'interruzione di riga prima del secondo if , e l'autore del codice vedrà immediatamente che ha scritto if invece di else if .

Ecco perché, per ridurre il rischio di scrivere codice che non esprime chiaramente le intenzioni dell'autore e può essere facilmente letto male:

  • Evita le break s mancanti nelle istruzioni switch , tranne nel caso in cui il precedente case non contenga alcuna logica:

    switch (something)
    {
        case 1: // Not having a 'break' is fine. The intention of the author is clear.
        case 2:
            hello();
            break;
    
        case 3:
            world();
            break;
    }
    
  • Evita else if che può essere confuso con o, per errore, sostituito da, un if , a meno che le linee guida sullo stile non ti proteggano.

  • Usa mappe ed ereditarietà quando appropriato. In la maggior parte dei casi, le mappe e l'ereditarietà sono più appropriate di if/elif/else o switch . Il più lungo if/elif/else o switch tendono ad essere un buon segno che dovrebbero essere sostituiti da una mappa (spesso) o un'ereditarietà (raramente) o dovrebbero essere completamente refactored (spesso).

    Il record che ho visto finora è una dichiarazione switch contenente circa il mille% dicase s (scritta da un esperto orgoglioso della sua esperienza professionale di 15 anni come sviluppatore principale ).

  • Appiattisci le gerarchie quando possibile. Questo codice:

    if (n == a) {
        f1();
    }
    else {
        if (n == b) {
            f2();
        }
        else {
            f3();
        }
    }
    

    dovrebbe essere immediatamente refactored in:

    if (n == a) {
        f1();
    }
    else if (n == b) {
        f2();
    }
    else {
        f3();
    }
    

    o

    switch (n) {
        case a:  f1(); break;
        case a:  f2(); break;
        default: f3();
    }
    
  • Attenzione ai programmatori folle. Questo pezzo di codice è equivalente a quello del punto precedente, se a , b , c e n sono numeri (le cose sarebbero diverse se si trattasse di chiamate a funzioni con effetti collaterali):

    if (!(n == a)) {
        if (n - b == 0 && !(n > b)) {
            f2();
        }
    }
    
    if (!(n == a) && n - b < 0 || b - n < 0) {
        f3();
    }
    
    if (n != a) {
        // Do nothing.
    }
    else {
        if (!(n > a) && ! (n < a)) {
            f1();
        }
    }
    
risposta data 14.01.2015 - 03:46
fonte
2

Vedo ciò che stai dicendo, ma in definitiva non è improprio.

Se è solo uno o due elses , sarà sufficiente una modifica if-else . Ma se crescerà di più, direi che le dichiarazioni di switch possono trasformarsi rapidamente in qualcosa che è solo più pulito e meno pungente da guardare. Oppure ... a seconda di chi sei personalmente, potrebbe trasformarlo in qualcosa che è ancora più disordinato.

È rigorosamente cosmetico nel caso in cui si esce sempre da tutto, ma dire che è particolarmente negativo è come dire che tra /*...*/ commenti e // commenti, uno è completamente migliore dell'altro. Se non c'è niente di sbagliato nelle lunghe catene if-else , allora cosa c'è di sbagliato nelle più banali dichiarazioni switch ? È solo cosmetico, nei casi in cui si applica questa domanda, e troverai persone su entrambi i lati del recinto su cui sembra più ordinato. Non esiste un'enorme convenzione su questo.

Ma se qualcosa non va con le dichiarazioni switch in cui si esce da tutto, invece di usare solo if-else chains, allora cosa c'è per giustificare switch dichiarazioni in cui non si esce da tutto ? Prendi il tuo primo esempio (che sono d'accordo con Brandon , era nitido!):

private void updateDatabase(int oldVersion){
    switch(oldVersion){
        case 1:
            updateToVersion2();
        case 2:
            updateToVersion3();
        case 3:
            updateToVersion4();
    }
}

Non è possibile che sia stato scritto così facilmente come segue:

private void updateDatabase(int oldVersion){
    if (oldVersion > 0 && oldVersion < 4){
        if (oldVersion < 3){
            if (oldVersion < 2){
                updateToVersion2();
            }
            updateToVersion3();
        }
        updateToVersion4();
    }
}

Questo è migliore o peggiore esteticamente? È una questione di opinione. Va meglio o peggio in termini di prestazioni? Tecnicamente, a seconda di come viene compilato il linguaggio, c'è probabilmente un confronto di int in più rispetto a% di co_de che è stato introdotto, ma questo causerà una frazione microscopica del colpo di prestazioni che usando un linguaggio di programmazione di alto livello ? Realisticamente, probabilmente hai già fatto in modo che int sia superiore a oldVersion , oppure dovresti aggiungere un controllo simile alla tua funzione con l'istruzione 0 .

Alcune persone pensano anche questo:

switch (true)
{
    case variable1:
        doSomething1();
        break;

    case variable2 && variable3:
        doSomething2();
        break;

    case !variable4 && variabl5:
        doSomething3();
        break;
        .
        .
        .
    default:
        doSomethingElse();
}

è in definitiva un codice più pulito di:

if (variable1)
{
    doSomething1();
}
else if (variable2 && variable3)
{
    doSomething2();
}
else if (!variable4 && variable5)
{
    doSomething3();
.
.
.
}
else
{
    doSomethingElse();
}

(Si noti la differenza in cui le parentesi graffe sono state collocate negli ultimi due frammenti, quando le persone lo fanno in questo modo, che è oggetto di un dibattito in corso in alcune lingue, probabilmente influenzeranno la loro disposizione verso la pulizia di% dichiarazioni diswitch.)

Non c'è niente di sbagliato nell'usuale uso di breakout di tutte le istruzioni di switch , ma un programmatore esperto dovrebbe essere in grado di vedere che cosa è in loro possesso.

    
risposta data 14.01.2015 - 03:37
fonte
2

Alcune lingue ( Scala compilate in JVM, Haskell, Ocaml, ecc ...) hanno corrispondenza del modello che è superiore a switch poiché decostruisce le strutture dati e le variabili di collegamento (localmente al caso corrispondente).

Ad esempio, in Ocaml puoi definire un semplicistico AST tipo di dati (un tipo di somma o unione taggata ) per espressioni come:

type expr = 
    Num of int
   | Sum of expr * expr
   | Diff of expr * expr
   | Prod of expr * expr
  (* etc... *)
;;

Quindi dovresti rappresentare l'AST di 2*(3+4) come

   Prod (Num 2, Sum (Num 3, Num 4))

e la tua funzione di valutazione ricorsiva è

let rec eval exp = 
  match exp with
   Num x -> x
   | Sum (el,er) -> (eval el) + (eval er)
   | Diff (el,er) -> (eval el) - (eval er)
   | Prod (el,er) -> (eval el) * (eval er)
   (* etc....*)

Si noti che le variabili di corrispondenza el e er hanno il loro ambito limitato a ogni clausola di corrispondenza, come le righe precedenti che iniziano con | (e sono associate all'operazione di corrispondenza)

Il compilatore generalmente traduce la corrispondenza del modello in switch come costrutti (ad esempio salti indicizzati) ed estrazioni di campi.

    
risposta data 14.01.2015 - 09:46
fonte

Leggi altre domande sui tag