Comment éviter les chaînes «si»?


266

En supposant que j'ai ce pseudo-code:

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

executeThisFunctionInAnyCase();

Les fonctions executeStepXdoivent être exécutées si et seulement si la précédente réussit. Dans tous les cas, la executeThisFunctionInAnyCasefonction doit être appelée à la fin. Je suis un débutant en programmation, donc désolé pour la question très basique: existe-t-il un moyen (en C / C ++ par exemple) d'éviter que cette longue ifchaîne produise ce genre de "pyramide de code", au détriment de la lisibilité du code ?

Je sais que si nous pouvions ignorer l' executeThisFunctionInAnyCaseappel de fonction, le code pourrait être simplifié comme suit:

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

Mais la contrainte est l' executeThisFunctionInAnyCaseappel de fonction. La breakdéclaration pourrait-elle être utilisée d'une manière ou d'une autre?


254
@ FrédéricHamidi mal tort mal! Ne dites jamais que conduire votre flux de programme avec des exceptions est bon! Les exceptions ne sont certainement PAS adaptées à cette fin, pour trop de raisons.
Piotr Zierhoffer du

26
@Piotr, j'ai été gâté par Python (ce qui encourage réellement cela). Je sais que les exceptions ne sont pas censées être utilisées pour le contrôle de flux en C ++, mais est-ce vraiment le contrôle de flux ici? Une fonction de retour ne pourrait-elle pas falseêtre considérée comme apparentée à une situation exceptionnelle?
Frédéric Hamidi

13
Cela dépend de la sémantique d'un programme. Un falseretour peut être assez normal.
dornhege

28
J'ai ramené votre question à sa première révision. Vous ne devez pas changer radicalement votre question après avoir reçu un certain nombre de questions (> 0), car cela invaliderait toutes les réponses données jusqu'à ce moment et créerait de la confusion. Ouvrez plutôt une nouvelle question.
Chaussure

13
Je souhaite que tous les «programmeurs débutants» posent des questions de conception comme celle-ci.
Jezen Thomas

Réponses:


486

Vous pouvez utiliser un &&(logique ET):

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

cela répondra à vos deux exigences:

  • executeStep<X>()ne doit évaluer que si le précédent a réussi (c'est ce qu'on appelle l' évaluation des courts-circuits )
  • executeThisFunctionInAnyCase() sera exécuté dans tous les cas

29
C'est à la fois sémantiquement correct (en effet, nous voulons que TOUTES les conditions soient vraies) ainsi qu'une très bonne technique de programmation (évaluation des courts-circuits). De plus, cela peut être utilisé dans toute situation complexe où la création de fonctions gâcherait le code.
Sanchises

58
@RobAu: Ensuite, il sera bon pour les programmeurs juniors de voir enfin du code qui repose sur une évaluation abrégée et pourrait les inciter à reprendre ce sujet, ce qui les aiderait à leur tour à devenir des programmeurs seniors. Donc, clairement un gagnant-gagnant: un code décent et quelqu'un a appris quelque chose en le lisant.
x4u

24
Cela devrait être la meilleure réponse
Nom d'affichage

61
@RobAu: Ce n'est pas un hack tirant parti d'une astuce de syntaxe obscure, il est très idiomatique dans presque tous les langages de programmation, au point d'être une pratique incontestablement standard.
BlueRaja - Danny Pflughoeft

38
Cette solution ne s'applique que si les conditions sont réellement des appels de fonction simples. Dans le code réel, ces conditions peuvent être 2-5 lignes longues (et être eux - mêmes combinaisons de beaucoup d' autres &&et ||) donc il n'y a aucun moyen que vous voulez les rejoindre en simple ifdéclaration sans vissage la lisibilité. Et il n'est pas toujours facile de déplacer ces conditions vers des fonctions externes, car elles peuvent dépendre de nombreuses variables locales précédemment calculées, ce qui créerait un terrible gâchis si vous essayez de passer chacune d'elles comme argument individuel.
hamstergene

358

Utilisez simplement une fonction supplémentaire pour faire fonctionner votre deuxième version:

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'utilisation d'if profondément imbriqués (votre première variante) ou le désir de rompre avec "une partie d'une fonction" signifie généralement que vous avez besoin d'une fonction supplémentaire.


51
+1 enfin quelqu'un a posté une réponse sensée. À mon avis, c'est la manière la plus correcte, la plus sûre et la plus lisible.
Lundin

31
+1 C'est une bonne illustration du "principe de responsabilité unique". La fonction se foofraye un chemin à travers une séquence de conditions et d'actions liées. La fonction barest proprement séparée des décisions. Si nous avons vu les détails des conditions et des actions, il se pourrait que cela fooen fasse encore trop, mais pour l'instant c'est une bonne solution.
GraniteRobert

13
L'inconvénient est que C n'a pas de fonctions imbriquées, donc si ces 3 étapes nécessaires pour utiliser des variables de barvotre part devraient les passer manuellement en footant que paramètres. Si tel est le cas et si elle foon'est appelée qu'une seule fois, je me tromperais vers l'utilisation de la version goto pour éviter de définir deux fonctions étroitement couplées qui finiraient par ne pas être très réutilisables.
hugomg

7
Pas sûr de la syntaxe de court-circuit pour C, mais en C # foo () pourrait s'écrireif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Les Goto sont utilisés tout le temps, en particulier dans la programmation de systèmes lorsque vous devez dérouler beaucoup de code de configuration.
Scotty Bauer

166

