Come evitare le catene "if"?


266

Supponendo di avere questo pseudo-codice:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

Le funzioni executeStepXdovrebbero essere eseguite se e solo se la precedente ha esito positivo. In ogni caso, la executeThisFunctionInAnyCasefunzione dovrebbe essere chiamata alla fine. Sono un principiante nella programmazione, quindi mi dispiace per la domanda basilare: esiste un modo (per esempio in C / C ++) per evitare che quella lunga ifcatena produca quel tipo di "piramide di codice", a spese della leggibilità del codice ?

So che se potessimo saltare la executeThisFunctionInAnyCasechiamata di funzione, il codice potrebbe essere semplificato come:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Ma il vincolo è la executeThisFunctionInAnyCasechiamata di funzione. La breakdichiarazione potrebbe essere usata in qualche modo?


254
@ FrédéricHamidi wrong wrong wrong! Non dire mai che guidare il flusso del programma con le eccezioni è buono! Le eccezioni NON sono sicuramente adatte a questo scopo, per troppi motivi.
Piotr Zierhoffer,

26
@Piotr, sono stato viziato da Python (che in realtà lo incoraggia). So che le eccezioni non dovrebbero essere utilizzate per il controllo del flusso in C ++, ma è davvero il controllo del flusso qui? Una funzione che ritorna non potrebbe falseessere considerata simile a una situazione eccezionale?
Frédéric Hamidi,

13
Dipende dalla semantica di un programma. Un falseritorno può essere abbastanza normale.
dornhege,

28
Ho riportato la tua domanda alla sua prima revisione. Non dovresti cambiare radicalmente la tua domanda dopo aver ricevuto un certo numero di domande (> 0), perché ciò invaliderà tutte le risposte fornite fino a quel momento e creerà confusione. Apri invece una nuova domanda.
Scarpa il

13
Vorrei che tutti i "programmatori principianti" facessero domande di progettazione come questa.
Jezen Thomas,

Risposte:


486

È possibile utilizzare un &&(AND logico):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

questo soddisferà entrambi i tuoi requisiti:

  • executeStep<X>()dovrebbe valutare solo se il precedente ha avuto successo (questo si chiama valutazione del corto circuito )
  • executeThisFunctionInAnyCase() sarà eseguito in ogni caso

29
Ciò è sia semanticamente corretto (anzi, vogliamo che TUTTE le condizioni siano vere) sia un'ottima tecnica di programmazione (valutazione del corto circuito). Inoltre, questo può essere utilizzato in qualsiasi situazione complessa in cui la creazione di funzioni rovinerebbe il codice.
Sanchises,

58
@RobAu: Quindi sarà bene per i programmatori junior vedere finalmente il codice che si basa sulla valutazione scorciatoia e potrebbe spingerli a riprendere questo argomento che a loro volta li aiuterebbe a diventare programmatori senior. Quindi chiaramente un vantaggio: un codice decente e qualcuno ha imparato qualcosa leggendolo.
x4u,

24
Questa dovrebbe essere la risposta migliore
Visualizza nome

61
@RobAu: Questo non è un trucco che sfrutta qualche oscuro trucco di sintassi, è altamente idiomatico in quasi tutti i linguaggi di programmazione, al punto da essere una pratica standard indiscutibilmente.
BlueRaja - Danny Pflughoeft il

38
Questa soluzione si applica solo se le condizioni sono in realtà chiamate di funzioni semplici. Nel codice reale, tali condizioni possono essere lunghe 2-5 righe (e sono esse stesse combinazioni di molte altre &&e ||), quindi non c'è modo di unirle in un'unica ifistruzione senza rovinare la leggibilità. E non è sempre facile spostare queste condizioni su funzioni esterne, perché potrebbero dipendere da molte variabili locali calcolate in precedenza, il che creerebbe un disastro terribile se si tenta di passare ognuna come argomento individuale.
hamstergene,

358

Usa una funzione aggiuntiva per far funzionare la tua seconda versione:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

L'uso di if profondi (la tua prima variante) o il desiderio di uscire da "parte di una funzione" di solito significa che hai bisogno di una funzione extra.


51
+1 finalmente qualcuno ha pubblicato una risposta sana. Questo è il modo più corretto, sicuro e leggibile, secondo me.
Lundin,

31
+1 Questa è una buona illustrazione del "Principio della singola responsabilità". La funzione si foofa strada attraverso una sequenza di condizioni e azioni correlate. La funzione barè nettamente separata dalle decisioni. Se vedessimo i dettagli delle condizioni e delle azioni, si potrebbe scoprire che foosta ancora facendo troppo, ma per ora questa è una buona soluzione.
GraniteRobert,

13
Il rovescio della medaglia è che C non ha funzioni nidificate, quindi se quei 3 passaggi necessari per usare le variabili da barte dovessero passarli manualmente foocome parametri. Se questo è il caso e se fooviene chiamato solo una volta, mi sbaglierei nell'usare la versione goto per evitare di definire due funzioni strettamente accoppiate che finirebbero per non essere molto riutilizzabili.
hugomg,

7
Non sono sicuro della sintassi del corto circuito per C, ma in C # foo () potrebbe essere scritto comeif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis,

6
@ user1598390 I goto vengono utilizzati continuamente, specialmente nella programmazione di sistemi quando è necessario svolgere molto codice di configurazione.
Scotty Bauer,