Les programmeurs C de la vieille école utilisent gotodans ce cas. C'est le seul usage gotoqui est réellement encouragé par le guide de style Linux, il s'appelle l'exit de la fonction centralisée:

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;
}

Certaines personnes contournent l'utilisation gotoen enveloppant le corps dans une boucle et en la rompant, mais les deux approches font effectivement la même chose. L' gotoapproche est meilleure si vous avez besoin d'un autre nettoyage uniquement si elle executeStepA()a réussi:

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;
}

Avec l'approche en boucle, vous vous retrouveriez avec deux niveaux de boucles dans ce cas.


108
+1. Beaucoup de gens voient un gotoet pensent immédiatement "C'est un code terrible" mais il a ses utilisations valides.
Graeme Perrow

46
Sauf qu'il s'agit en fait d'un code assez salissant, du point de vue de la maintenance. Surtout lorsqu'il y a plusieurs étiquettes, où le code devient également plus difficile à lire. Il existe des moyens plus élégants d'y parvenir: utilisez les fonctions.
Lundin

29
-1 Je vois le gotoet je n'ai même pas besoin de penser pour voir que c'est un code terrible. J'ai dû une fois le maintenir, c'est très méchant. Le PO propose une alternative raisonnable en langage C à la fin de la question, et je l'ai incluse dans ma réponse.
Bravo et hth. - Alf

56
Il n'y a rien de mal à cette utilisation limitée et autonome de goto. Mais attention, le goto est un médicament de passerelle et si vous ne faites pas attention, vous réaliserez un jour que personne d'autre ne mange de spaghetti avec ses pieds, et vous le faites depuis 15 ans après avoir pensé "Je peux juste résoudre ce problème sinon bug cauchemardesque avec une étiquette ... "
Tim Post

66
J'ai écrit probablement cent mille lignes de code extrêmement clair et facile à entretenir dans ce style. Les deux choses clés à comprendre sont (1) la discipline! établir une ligne directrice claire pour la disposition de chaque fonction et ne la violer qu'en cas de besoin extrême, et (2) comprendre que ce que nous faisons ici est de simuler des exceptions dans un langage qui n'en a pas . throwest à bien des égards pire que gotoparce qu'avec throwce n'est même pas clair du contexte local où vous allez vous retrouver! Utilisez les mêmes considérations de conception pour le flux de contrôle de style goto que pour les exceptions.
Eric Lippert

131

Il s'agit d'une situation courante et il existe de nombreuses façons courantes de la gérer. Voici ma tentative de réponse canonique. Veuillez commenter si j'ai raté quelque chose et je garderai ce post à jour.

Ceci est une flèche

Ce que vous discutez est connu sous le nom d' anti-modèle de flèche . Il s'agit d'une flèche car la chaîne d'if imbriqués forme des blocs de code qui se développent de plus en plus à droite puis de nouveau à gauche, formant une flèche visuelle qui "pointe" vers le côté droit du volet de l'éditeur de code.

Aplatissez la flèche avec la garde

Nous discutons ici de quelques moyens courants d'éviter la flèche . La méthode la plus courante consiste à utiliser un modèle de garde , dans lequel le code gère d'abord les flux d'exception, puis gère le flux de base, par exemple au lieu de

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

... vous utiliseriez ....

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

Lorsqu'il y a une longue série de gardes, cela aplatit considérablement le code car toutes les gardes apparaissent complètement à gauche et vos ifs ne sont pas imbriqués. De plus, vous associez visuellement la condition logique à son erreur associée, ce qui permet de savoir plus facilement ce qui se passe:

La Flèche:

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;
}

Garde:

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;
} 

Ceci est objectivement et quantifiablement plus facile à lire car

  1. Les caractères {et} pour un bloc logique donné sont plus proches les uns des autres
  2. La quantité de contexte mental nécessaire pour comprendre une ligne particulière est plus petite
  3. L'intégralité de la logique associée à une condition if est plus susceptible d'être sur une seule page
  4. La nécessité pour le codeur de faire défiler la page / l'œil est considérablement réduite

Comment ajouter du code commun à la fin

Le problème avec le modèle de garde est qu'il repose sur ce qu'on appelle le «retour opportuniste» ou la «sortie opportuniste». En d'autres termes, cela rompt le modèle selon lequel chaque fonction doit avoir exactement un point de sortie. Il s'agit d'un problème pour deux raisons:

  1. Cela frotte certaines personnes dans le mauvais sens, par exemple les personnes qui ont appris à coder sur Pascal ont appris qu'une fonction = un point de sortie.
  2. Il ne fournit pas de section de code qui s'exécute à la sortie, quel que soit le sujet traité .

Ci-dessous, j'ai fourni quelques options pour contourner cette limitation soit en utilisant les fonctionnalités du langage, soit en évitant complètement le problème.

Option 1. Vous ne pouvez pas faire cela: utilisez finally

Malheureusement, en tant que développeur c ++, vous ne pouvez pas faire cela. Mais c'est la réponse numéro un pour les langues qui contiennent un mot-clé finally, car c'est exactement à cela qu'il sert.

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

Option 2. Évitez le problème: restructurez vos fonctions

Vous pouvez éviter le problème en divisant le code en deux fonctions. Cette solution a l'avantage de fonctionner pour n'importe quelle langue, et en outre, elle peut réduire la complexité cyclomatique , qui est un moyen éprouvé de réduire votre taux de défauts, et améliore la spécificité de tous les tests unitaires automatisés.

Voici un exemple:

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

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

Option 3. Astuce linguistique: utilisez une fausse boucle

Une autre astuce que je vois consiste à utiliser while (true) et break, comme indiqué dans les autres réponses.

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

Bien que cela soit moins «honnête» que l'utilisation goto, il est moins susceptible d'être gâché lors de la refactorisation, car il marque clairement les limites de la portée logique. Un codeur naïf qui coupe et colle vos étiquettes ou vos gotorelevés peut causer de gros problèmes! (Et franchement, le modèle est si commun maintenant je pense qu'il communique clairement l'intention, et n'est donc pas du tout "malhonnête").

Il existe d'autres variantes de cette option. Par exemple, on pourrait utiliser à la switchplace de while. Toute construction de langage avec un breakmot clé fonctionnerait probablement.

Option 4. Tirez parti du cycle de vie des objets

Une autre approche exploite le cycle de vie des objets. Utilisez un objet contextuel pour transporter vos paramètres (quelque chose que notre exemple naïf manque étrangement) et éliminez-le lorsque vous avez terminé.

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
}

Remarque: assurez-vous de bien comprendre le cycle de vie de l'objet de la langue de votre choix. Vous avez besoin d'une sorte de ramasse-miettes déterministe pour que cela fonctionne, c'est-à-dire que vous devez savoir quand le destructeur sera appelé. Dans certaines langues, vous devrez utiliser Disposeun destructeur.

Option 4.1. Tirer parti du cycle de vie de l'objet (modèle wrapper)

Si vous allez utiliser une approche orientée objet, autant le faire correctement. Cette option utilise une classe pour «encapsuler» les ressources qui nécessitent un nettoyage, ainsi que ses autres opérations.

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

Encore une fois, assurez-vous de bien comprendre le cycle de vie de votre objet.

Option 5. Astuce linguistique: utiliser l'évaluation de court-circuit

Une autre technique consiste à profiter de l' évaluation des courts-circuits .

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

Cette solution tire parti du fonctionnement de l'opérateur &&. Lorsque le côté gauche de && est évalué à faux, le côté droit n'est jamais évalué.

Cette astuce est plus utile lorsque du code compact est requis et lorsque le code ne risque pas de voir beaucoup de maintenance, par exemple, vous implémentez un algorithme bien connu. Pour un codage plus général, la structure de ce code est trop fragile; même une modification mineure de la logique pourrait déclencher une réécriture totale.


12
Finalement? C ++ n'a pas de clause finally. Un objet avec un destructeur est utilisé dans les situations où vous pensez avoir besoin d'une clause finally.
Bill Door

1
Modifié pour tenir compte des deux commentaires ci-dessus.
John Wu