166

I programmatori C della vecchia scuola usano gotoin questo caso. È l'unico utilizzo gotoche è in realtà incoraggiato dalla styleguide di Linux, si chiama exit della funzione centralizzata:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Alcune persone aggirano usando gotoavvolgendo il corpo in un anello e rompendosi da esso, ma in effetti entrambi gli approcci fanno la stessa cosa. L' gotoapproccio è migliore se hai bisogno di qualche altra pulizia solo se ha executeStepA()avuto successo:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Con l'approccio loop, in questo caso si otterrebbero due livelli di loop.


108
+1. Molte persone vedono un gotoe pensano subito "Questo è un codice terribile" ma ha i suoi usi validi.
Graeme Perrow,

46
Solo che questo è in realtà un codice piuttosto disordinato, dal punto di vista della manutenzione. Soprattutto quando ci sono più etichette, in cui il codice diventa anche più difficile da leggere. Ci sono modi più eleganti per raggiungere questo obiettivo: utilizzare le funzioni.
Lundin,

29
-1 Vedo il gotoe non devo nemmeno pensare di vedere che questo è un codice terribile. Una volta ho dovuto mantenere tale, è molto brutto. L'OP suggerisce una ragionevole alternativa al linguaggio C alla fine della domanda e l'ho inclusa nella mia risposta.
Saluti e hth. - Alf,

56
Non c'è niente di sbagliato in questo uso limitato e autonomo di goto. Ma attenzione, goto è una droga gateway e se non stai attento un giorno ti accorgerai che nessun altro mangia spaghetti con i piedi e lo fai da 15 anni a partire da quando hai pensato "Posso solo aggiustarlo altrimenti bug da incubo con un'etichetta ... "
Tim Post

66
Ho probabilmente scritto centomila righe di codice estremamente chiaro e di facile manutenzione in questo stile. Le due cose chiave da capire sono (1) disciplina! stabilire una chiara linea guida per il layout di ogni funzione e violarla solo in caso di estrema necessità, e (2) comprendere che ciò che stiamo facendo qui è simulare eccezioni in un linguaggio che non le possiede . throwè per molti versi peggio che gotoperché thrownon è nemmeno chiaro dal contesto locale in cui finirai! Utilizzare le stesse considerazioni di progettazione per il flusso di controllo in stile goto come si farebbe per le eccezioni.
Eric Lippert,

131

Questa è una situazione comune e ci sono molti modi comuni per affrontarla. Ecco il mio tentativo di risposta canonica. Si prega di commentare se ho perso qualcosa e terrò aggiornato questo post.

Questa è una freccia

Quello di cui stai discutendo è noto come anti-schema delle frecce . Si chiama freccia perché la catena di if nidificati forma blocchi di codice che si espandono sempre più a destra e poi di nuovo a sinistra, formando una freccia visiva che "punta" sul lato destro del riquadro dell'editor del codice.

Appiattire la freccia con la guardia

Alcuni modi comuni per evitare la freccia sono discussi qui . Il metodo più comune è utilizzare un modello di protezione , in cui il codice gestisce prima i flussi di eccezioni e quindi gestisce il flusso di base, ad esempio invece di

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... useresti ...

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Quando c'è una lunga serie di guardie, questo appiattisce considerevolmente il codice poiché tutte le guardie appaiono completamente a sinistra e i tuoi if non sono nidificati. Inoltre, stai accoppiando visivamente la condizione logica con il suo errore associato, il che rende molto più facile dire cosa sta succedendo:

Freccia:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Guardia:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Questo è oggettivamente e quantificabilmente più facile da leggere perché

  1. I caratteri {e} per un dato blocco logico sono più vicini
  2. La quantità di contesto mentale necessaria per comprendere una determinata linea è minore
  3. L'intera logica associata a una condizione if ha più probabilità di trovarsi su una pagina
  4. La necessità per il programmatore di scorrere la pagina / traccia dell'occhio è notevolmente ridotta

Come aggiungere un codice comune alla fine

Il problema con il modello di guardia è che si basa su ciò che viene chiamato "ritorno opportunistico" o "uscita opportunistica". In altre parole, interrompe il modello secondo cui ogni funzione dovrebbe avere esattamente un punto di uscita. Questo è un problema per due motivi:

  1. Sfrega alcune persone nel modo sbagliato, ad esempio le persone che hanno imparato a programmare su Pascal hanno imparato che una funzione = un punto di uscita.
  2. Non fornisce una sezione di codice che viene eseguita all'uscita a prescindere da quale sia l'argomento in questione.

Di seguito ho fornito alcune opzioni per aggirare questa limitazione utilizzando le funzionalità della lingua o evitando del tutto il problema.

Opzione 1. Non puoi farlo: usa finally

Sfortunatamente, come sviluppatore di c ++, non puoi farlo. Ma questa è la risposta numero uno per le lingue che contengono finalmente una parola chiave, poiché è esattamente quello che serve.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Opzione 2. Evitare il problema: ristrutturare le funzioni

È possibile evitare il problema suddividendo il codice in due funzioni. Questa soluzione ha il vantaggio di lavorare per qualsiasi lingua e inoltre può ridurre la complessità ciclomatica , che è un modo comprovato per ridurre la percentuale di difetti e migliora la specificità di eventuali test di unità automatizzati.