2
Avec du code trivial (par exemple dans mon exemple), les modèles imbriqués sont probablement plus compréhensibles. Avec le code du monde réel (qui peut s'étendre sur plusieurs pages), le motif de garde est objectivement plus facile à lire car il nécessitera moins de défilement et moins de suivi des yeux, par exemple la distance moyenne de {à} est plus courte.
John Wu

1
J'ai vu un motif imbriqué où le code n'était plus visible sur un écran 1920 x 1080 ... Essayez de savoir quel code de gestion des erreurs sera effectué si la troisième action a échoué ... J'ai utilisé do {...} pendant (0) à la place pour que vous n'ayez pas besoin de la pause finale (d'autre part, tandis que (true) {...} permet à "continuer" de tout recommencer.
gnasher729

2
Votre option 4 est une fuite de mémoire en C ++ (en ignorant l'erreur de syntaxe mineure). On n'utilise pas "nouveau" dans ce genre de cas, dites simplement "MyContext myContext;".
Sumudu Fernando

60

Fais juste

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

C'est si simple.


En raison de trois modifications auxquelles chacune a fondamentalement changé la question (quatre si l'on compte la révision jusqu'à la version 1), j'inclus l'exemple de code auquel je réponds:

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

executeThisFunctionInAnyCase();

14
J'ai répondu à cette question (de façon plus concise) en réponse à la première version de la question, et elle a reçu 20 votes positifs avant d'être supprimée par Bill le Lézard, après quelques commentaires et modifications par Darkness Races in Orbit.
Bravo et hth. - Alf

1
@ Cheersandhth-Alf: Je n'arrive pas à croire qu'il a été supprimé du mod. C'est vraiment dommage. (+1)
The Paramagnetic Croissant

2
Mon +1 à votre courage! (3 fois importe: D).
haccks

Il est tout à fait important que les programmeurs débutants apprennent des choses comme l'exécution des ordres avec plusieurs "et" booléens, comment cela est différent dans différentes langues, etc. Et cette réponse est un excellent indicateur de cela. Mais c'est juste un "non-starter" dans la programmation commerciale normale. Ce n'est pas simple, ce n'est pas maintenable, ce n'est pas modifiable. Bien sûr, si vous écrivez simplement un "code de cow-boy" rapide pour vous-même, faites-le. Mais il n'a en quelque sorte «aucun lien avec l'ingénierie logicielle quotidienne telle qu'elle est pratiquée aujourd'hui». BTW désolé pour la "confusion-montage" ridicule que vous avez dû endurer ici, @cheers :)
Fattie

1
@JoeBlow: Je suis avec Alf à ce sujet - je trouve la &&liste beaucoup plus claire. Je brise généralement les conditions sur des lignes distinctes et j'ajoute une fin // explanationà chacune ... En fin de compte, c'est beaucoup moins de code à regarder, et une fois que vous comprenez comment cela &&fonctionne, il n'y a pas d'effort mental continu. Mon impression est que la plupart des programmeurs C ++ professionnels seraient familiers avec cela, mais comme vous le dites dans différents secteurs / projets, l'objectif et l'expérience diffèrent.
Tony Delroy

35

Il existe en fait un moyen de différer les actions en C ++: en utilisant le destructeur d'un objet.

En supposant que vous avez accès à 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

Et puis en utilisant cet utilitaire:

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
Ce n'est que de l'obscurcissement complet pour moi. Je ne comprends vraiment pas pourquoi tant de programmeurs C ++ aiment résoudre un problème en y jetant autant de fonctionnalités de langage que possible, jusqu'au point où tout le monde a oublié quel problème vous résolviez réellement: ils ne se soucient plus, car ils sont maintenant tellement intéressé à utiliser toutes ces fonctionnalités de langage exotique. Et à partir de là, vous pouvez vous occuper pendant des jours et des semaines à écrire du méta-code, puis à maintenir le méta-code, puis à écrire du méta-code pour gérer le méta-code.
Lundin

24
@Lundin: Eh bien, je ne comprends pas comment on peut se contenter d'un code fragile qui se cassera dès qu'un début continu / pause / retour sera introduit ou qu'une exception sera levée. D'un autre côté, cette solution est résiliente face à une maintenance future et ne repose que sur le fait que les destructeurs sont exécutés pendant le déroulement, ce qui est l'une des fonctionnalités les plus importantes de C ++. Qualifier cela d' exotique alors que c'est le principe sous-jacent qui alimente tous les conteneurs Standard est pour le moins amusant.
Matthieu M.

7
@Lundin: L'avantage du code de Matthieu est qu'il executeThisFunctionInAnyCase();s'exécutera même s'il foo();lève une exception. Lors de l'écriture de code sans exception, il est recommandé de placer toutes ces fonctions de nettoyage dans un destructeur.
Brian

6
@Brian Alors ne jetez aucune exception foo(). Et si vous le faites, attrapez-le. Problème résolu. Corrigez les bogues en les corrigeant, pas en écrivant des solutions.
Lundin

18
@Lundin: la Deferclasse est un petit morceau de code réutilisable qui vous permet d'effectuer n'importe quel nettoyage de fin de bloc, d'une manière sûre d'exception. il est plus communément appelé garde de portée . oui, toute utilisation d'un garde-portée peut être exprimée de manière plus manuelle, tout comme n'importe quelle forboucle peut être exprimée sous forme de bloc et de whileboucle, qui à son tour peut être exprimée avec ifet goto, qui peut être exprimée en langage assembleur si vous le souhaitez, ou pour ceux qui sont de vrais maîtres, en altérant les bits en mémoire via des rayons cosmiques dirigés par l'effet papillon de grognements courts et de chants spéciaux. mais pourquoi faire ça.
Bravo et hth. - Alf

34

Il y a une belle technique qui n'a pas besoin d'une fonction wrapper supplémentaire avec les instructions de retour (la méthode prescrite par Itjax). Il utilise une while(0)pseudo-boucle do . Le while (0)garantit qu'il ne s'agit en fait pas d'une boucle mais qu'il n'est exécuté qu'une seule fois. Cependant, la syntaxe de boucle permet l'utilisation de l'instruction break.

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

11
L'utilisation d'une fonction avec plusieurs retours est similaire, mais plus lisible, à mon avis.
Lundin

4
Oui, c'est certainement plus lisible ... mais d'un point de vue efficacité, la surcharge d'appel supplémentaire (passage et retour de paramètres) peut être évitée avec la construction do {} while (0).
Debasis

5
Vous êtes toujours libre de faire la fonction inline. Quoi qu'il en soit, c'est une bonne technique à connaître, car elle aide à plus que ce problème.
Ruslan

2
@Lundin Vous devez tenir compte de la localité du code, répandre du code sur trop d'endroits a aussi des problèmes.
API-Beast

3
D'après mon expérience, c'est un idiome très inhabituel. Il me faudrait un certain temps pour comprendre ce qui se passait, et c'est un mauvais signe lorsque je passe en revue le code. Étant donné qu'il ne semble pas avoir d'avantages par rapport aux autres approches, plus courantes et donc plus lisibles, je n'ai pas pu l'approuver.
Cody Gray

19

Vous pouvez également faire ceci:

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();

De cette façon, vous avez une taille de croissance linéaire minimale, +1 ligne par appel, et il est facilement maintenable.


EDIT : (Merci @Unda) Pas un grand fan parce que vous perdez la 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
Il s'agissait des appels de fonction accidentels à l'intérieur de push_back (), mais vous l'avez quand même corrigé :)
Quentin

4
J'aimerais savoir pourquoi cela a obtenu un downvote. En supposant que les étapes d'exécution sont en effet symétriques, comme elles semblent l'être, alors c'est propre et maintenable.
ClickRick

1
Bien que cela puisse sans doute sembler plus propre. Cela pourrait être plus difficile à comprendre pour les gens, et c'est certainement plus difficile à comprendre pour le compilateur!
Roy T.

1
Traiter davantage vos fonctions comme des données est souvent une bonne idée - une fois que vous l'avez fait, vous remarquerez également des remaniements ultérieurs. Encore mieux, chaque pièce que vous manipulez est une référence d'objet, pas seulement une référence de fonction - cela vous donnera encore plus de possibilités d'améliorer votre code sur toute la ligne.
Bill K

3
Légèrement sur-conçu pour un cas trivial, mais cette technique a définitivement une fonctionnalité intéressante que d'autres non: vous pouvez changer l'ordre d'exécution et [nombre de] les fonctions appelées à l'exécution, et c'est bien :)
Xocoatzin

18

Est-ce que cela fonctionnerait? Je pense que c'est équivalent à votre code.

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

3
J'appelle généralement la variable oklorsque j'utilise la même variable comme celle-ci.
Macke

1
Je serais très intéressé à savoir pourquoi les downvotes. Qu'est-ce qui ne va pas ici?
ABCplus

1
comparer votre réponse avec l'approche en court-circuit pour la complexité cyclomatique.
AlphaGoku

14

En supposant que le code souhaité est tel que je le vois actuellement:

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

Je dirais que l'approche correcte, en ce sens qu'elle est la plus simple à lire et la plus facile à maintenir, aurait moins de niveaux d'indentation, ce qui est (actuellement) l'objectif déclaré de la question.

// 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();

Cela évite tout besoin de gotos, d'exceptions, de whileboucles factices ou d'autres constructions difficiles et continue simplement le travail à portée de main.


Lorsque vous utilisez des boucles, il est généralement acceptable de les utiliser returnet breakde sauter hors de la boucle sans avoir besoin d'introduire des variables "drapeau" supplémentaires. Dans ce cas, l'utilisation d'un goto serait tout aussi innocente - rappelez-vous que vous échangez une complexité goto supplémentaire contre une complexité variable mutable supplémentaire.
hugomg

2
@hugomg Les variables étaient dans la question d'origine. Il n'y a pas de complexité supplémentaire ici. Des hypothèses ont été émises sur la question (par exemple, que les variables sont nécessaires dans le code expurgé), elles ont donc été conservées. S'ils ne sont pas nécessaires, le code peut être simplifié, mais étant donné la nature incomplète de la question, aucune autre hypothèse ne peut être formulée valablement.
ClickRick

Approche très utile, esp. pour une utilisation par un auto-déclaré newbie, il fournit une solution plus propre sans inconvénients. Je note également qu'il ne repose pas sur le fait d' stepsavoir la même signature ou même d'être des fonctions plutôt que des blocs. Je peux voir que cela est utilisé comme un refactorisateur de premier passage même lorsqu'une approche plus sophistiquée est valide.
Keith

12

La déclaration break pourrait-elle être utilisée d'une manière ou d'une autre?

Ce n'est peut-être pas la meilleure solution, mais vous pouvez mettre vos instructions en do .. while (0)boucle et utiliser des breakinstructions à la place de return.


2
Ce n'est pas moi qui l'ai rejeté, mais ce serait un abus d'une construction en boucle pour quelque chose dont l'effet est ce qui est actuellement souhaité mais entraînerait inévitablement de la douleur. Probablement pour le prochain développeur qui doit le maintenir dans 2 ans après que vous êtes passé à un autre projet.
ClickRick

3
@ClickRick utilise do .. while (0)pour les définitions de macro abusent également des boucles, mais il est considéré comme OK.
ouah

1
Peut-être, mais il existe des moyens plus propres d'y parvenir.
ClickRick

4
@ClickRick le seul but de ma réponse est de répondre La déclaration break pourrait être utilisée d'une certaine manière et la réponse est oui, les premiers mots de ma réponse suggèrent que ce n'est peut-être pas la solution à utiliser.
ouah

2
Cette réponse ne devrait être qu'un commentaire
msmucker0527

12

Vous pouvez mettre toutes les ifconditions, formatées comme vous le souhaitez dans une fonction qui leur est propre, le retour exécute la executeThisFunctionInAnyCase()fonction.

À partir de l'exemple de base de l'OP, les tests et l'exécution des conditions peuvent être séparés en tant que tels;

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

Et puis appelé comme tel;

InitialSteps();
executeThisFunctionInAnyCase();

Si des lambdas C ++ 11 sont disponibles (il n'y avait pas de balise C ++ 11 dans l'OP, mais ils peuvent toujours être une option), alors nous pouvons renoncer à la fonction séparée et envelopper cela dans un 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

Si vous n'aimez pas gotoet n'aimez pas les do { } while (0);boucles et que vous aimez utiliser C ++, vous pouvez également utiliser un lambda temporaire pour avoir le même effet.

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

executeThisFunctionInAnyCase();

1
if vous n'aimez pas goto && vous n'aimez pas faire {} tandis que (0) && vous aimez C ++ ... Désolé, vous n'avez pas pu résister, mais cette dernière condition échoue parce que la question est étiquetée c ainsi que c ++
ClickRick

@ClickRick c'est toujours difficile de plaire à tout le monde. À mon avis, il n'y a pas de C / C ++ que vous codez habituellement dans l'un ou l'autre et l'utilisation de l'autre est désapprouvée.
Alex

9

Les chaînes de IF / ELSE dans votre code ne sont pas le problème de langue, mais la conception de votre programme. Si vous êtes en mesure de reformuler ou de réécrire votre programme, je vous suggère de consulter Design Patterns ( http://sourcemaking.com/design_patterns ) pour trouver une meilleure solution.

Habituellement, lorsque vous voyez beaucoup de FI et autres dans votre code, c'est l'occasion d'implémenter le modèle de conception de stratégie ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) ou peut-être une combinaison d'autres modèles.

Je suis sûr qu'il existe des alternatives pour écrire une longue liste de if / else, mais je doute qu'ils changeront quoi que ce soit, sauf que la chaîne vous semblera jolie (Cependant, la beauté est dans l'œil du spectateur s'applique toujours au code aussi:-) ) . Vous devriez être préoccupé par des choses comme (dans 6 mois quand j'ai une nouvelle condition et que je ne me souviens de rien de ce code, pourrai-je l'ajouter facilement? Ou si la chaîne change, à quelle vitesse et sans erreur vais-je le mettre en œuvre)


9

Vous faites juste ça ..

coverConditions();
executeThisFunctionInAnyCase();

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

99 fois sur 100, c'est la seule façon de le faire.

N'essayez jamais, jamais, jamais de faire quelque chose de "délicat" dans le code informatique.


Au fait, je suis presque sûr que ce qui suit est la solution réelle que vous aviez en tête ...

L' instruction continue est essentielle dans la programmation algorithmique. (Autant que, l'instruction goto est critique dans la programmation algorithmique.)

Dans de nombreux langages de programmation, vous pouvez le faire:

-(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");
    }

(Remarquez tout d'abord: les blocs nus comme cet exemple sont une partie critique et importante de l'écriture d'un beau code, en particulier si vous avez affaire à une programmation "algorithmique".)

Encore une fois, c'est exactement ce que vous aviez dans votre tête, non? Et c'est la belle façon de l'écrire, donc vous avez un bon instinct.

Cependant, tragiquement, dans la version actuelle de objective-c (Mis à part - je ne sais pas pour Swift, désolé), il existe une fonctionnalité risible qui vérifie si le bloc englobant est une boucle.

entrez la description de l'image ici

Voici comment contourner cela ...

-(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");
    }

Alors n'oubliez pas que ..

faire {} while (false);

signifie simplement "faire ce bloc une fois".

c'est-à-dire qu'il n'y a absolument aucune différence entre écrire do{}while(false);et simplement écrire {}.

Cela fonctionne maintenant parfaitement comme vous le vouliez ... voici la sortie ...

entrez la description de l'image ici

Il est donc possible que vous voyiez l'algorithme dans votre tête. Vous devriez toujours essayer d'écrire ce que vous avez en tête. (Surtout si vous n'êtes pas sobre, car c'est à ce moment que la jolie sort! :))

Dans les projets "algorithmiques" où cela se produit souvent, dans l'objectif-c, nous avons toujours une macro comme ...

#define RUNONCE while(false)

... alors vous pouvez le faire ...

-(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");
    }

Il y a deux points:

a, même s'il est stupide que objective-c vérifie le type de bloc dans lequel se trouve une instruction continue, il est troublant de "combattre cela". C'est donc une décision difficile.

b, il y a la question si vous indentez, dans l'exemple, ce bloc? Je perds le sommeil sur des questions comme ça, donc je ne peux pas vous conseiller.

J'espère que ça aide.


Vous avez manqué la deuxième réponse la mieux votée .
Palec

Avez-vous mis la prime pour obtenir plus de représentants? :)
TMS

Au lieu de mettre tous ces commentaires dans le if, vous pouvez également utiliser des noms de fonction plus descriptifs et placer les commentaires dans les fonctions.
Thomas Ahle

Beurk. Je vais prendre la solution 1 ligne qui se lit de manière concise avec l'évaluation des courts-circuits (qui est dans les langues depuis plus de 20 ans et qui est bien connue) au cours de ce gâchis tous les jours. Je pense que nous pouvons tous les deux convenir que nous sommes heureux de ne pas travailler ensemble.
M2tM

8

Demandez à vos fonctions d'exécution de lever une exception si elles échouent au lieu de renvoyer false. Ensuite, votre code d'appel pourrait ressembler à ceci:

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

Bien sûr, je suppose que dans votre exemple d'origine, l'étape d'exécution ne retournerait que faux en cas d'erreur survenant à l'intérieur de l'étape?


3
utiliser l'exception pour contrôler le flux est souvent considéré comme une mauvaise pratique et un code malodorant
user902383

8

Beaucoup de bonnes réponses déjà, mais la plupart d'entre elles semblent faire des compromis sur une partie (certes très peu) de la flexibilité. Une approche courante qui ne nécessite pas ce compromis consiste à ajouter une variable d' état / continue . Le prix est, bien sûr, une valeur supplémentaire pour suivre:

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;

Pourquoi ok &= conditionX;et pas simplement ok = conditionX;?
ABCplus

@ user3253359 Dans de nombreux cas, oui, vous pouvez le faire. Ceci est une démo conceptuelle; dans le code de travail, nous essayons de le simplifier autant que possible
blgt

+1 L'une des rares réponses propres et maintenables qui fonctionne en c , comme stipulé dans la question.
ClickRick

6

En C ++ (la question est balisée à la fois C et C ++), si vous ne pouvez pas modifier les fonctions pour utiliser des exceptions, vous pouvez toujours utiliser le mécanisme d'exception si vous écrivez une petite fonction d'aide comme

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

Ensuite, votre code pourrait se lire comme suit:

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

executeThisFunctionInAnyCase();

Si vous êtes dans une syntaxe sophistiquée, vous pouvez le faire fonctionner via un cast explicite:

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

Ensuite, vous pouvez écrire votre code comme

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

executeThisFunctionInAnyCase();

Refactoriser les contrôles de valeur dans les exceptions n'est pas nécessairement une bonne façon de procéder, il y a des exceptions de déroulement excessives.
John Wu

4
-1 L'utilisation de exceptions pour le flux normal en C ++ comme ceci est une mauvaise pratique de programmation. En C ++, les exceptions doivent être réservées à des circonstances exceptionnelles.
Jack Aidley

1
D'après le texte de la question (souligné par moi): "Les fonctions executeStepX doivent être exécutées si et seulement si la précédente réussit. " En d'autres termes, la valeur de retour est utilisée pour indiquer l'échec. Autrement dit, il s'agit de la gestion des erreurs (et on espère que les échecs sont exceptionnels). La gestion des erreurs est exactement pour laquelle les exceptions ont été inventées.
celtschk

1
Nan. Premièrement, des exceptions ont été créées pour permettre la propagation des erreurs et non la gestion des erreurs ; deuxièmement, "Les fonctions executeStepX doivent être exécutées si et seulement si les précédentes réussissent." ne signifie pas que le booléen false retourné par la fonction précédente indique un cas manifestement exceptionnel / erroné. Votre déclaration n'est donc pas séquentielle . La gestion des erreurs et la désinfection des flux peuvent être mises en œuvre par de nombreux moyens différents, les exceptions sont un outil permettant la propagation des erreurs et la gestion des erreurs hors de lieu , et excellent dans ce domaine.

6

Si votre code est aussi simple que votre exemple et que votre langue prend en charge les évaluations de court-circuit, vous pouvez essayer ceci:

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

Si vous passez des arguments à vos fonctions et récupérez d'autres résultats afin que votre code ne puisse pas être écrit de la manière précédente, la plupart des autres réponses seraient mieux adaptées au problème.


En fait, j'ai édité ma question pour mieux expliquer le sujet, mais elle a été rejetée pour ne pas invalider la plupart des réponses. : \
ABCplus

Je suis un nouvel utilisateur de SO et un programmeur débutant. 2 questions alors: y a-t-il un risque qu'une autre question comme celle que vous avez dite soit marquée comme dupliquée à cause de CETTE question? Un autre point est: comment un utilisateur / programmeur novice SO pourrait-il choisir la meilleure réponse entre tous (presque bon je suppose ..)?
ABCplus

6

Pour C ++ 11 et au-delà, une bonne approche pourrait être d'implémenter un système de sortie de portée similaire au mécanisme de portée (sortie) de D.

Une façon possible de l'implémenter est d'utiliser des lambdas C ++ 11 et des macros d'assistance:

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 })

Cela vous permettra de revenir tôt à partir de la fonction et de vous assurer que le code de nettoyage que vous définissez est toujours exécuté à la sortie de la portée:

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

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Les macros ne sont vraiment que de la décoration. MakeScopeExit()peut être utilisé directement.


Il n'y a pas besoin de macros pour que cela fonctionne. Et [=]est généralement mauvais pour un lambda de portée.
Yakk - Adam Nevraumont

Oui, les macros sont juste pour la décoration et peuvent être jetées. Mais ne diriez-vous pas que la capture par valeur est l'approche "générique" la plus sûre?
glampert

1
non: si votre lambda ne persistera pas au-delà de la portée actuelle où le lambda est créé, utilisez [&]: il est sûr et peu surprenant. Capture par valeur uniquement lorsque la lambda (ou les copies) pourrait survivre plus longtemps que la portée au point de déclaration ...
Yakk - Adam Nevraumont

Oui, cela a du sens. Je vais le changer. Merci!
glampert

6

Pourquoi personne ne donne la solution la plus simple? :RÉ

Si toutes vos fonctions ont la même signature, vous pouvez le faire de cette façon (pour le langage C):

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

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

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Pour une solution C ++ propre, vous devez créer une classe d'interface qui contient une méthode d' exécution et encapsule vos étapes dans des objets.
Ensuite, la solution ci-dessus ressemblera à ceci:

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

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

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