Ecco un esempio:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Opzione 3. Trucco linguistico: utilizzare un loop falso

Un altro trucco comune che vedo è usare while (true) e break, come mostrato nelle altre risposte.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Anche se questo è meno "onesto" rispetto all'uso goto, è meno incline ad essere incasinato durante il refactoring, in quanto segna chiaramente i confini dell'ambito logico. Un programmatore ingenuo che taglia e incolla le etichette o le gotodichiarazioni può causare gravi problemi! (E francamente il modello è così comune ora penso che comunichi chiaramente l'intento, e quindi non sia affatto "disonesto").

Esistono altre varianti di queste opzioni. Ad esempio, si potrebbe usare al switchposto di while. Qualsiasi costrutto di linguaggio con una breakparola chiave probabilmente funzionerebbe.

Opzione 4. Sfrutta il ciclo di vita dell'oggetto

Un altro approccio sfrutta il ciclo di vita dell'oggetto. Usa un oggetto di contesto per portare in giro i tuoi parametri (qualcosa che manca in modo sospetto al nostro esempio ingenuo) e smaltirlo quando hai finito.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Nota: assicurati di comprendere il ciclo di vita dell'oggetto della tua lingua preferita. Hai bisogno di una sorta di garbage collection deterministica perché questo funzioni, cioè devi sapere quando verrà chiamato il distruttore. In alcune lingue dovrai usare Disposeinvece di un distruttore.

Opzione 4.1. Sfrutta il ciclo di vita dell'oggetto (modello di wrapper)

Se hai intenzione di utilizzare un approccio orientato agli oggetti, puoi farlo nel modo giusto. Questa opzione utilizza una classe per "avvolgere" le risorse che richiedono la pulizia, così come le sue altre operazioni.

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Ancora una volta, assicurati di comprendere il ciclo di vita degli oggetti.

Opzione 5. Trucco linguistico: utilizzare la valutazione del corto circuito

Un'altra tecnica è quella di sfruttare la valutazione del corto circuito .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Questa soluzione sfrutta il modo in cui opera l'operatore &&. Quando il lato sinistro di && viene considerato falso, il lato destro non viene mai valutato.

Questo trucco è molto utile quando è richiesto un codice compatto e quando è probabile che il codice non richieda molta manutenzione, ad esempio si sta implementando un noto algoritmo. Per una codifica più generale la struttura di questo codice è troppo fragile; anche una piccola modifica alla logica potrebbe innescare una riscrittura totale.


12
Finalmente? C ++ non ha una clausola finally. Un oggetto con un distruttore viene utilizzato in situazioni in cui pensi di aver bisogno di una clausola finally.
Bill Door,

1
Modificato per accogliere i due commenti sopra.
John Wu,

2
Con un codice banale (ad esempio nel mio esempio) i modelli nidificati sono probabilmente più comprensibili. Con il codice del mondo reale (che potrebbe estendersi su più pagine), il modello di protezione è oggettivamente più facile da leggere perché richiederà meno scorrimento e meno tracciamento degli occhi, ad esempio la distanza media da {a} è più breve.
John Wu,

1
Ho visto un modello nidificato in cui il codice non era più visibile su uno schermo 1920 x 1080 ... Prova a scoprire quale codice di gestione degli errori verrà eseguito se la terza azione fallisce ... Ho usato do {...} mentre (0) invece non hai bisogno dell'interruzione finale (d'altra parte, mentre (true) {...} consente a "continue" di ricominciare da capo.
gnasher729

2
La tua opzione 4 è effettivamente una perdita di memoria in C ++ (ignorando l'errore di sintassi minore). Non si usa "nuovo" in questo tipo di caso, basta dire "MyContext myContext;".
Sumudu Fernando,

60

Basta fare

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

È così semplice.


A causa di tre modifiche, ognuna delle quali ha sostanzialmente modificato la domanda (quattro se una riporta la revisione alla versione 1), includo l'esempio di codice a cui sto rispondendo:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
Ho risposto (in modo più conciso) in risposta alla prima versione della domanda, e ha ottenuto 20 voti positivi prima di essere eliminato da Bill the Lizard, dopo alcuni commenti e modifiche di Darkness Races in Orbit.
Saluti e hth. - Alf,

1
@ Cheersandhth-Alf: non riesco a credere che sia stato eliminato dal mod. È semplicemente un peccato. (+1)
The Paramagnetic Croissant,

2
Il mio +1 per il tuo coraggio! (3 volte importa: D).
Hawcks,

È completamente importante che i programmatori principianti imparino cose come l'esecuzione degli ordini con più "e" booleani, come sia diverso in diverse lingue ecc. E questa risposta è un ottimo indicatore di ciò. Ma è solo un "non-principiante" nella normale programmazione commerciale. Non è semplice, non è gestibile, non è modificabile. Certo, se stai solo scrivendo un breve "codice da cowboy" per te stesso, fallo. Ma ha una sorta di "nessuna connessione con l'ingegneria del software di tutti i giorni come viene praticata oggi". A proposito scusa per la ridicola "confusione" che dovevi sopportare qui, @cheers :)
Fattie,

1
@JoeBlow: sto con Alf su questo - trovo la &&lista molto più chiara. Di solito rompo le condizioni per separare le linee e aggiungere un trascinamento // explanationa ciascuna .... Alla fine, è enormemente meno codice da guardare e una volta capito come &&funziona non c'è sforzo mentale in corso. La mia impressione è che la maggior parte dei programmatori C ++ professionisti ne abbia familiarità, ma come dici in diversi settori / progetti l'attenzione e l'esperienza differiscono.
Tony Delroy,

35

Esiste effettivamente un modo per rinviare le azioni in C ++: usare il distruttore di un oggetto.

Supponendo che tu abbia accesso a C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

E poi usando quella utility:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

67
Questo è solo un offuscamento completo per me. Non capisco davvero perché a così tanti programmatori C ++ piace risolvere un problema lanciando il maggior numero possibile di funzionalità linguistiche, fino al punto in cui tutti hanno dimenticato quale problema stavate effettivamente risolvendo: a loro non importa più, perché sono ora così interessato a usare tutte quelle caratteristiche del linguaggio esotico. E da lì in poi, puoi tenerti occupato per giorni e settimane a scrivere meta-codice, quindi mantenere il meta-codice, quindi scrivere meta-codice per gestire il meta-codice.
Lundin,

24
@Lundin: Beh, non capisco come si possa essere soddisfatti del fragile codice che si romperà non appena verrà introdotto un inizio / interruzione / ritorno o verrà generata un'eccezione. D'altra parte, questa soluzione è resiliente di fronte alla manutenzione futura e si basa solo sul fatto che i distruttori vengono eseguiti durante lo svolgimento, che è una delle caratteristiche più importanti del C ++. Qualificarlo come esotico mentre è il principio alla base che alimenta tutti i contenitori Standard è divertente, per non dire altro.
Matthieu M.,

7
@Lundin: il vantaggio del codice di Matthieu è che executeThisFunctionInAnyCase();verrà eseguito anche se foo();genera un'eccezione. Quando si scrive un codice sicuro per le eccezioni, è buona norma mettere tutte queste funzioni di pulizia in un distruttore.
Brian,

6
@Brian Quindi non gettare alcuna eccezione foo(). E se lo fai, prendilo. Problema risolto. Correggi i bug correggendoli, non scrivendo soluzioni alternative.
Lundin,

18
@Lundin: la Deferclasse è un piccolo pezzo di codice riutilizzabile che ti consente di eseguire qualsiasi pulizia di fine blocco, in modo eccezionalmente sicuro. è più comunemente noto come protezione dell'oscilloscopio . sì, qualsiasi uso di una protezione dell'oscilloscopio può essere espresso in altri modi più manuali, proprio come qualsiasi forloop può essere espresso come un blocco e un whileloop, che a sua volta può essere espresso con ife goto, che può essere espresso in linguaggio assembly se lo si desidera, o per coloro che sono veri maestri, alterando i frammenti in memoria tramite raggi cosmici diretti dall'effetto farfalla di grugniti e canti speciali. ma, perché farlo.
Saluti e hth. - Alf,

34

C'è una bella tecnica che non ha bisogno di una funzione wrapper aggiuntiva con le istruzioni return (il metodo prescritto da Itjax). Si avvale di un do while(0)pseudo-loop. Le while (0)assicura che non è in realtà un ciclo ma eseguito solo una volta. Tuttavia, la sintassi del ciclo consente l'uso dell'istruzione break.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
L'utilizzo di una funzione con più ritorni è simile, ma più leggibile, secondo me.
Lundin,

4
Sì, sicuramente è più leggibile ... tuttavia, dal punto di vista dell'efficienza, l'overhead di chiamata di funzione aggiuntiva (passaggio parametri e ritorno) può essere evitato con il costrutto do {} while (0).
Debasis,

5
Sei ancora libero di svolgere la funzione inline. Comunque, questa è una buona tecnica da sapere, perché aiuta in più di questo problema.
Ruslan,

2
@Lundin Devi tenere conto della localizzazione del codice, anche se la diffusione del codice in troppi luoghi ha dei problemi.
API-Beast,

3
Nella mia esperienza, questo è un linguaggio molto insolito. Mi ci vorrebbe un po 'per capire cosa diavolo stava succedendo, e questo è un brutto segno quando sto rivedendo il codice. Dato che sembra non avere vantaggi rispetto agli altri, approcci più comuni e quindi più leggibili, non ho potuto approvarlo.
Cody Grey

19

Puoi anche fare questo:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

In questo modo hai una dimensione di crescita lineare minima, +1 linea per chiamata ed è facilmente manutenibile.


EDIT : (Grazie @Unda) Non sei un grande fan perché perdi visibilità IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
Riguardava le chiamate accidentali di funzioni all'interno di push_back (), ma lo hai corretto comunque :)
Quentin,

4
Mi piacerebbe sapere perché questo ha avuto un downvote. Supponendo che i passaggi di esecuzione siano effettivamente simmetrici, come sembrano, quindi è pulito e mantenibile.
ClickRick,

1
Anche se questo potrebbe probabilmente sembrare più pulito. Potrebbe essere più difficile da capire per le persone ed è sicuramente più difficile da capire per il compilatore!
Roy T.

1
Trattare le tue funzioni più come dati è spesso una buona idea - una volta che hai fatto noterai anche i successivi refactoring. Ancora meglio è dove ogni pezzo che stai gestendo è un riferimento a un oggetto, non solo un riferimento a una funzione: questo ti darà ancora più possibilità di migliorare il tuo codice in futuro.
Bill K,

3
Leggermente troppo ingegnerizzato per un caso banale, ma questa tecnica ha definitivamente una bella caratteristica che altri non hanno: puoi cambiare l'ordine di esecuzione e il [numero di] funzioni chiamate in fase di esecuzione, e questo è carino :)
Xocoatzin

18

Funzionerebbe? Penso che questo sia equivalente al tuo codice.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
Di solito chiamo la variabile okquando utilizzo la stessa variabile in questo modo.
Macke,

1
Sarei molto interessato a sapere perché i downvotes. Cosa non va qui?
ABCplus,

1
confronta la tua risposta con l'approccio del corto circuito per la complessità ciclomatica.
AlphaGoku,

14

Supponendo che il codice desiderato sia come lo vedo attualmente:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Direi che l'approccio corretto, in quanto è il più semplice da leggere e più facile da mantenere, avrebbe meno livelli di rientro, che è (attualmente) lo scopo dichiarato della domanda.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Questo evita qualsiasi necessità di gotos, eccezioni, whileloop fittizi o altri costrutti difficili e va semplicemente avanti con il semplice lavoro a portata di mano.


Quando si usano i loop, è generalmente accettabile usare returne breaksaltare fuori dal loop senza la necessità di introdurre variabili "flag" extra. In questo caso, usare un goto sarebbe allo stesso modo innocuo - ricorda che stai commerciando una complessità goto extra con una complessità variabile extra mutabile.
hugomg,

2
@hugomg Le variabili erano nella domanda originale. Non c'è ulteriore complessità qui. Sono state fatte ipotesi sulla domanda (ad es. Che le variabili sono necessarie nel codice redatto), quindi sono state mantenute. Se non sono necessari, il codice può essere semplificato, ma data la natura incompleta della domanda non vi sono altre ipotesi che possano essere validamente formulate.
ClickRick,

Approccio molto utile, esp. per l'uso da parte di un autodidatta newbie, fornisce una soluzione più pulita senza inconvenienti. Noto anche che non si basa sul fatto di stepsavere la stessa firma o addirittura di essere funzioni piuttosto che blocchi. Vedo che questo viene usato come refactoring di primo passaggio anche quando è valido un approccio più sofisticato.
Keith,

12

L'istruzione break potrebbe essere utilizzata in qualche modo?

Forse non è la soluzione migliore ma puoi mettere le tue affermazioni in un do .. while (0)ciclo e usare le breakistruzioni invece di return.


2
Non sono stato io a declassarlo, ma questo sarebbe un abuso di un costrutto a ciclo continuo per qualcosa in cui l'effetto è ciò che è attualmente desiderato, ma inevitabilmente provocherebbe dolore. Probabilmente per il prossimo sviluppatore che dovrà mantenerlo tra 2 anni dopo essere passato a un altro progetto.
ClickRick,

3
Anche l'uso di @ClickRick do .. while (0)per le definizioni macro sta abusando dei loop ma è considerato OK.
ouah,

1
Forse, ma ci sono modi più puliti per raggiungerlo.
ClickRick,

4
@ClickRick l'unico scopo della mia risposta è rispondere Potrebbe essere usata in qualche modo la frase di interruzione e la risposta è sì, le prime parole nella mia risposta suggeriscono che questa potrebbe non essere la soluzione da usare.
ouah,

2
Questa risposta dovrebbe essere solo un commento
msmucker0527,

12

È possibile inserire tutte le ifcondizioni, formattate come desiderato in una funzione propria, al ritorno eseguire la executeThisFunctionInAnyCase()funzione.

Dall'esempio di base nell'OP, il test delle condizioni e l'esecuzione possono essere suddivisi come tali;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

E poi chiamato come tale;

InitialSteps();
executeThisFunctionInAnyCase();

Se sono disponibili lambda C ++ 11 (non c'era un tag C ++ 11 nell'OP, ma potrebbero essere ancora un'opzione), allora possiamo rinunciare alla funzione separata e racchiuderla in una lambda.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

Se non ti piacciono gotoe non ti piacciono i do { } while (0);loop e ti piace usare il C ++ puoi anche usare un lambda temporaneo per avere lo stesso effetto.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if non ti piace vai non && ti piace fare {} mentre (0) && ti piace C ++ ... Mi dispiace, non ho potuto resistere, ma quest'ultima condizione fallisce perché la domanda è taggata c così come c ++
ClickRick

@ClickRick è sempre difficile piacere a tutti. Secondo me non esiste nulla come C / C ++ che di solito codifichi in uno di questi e l'uso dell'altro è accigliato.
Alex,

9

Le catene di IF / ELSE nel tuo codice non sono il problema della lingua, ma il design del tuo programma. Se riesci a ricodificare o riscrivere il tuo programma, ti suggerisco di cercare in Design Patterns ( http://sourcemaking.com/design_patterns ) per trovare una soluzione migliore.

Di solito, quando vedi molti IF e altri nel tuo codice, è un'opportunità per implementare il modello di progettazione strategica ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) o forse una combinazione di altri schemi.

Sono sicuro che ci sono alternative per scrivere un lungo elenco di if / else, ma dubito che cambieranno qualcosa tranne che la catena ti sembrerà carina (Tuttavia, la bellezza è negli occhi di chi guarda si applica ancora al codice pure:-) ) . Dovresti preoccuparti di cose come (tra 6 mesi quando ho una nuova condizione e non ricordo nulla di questo codice, sarò in grado di aggiungerlo facilmente? O cosa succede se la catena cambia, quanto velocemente e senza errori lo implementerò)


9

Lo fai e basta ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 volte su 100, questo è l'unico modo per farlo.

Mai e poi mai, mai provare a fare qualcosa di "complicato" nel codice del computer.


A proposito, sono abbastanza sicuro che la seguente sia la vera soluzione che avevi in ​​mente ...

L' istruzione continue è fondamentale nella programmazione algoritmica. (Così come, la dichiarazione goto è fondamentale nella programmazione algoritmica.)

In molti linguaggi di programmazione puoi farlo:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Nota prima di tutto: i blocchi nudi come quell'esempio sono una parte fondamentale e importante della scrittura di un bellissimo codice, in particolare se hai a che fare con la programmazione "algoritmica".)

Ancora una volta, è esattamente quello che avevi in ​​testa, giusto? E questo è il modo bellissimo di scriverlo, quindi hai un buon istinto.

Tuttavia, tragicamente, nella versione attuale di goal-c (A parte - non so Swift, scusate) c'è una caratteristica risibile in cui controlla se il blocco che lo racchiude è un loop.

inserisci qui la descrizione dell'immagine

Ecco come aggirare quel ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Quindi non dimenticarlo ..

do {} while (false);

significa semplicemente "esegui questo blocco una volta".

cioè, non c'è assolutamente alcuna differenza tra la scrittura do{}while(false);e la semplice scrittura {}.

Ora funziona perfettamente come desiderato ... ecco l'output ...

inserisci qui la descrizione dell'immagine

Quindi, è possibile che sia così che vedi l'algoritmo nella tua testa. Dovresti sempre provare a scrivere quello che hai in testa. (Soprattutto se non sei sobrio, perché è lì che esce la bella! :))

Nei progetti "algoritmici" in cui ciò accade molto, nell'obiettivo-c, abbiamo sempre una macro come ...

#define RUNONCE while(false)

... quindi puoi farlo ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Ci sono due punti:

a, anche se è stupido che goal-c controlli il tipo di blocco in cui si trova un'istruzione continue, è preoccupante "combatterlo". Quindi è una decisione difficile.

b, c'è la domanda dovresti indentare, nell'esempio, quel blocco? Perdo il sonno per domande del genere, quindi non posso consigliare.

Spero che sia d'aiuto.



Hai messo la taglia per ottenere più rappresentante? :)
TMS

Invece di inserire tutti quei commenti in if, puoi anche usare nomi di funzioni più descrittivi e inserire i commenti nelle funzioni.
Thomas Ahle,

Che schifo. Prenderò la soluzione a 1 riga che si legge in modo conciso con la valutazione del corto circuito (che è stata nelle lingue per oltre 20 anni ed è ben nota) su questo pasticcio ogni giorno. Penso che possiamo entrambi concordare sul fatto che siamo felici di non lavorare l'uno con l'altro.
M2tM,

8

Chiedi alle tue funzioni di esecuzione di generare un'eccezione se falliscono invece di restituire false. Quindi il tuo codice chiamante potrebbe apparire così:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Naturalmente suppongo che nel tuo esempio originale il passaggio di esecuzione restituisca false solo nel caso in cui si verifichi un errore all'interno del passaggio?


3
l'uso dell'eccezione per controllare il flusso è spesso considerato una cattiva pratica e un codice maleodorante
user902383

8

Molte buone risposte già, ma la maggior parte di esse sembra compromettere alcuni (certamente molto poco) della flessibilità. Un approccio comune che non richiede questo compromesso è l'aggiunta di una variabile status / keep-going . Il prezzo è, ovviamente, un valore extra per tenere traccia di:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

Perché ok &= conditionX;e non semplicemente ok = conditionX;?
ABCplus,

@ user3253359 In molti casi, sì, puoi farlo. Questa è una demo concettuale; nel codice di lavoro cercheremmo di semplificarlo il più possibile
blgt

+1 Una delle poche risposte pulite e mantenibili che funziona in c , come stabilito nella domanda.
ClickRick

6

In C ++ (la domanda è taggata sia in C sia in C ++), se non puoi cambiare le funzioni per usare le eccezioni, puoi comunque usare il meccanismo delle eccezioni se scrivi una piccola funzione di supporto come

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Quindi il tuo codice potrebbe essere il seguente:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Se ti piace la sintassi elaborata, puoi invece farlo funzionare tramite cast esplicito:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Quindi puoi scrivere il tuo codice come

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Il refactoring dei controlli di valore in eccezioni non è necessariamente una buona strada da percorrere, vi sono notevoli eccezioni generali di svolgimento.
John Wu,

4
-1 L'uso di eccezioni per il flusso normale in C ++ come questo è una cattiva pratica di programmazione. Nel C ++ le eccezioni dovrebbero essere riservate a circostanze eccezionali.
Jack Aidley,

1
Dal testo della domanda (enfasi da me): "Le funzioni executeStepX dovrebbero essere eseguite se e solo se la precedente ha esito positivo. " In altre parole, il valore restituito viene utilizzato per indicare un errore. Cioè, questa è la gestione degli errori (e si spera che i guasti siano eccezionali). La gestione degli errori è esattamente ciò per cui sono state inventate le eccezioni.
Celtschk,

1
No. In primo luogo, sono state create eccezioni per consentire la propagazione degli errori , non la gestione degli errori ; in secondo luogo, "Le funzioni executeStepX devono essere eseguite se e solo se la precedente ha esito positivo." non significa che il booleano falso restituito dalla funzione precedente indichi un caso ovviamente eccezionale / errato. La tua affermazione è quindi non sequitur . La gestione degli errori e la sanificazione del flusso possono essere implementate in molti modi diversi, le eccezioni sono uno strumento che consente la propagazione degli errori e la gestione degli errori fuori posto , ed eccellere in questo.

6

Se il tuo codice è semplice come il tuo esempio e la tua lingua supporta le valutazioni di corto circuito, puoi provare questo:

StepA() && StepB() && StepC() && StepD();
DoAlways();

Se si stanno trasmettendo argomenti alle proprie funzioni e si ottengono altri risultati in modo che il codice non possa essere scritto nel modo precedente, molte delle altre risposte sarebbero più adatte al problema.


In effetti ho modificato la mia domanda per spiegare meglio l'argomento, ma è stato rifiutato per non invalidare la maggior parte delle risposte. : \
ABCplus

Sono un nuovo utente in SO e un programmatore principiante. 2 domande quindi: esiste il rischio che un'altra domanda come quella che hai detto sia contrassegnata come duplicata a causa di QUESTA domanda? Un altro punto è: come potrebbe un utente / programmatore SO principiante scegliere la migliore risposta tra tutti lì (quasi bene suppongo ..)?
ABCplus

6

Per C ++ 11 e oltre, un buon approccio potrebbe essere quello di implementare un sistema di uscita dell'ambito simile al meccanismo dell'ambito (uscita) di D.

Un modo possibile per implementarlo è utilizzare C ++ 11 lambdas e alcune macro di supporto:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Ciò ti consentirà di tornare presto dalla funzione e di garantire che qualsiasi codice di pulizia definito venga sempre eseguito all'uscita dall'ambito:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Le macro sono davvero solo decorazioni. MakeScopeExit()può essere usato direttamente.


Non è necessario che le macro funzionino. E di [=]solito è sbagliato per un lambda con ambito.
Yakk - Adam Nevraumont,

Sì, le macro sono solo per la decorazione e possono essere gettate via. Ma non diresti che l'acquisizione in base al valore è l'approccio "generico" più sicuro?
glampert

1
no: se il tuo lambda non persisterà oltre l'attuale ambito in cui viene creato il lambda, usa [&]: è sicuro e minimamente sorprendente. Cattura in base al valore solo quando la lambda (o le copie) potrebbe sopravvivere più a lungo del campo di applicazione nel punto della dichiarazione ...
Yakk - Adam Nevraumont

Sì, ha senso. Lo cambierò. Grazie!
glampert

6

Perché nessuno offre la soluzione più semplice? : D

Se tutte le tue funzioni hanno la stessa firma, puoi farlo in questo modo (per il linguaggio C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Per una soluzione C ++ pulita, è necessario creare una classe di interfaccia che contiene un metodo di esecuzione e avvolge i passi in oggetti.
Quindi, la soluzione sopra sarà simile a questa:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

Supponendo che non siano necessarie variabili di condizione individuali, invertire i test e utilizzare il else-falthrough in quanto il percorso "ok" consentirebbe di ottenere un set più verticale di istruzioni if ​​/ else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

L'omissione della variabile non riuscita rende il codice un po 'troppo oscuro per IMO.

Dichiarare le variabili all'interno va bene, non preoccuparti di = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Questo è oscuro, ma compatto:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

Sembra una macchina a stati, che è utile perché puoi facilmente implementarla con un modello a stati .

In Java sarebbe simile a questo:

interface StepState{
public StepState performStep();
}

Un'implementazione funzionerebbe come segue:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

E così via. Quindi puoi sostituire il grande if condition con:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

Come accennato da Rommik, potresti applicare un modello di design per questo, ma io userei il modello Decorator piuttosto che la strategia poiché vuoi fare una catena di chiamate. Se il codice è semplice, sceglierei una delle risposte ben strutturate per evitare l'annidamento. Tuttavia, se è complesso o richiede un concatenamento dinamico, il motivo Decoratore è una buona scelta. Ecco un diagramma di classe yUML :

diagramma di classe yUML

Ecco un esempio di programma LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

Il miglior libro da leggere sui modelli di design, IMHO, è Head First Design Patterns .


Qual è il vantaggio di questo rispetto a qualcosa come la risposta di Jefffrey?
Dason,

molto di più cambia resiliente, quando i requisiti cambiano questo approccio è più semplice da gestire senza molta conoscenza del dominio. Soprattutto se si considera la profondità e la durata di alcune sezioni di if nidificati. Tutto può diventare molto fragile e quindi ad alto rischio con cui lavorare. Non fraintendetemi, alcuni scenari di ottimizzazione potrebbero farvi sballare e tornare agli if, ma il 99% delle volte va bene. Ma il punto è che quando arrivi a quel livello non ti importa della manutenibilità, hai bisogno delle prestazioni.
John Nicholas,

4

Diverse risposte hanno suggerito uno schema che ho visto e usato molte volte, specialmente nella programmazione di rete. Negli stack di rete c'è spesso una lunga sequenza di richieste, ognuna delle quali può fallire e fermare il processo.

Lo schema comune era usare do { } while (false);

Ho usato una macro per while(false)renderlo do { } once;Il modello comune era:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Questo modello era relativamente facile da leggere e permetteva l'uso di oggetti che avrebbero distrutto correttamente e inoltre evitato i ritorni multipli rendendo più semplice il passo e il debug.


4

Per migliorare la risposta C ++ 11 di Mathieu ed evitare i costi di runtime sostenuti durante l'utilizzo di std::function, suggerirei di utilizzare quanto segue

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Questa semplice classe di template accetterà qualsiasi funzione che può essere chiamata senza parametri e lo fa senza allocazioni di memoria dinamica e quindi si conforma meglio all'obiettivo di astrazione del C ++ senza inutili spese generali. Il modello di funzione aggiuntivo è lì per semplificare l'utilizzo mediante la deduzione dei parametri del modello (che non è disponibile per i parametri del modello di classe)

Esempio di utilizzo:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Proprio come la risposta di Mathieu, questa soluzione è completamente sicura dalle eccezioni e executeThisFunctionInAnyCaseverrà chiamata in tutti i casi. In caso contrario executeThisFunctionInAnyCase, i distruttori sono contrassegnati in modo implicito noexcepte quindi std::terminateviene emessa una chiamata invece di provocare un'eccezione durante lo svolgimento dello stack.


+1 Stavo cercando questa risposta, quindi non avrei dovuto pubblicarla. Dovresti perfezionare l'avanzata functornel deferredcostruttore, non è necessario forzare a move.
Yakk - Adam Nevraumont,

@Yakk ha cambiato il costruttore in un costruttore di spedizioni
Joe

3

Sembra che tu voglia fare tutte le tue chiamate da un singolo blocco. Come altri l'hanno proposto, dovresti usare un whileciclo e lasciare usando breako una nuova funzione con cui puoi uscire return(potrebbe essere più pulita).

Personalmente bandisco goto, anche per l'uscita dalla funzione. Sono più difficili da individuare durante il debug.

Un'alternativa elegante che dovrebbe funzionare per il tuo flusso di lavoro è costruire un array di funzioni e iterare su questo.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

Fortunatamente, il debugger li individua per te. Se esegui il debug e non esegui il passaggio singolo del codice, lo stai facendo in modo sbagliato.
Cody Grey

Non capisco cosa intendi, né perché non potrei usare il passo singolo?
AxFab,

3

Dato che hai anche [... blocco di codice ...] tra le esecuzioni, immagino tu abbia allocazione di memoria o inizializzazioni di oggetti. In questo modo devi preoccuparti di pulire tutto ciò che hai già inizializzato all'uscita, e anche pulirlo se incontrerai un problema e una qualsiasi delle funzioni restituirà false.

In questo caso, la cosa migliore che ho avuto nella mia esperienza (quando ho lavorato con CryptoAPI) è stata la creazione di piccole classi, nel costruttore hai inizializzato i tuoi dati, nel distruttore non hai inizializzato. Ogni classe di funzione successiva deve essere figlia della classe di funzione precedente. Se qualcosa è andato storto, lancia un'eccezione.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

E poi nel tuo codice devi solo chiamare:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Immagino sia la soluzione migliore se ogni chiamata di ConditionX inizializza qualcosa, alloca memoria ed ecc. Meglio essere sicuri che tutto verrà pulito.


3

un modo interessante è lavorare con eccezioni.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Se scrivi tale codice, stai andando nella direzione sbagliata. Non vedrò come "il problema" avere tale codice, ma avere una "architettura" così disordinata.

Suggerimento: discuti quei casi con uno sviluppatore esperto di cui ti fidi ;-)


Penso che questa idea non possa sostituire ogni if-chain. Comunque, in molti casi, questo è un ottimo approccio!
WoIIe

3

Un altro approccio - do - whileloop, anche se è stato menzionato prima non vi era alcun esempio che mostrasse come sarebbe:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Beh, c'è già una risposta con whileloop ma do - whileloop non verifica ridondantemente true (all'inizio) ma invece alla fine xD (questo può essere ignorato, però)).


Ehi Zaffy - questa risposta ha una spiegazione massiccia dell'approccio do {} while (false). stackoverflow.com/a/24588605/294884 Lo hanno menzionato anche altre due risposte.
Fattie

È una domanda affascinante se è più elegante usare CONTINUE o BREAK in questa situazione!
Fattie

Ehi @JoeBlow Ho visto tutte le risposte ... volevo solo mostrare invece di parlarne :)
Zaffy

La mia prima risposta qui ho detto "Nessuno ha menzionato QUESTO ..." e immediatamente qualcuno ha gentilmente sottolineato che era la seconda risposta migliore :)
Fattie

@JoeBlow Eh, hai ragione. Proverò a risolverlo. Mi sento come ... xD Grazie comunque, la prossima volta pagherò un po 'di più :)
attesa
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.