executeThisFunctionInAnyCase();

5

En supposant que vous n'avez pas besoin de variables de condition individuelles, inverser les tests et utiliser le else-falthrough comme chemin "ok" vous permettrait d'obtenir un ensemble plus vertical d'instructions 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'omission de la variable échouée rend le code un peu trop obscur IMO.

Déclarer les variables à l'intérieur est très bien, ne vous inquiétez pas de = 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();

C'est obscur, mais compact:

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

runThisFunctionInAnyCase();

5

Cela ressemble à une machine à états, ce qui est pratique car vous pouvez facilement l'implémenter avec un modèle d'état .

En Java, cela ressemblerait à ceci:

interface StepState{
public StepState performStep();
}

Une implémentation fonctionnerait comme suit:

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

Etc. Ensuite, vous pouvez remplacer la grande condition if par:

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

5

Comme Rommik l'a mentionné, vous pouvez appliquer un modèle de conception pour cela, mais j'utiliserais le modèle Décorateur plutôt que la stratégie puisque vous souhaitez enchaîner les appels. Si le code est simple, j'irais avec l'une des réponses bien structurées pour empêcher l'imbrication. Cependant, s'il est complexe ou nécessite un chaînage dynamique, le motif Décorateur est un bon choix. Voici un diagramme de classe yUML :

diagramme de classe yUML

Voici un exemple de programme 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;
    }
}

Le meilleur livre à lire sur les modèles de conception, à mon humble avis, est Head First Design Patterns .


Quel est l'avantage de cela sur quelque chose comme la réponse de Jefffrey?
Dason

beaucoup plus résistant au changement, lorsque les exigences changent, cette approche est plus simple à gérer sans beaucoup de connaissances du domaine. Surtout lorsque vous considérez la profondeur et la longueur de certaines sections d'if imbriquées. Tout cela peut devenir très fragile et donc à haut risque de travailler avec. Ne vous méprenez pas, certains scénarios d'optimisation peuvent vous amener à déchirer cela et à revenir aux ifs, mais dans 99% des cas, cela ne pose aucun problème. Mais le fait est que lorsque vous atteignez ce niveau, vous ne vous souciez pas de la maintenabilité, vous avez besoin de la performance.
John Nicholas

4

Plusieurs réponses ont fait allusion à un modèle que j'ai vu et utilisé plusieurs fois, en particulier dans la programmation réseau. Dans les piles de réseau, il y a souvent une longue séquence de demandes, dont chacune peut échouer et arrêter le processus.

Le modèle commun était d'utiliser do { } while (false);

J'ai utilisé une macro pour le while(false)faire do { } once;Le motif commun était:

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

Ce modèle était relativement facile à lire, et permettait d'utiliser des objets qui seraient correctement détruits et évitait également les retours multiples, ce qui rendait le pas et le débogage un peu plus faciles.


4

Pour améliorer la réponse de Mathieu C ++ 11 et éviter les coûts d'exécution engendrés par l'utilisation de std::function, je suggère d'utiliser les éléments suivants

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));
}

Cette classe de modèle simple acceptera n'importe quel foncteur qui peut être appelé sans aucun paramètre, et cela sans aucune allocation de mémoire dynamique et se conforme donc mieux à l'objectif d'abstraction de C ++ sans surcharge inutile. Le modèle de fonction supplémentaire est là pour simplifier l'utilisation par déduction de paramètres de modèle (qui n'est pas disponible pour les paramètres de modèle de classe)

Exemple d'utilisation:

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

Tout comme la réponse de Mathieu, cette solution est totalement sûre et executeThisFunctionInAnyCasesera appelée dans tous les cas. Si eux- executeThisFunctionInAnyCasemêmes lancent, les destructeurs sont marqués implicitement noexceptet par conséquent, un appel à std::terminateserait émis au lieu de provoquer une exception lors du déroulement de la pile.


+1 Je cherchais cette réponse pour ne pas avoir à la poster. Vous devez vous perfectionner en avant functordans deferredle constructeur, pas besoin de forcer a move.
Yakk - Adam Nevraumont

@Yakk a changé le constructeur en constructeur de transfert
Joe

3

Il semble que vous souhaitiez passer tous vos appels à partir d'un seul bloc. Comme d'autres l'ont proposé, vous devez utiliser soit une whileboucle et quitter en utilisant breaksoit une nouvelle fonction avec laquelle vous pouvez quitter return(peut être plus propre).

Personnellement goto, je bannis , même pour la sortie de fonction. Ils sont plus difficiles à repérer lors du débogage.

Une alternative élégante qui devrait fonctionner pour votre flux de travail consiste à créer un tableau de fonctions et à itérer sur celui-ci.

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();

Heureusement, le débogueur les repère pour vous. Si vous déboguez et non pas à travers le code, vous le faites mal.
Cody Gray

Je ne comprends pas ce que tu veux dire, ni pourquoi je ne pouvais pas utiliser le pas à pas?
AxFab

3

Parce que vous avez également [... un bloc de code ...] entre les exécutions, je suppose que vous avez une allocation de mémoire ou des initialisations d'objet. De cette façon, vous devez vous soucier de nettoyer tout ce que vous avez déjà initialisé à la sortie, et également le nettoyer si vous rencontrez un problème et que l'une des fonctions retourne faux.

Dans ce cas, le mieux que j'ai eu dans mon expérience (quand je travaillais avec CryptoAPI) était de créer de petites classes, dans le constructeur vous initialisez vos données, dans le destructeur vous les désinitialisez. Chaque classe de fonction suivante doit être enfant de la classe de fonction précédente. Si quelque chose a mal tourné, jetez l'exception.

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;
};

Et puis dans votre code il vous suffit d'appeler:

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();

Je suppose que c'est la meilleure solution si chaque appel de ConditionX initialise quelque chose, alloue de la mémoire, etc. Mieux vaut être sûr que tout sera nettoyé.


3

une façon intéressante est de travailler avec des exceptions.

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

Si vous écrivez un tel code, vous allez en quelque sorte dans la mauvaise direction. Je ne le verrai pas comme "le problème" d'avoir un tel code, mais d'avoir une "architecture" aussi désordonnée.

Astuce: discutez de ces cas avec un développeur chevronné en qui vous avez confiance ;-)


Je pense que cette idée ne peut pas remplacer chaque chaîne if. Quoi qu'il en soit, dans de nombreux cas, c'est une très bonne approche!
WoIIe

3

Une autre approche - do - whileboucle, même si elle a été mentionnée auparavant, il n'y avait aucun exemple qui montrerait à quoi elle ressemble:

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

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

executeThisFunctionInAnyCase();

(Eh bien, il y a déjà une réponse avec whileboucle mais la do - whileboucle ne vérifie pas de manière redondante la vérité (au début) mais à la fin xD (cela peut être ignoré, cependant)).


Hey Zaffy - cette réponse a une explication massive de l'approche do {} while (false). stackoverflow.com/a/24588605/294884 Deux autres réponses l'ont également mentionné.
Fattie

C'est une question fascinante de savoir s'il est plus élégant d'utiliser CONTINUE ou BREAK dans cette situation!
Fattie

Hé @JoeBlow, j'ai vu toutes les réponses ... je voulais juste montrer au lieu d'en parler :)
Zaffy

Ma première réponse ici, j'ai dit "Personne n'a mentionné CECI ..." et immédiatement quelqu'un a gentiment souligné que c'était la 2ème réponse du haut :)
Fattie

@JoeBlow Eh, vous avez raison. Je vais essayer de régler ça. J'ai l'impression que ... xD Merci quand même, la prochaine fois je paierai un peu plus d'
attention
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.