277 votes

Comment éviter les chaînes "si"?

En supposant que j'ai ce pseudo-code:

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

executeThisFunctionInAnyCase();

Fonctions executeStepX doit être exécutée si et seulement si la précédente réussir. Dans tous les cas, l' executeThisFunctionInAnyCase fonction doit être appelée à la fin. Je suis un débutant dans la programmation, donc désolé pour la question très simple: est-il un moyen (en C/C++ par exemple) pour éviter que de longs if chaîne de production de ce genre de "pyramide de code", au détriment du code de la lisibilité?

Je sais que si l'on pouvait ignorer l' executeThisFunctionInAnyCase appel de fonction, le code peut être simplifié comme:

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

Mais la contrainte est l' executeThisFunctionInAnyCase appel de fonction. Pourrait l' break de la déclaration d'une certaine façon?

494voto

Jefffrey Points 31698

Vous pouvez utiliser un && (logique ET):

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

cela répondra à vos deux exigences:

  • executeStep<X>() devrait seulement évaluer si le précédent a réussi (ceci s'appelle l' évaluation de court-circuit )
  • executeThisFunctionInAnyCase() sera exécuté dans tous les cas

360voto

ltjax Points 11115

Utilisez simplement une fonction supplémentaire pour que votre deuxième version fonctionne:

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

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

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

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

Utiliser des ifs profondément imbriqués (votre première variante) ou le désir de sortir d'une «partie d'une fonction» signifie généralement que vous avez besoin d'une fonction supplémentaire.

165voto

cmaster Points 7460

Ancien de l'école C, les programmeurs utilisent goto dans ce cas. C'est celui de l'utilisation de goto qui est en fait encouragé par la Linux styleguide, il appelle la fonction centralisée de sortie:

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 travaillent autour de l'aide d' goto en enveloppant le corps dans une boucle et de rupture, mais effectivement les deux approches font la même chose. L' goto approche est préférable si vous avez besoin d'un nettoyage uniquement si executeStepA() a été réussie:

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 la boucle approche vous vous retrouvez avec deux niveaux de boucles dans ce cas.

135voto

John Wu Points 2633

C'est une situation commune et il y a plusieurs façons de traiter avec elle. Voici ma tentative de réponse canonique. S'il vous plaît commentaire si j'ai oublié quelque chose et je vais garder ce post à jour.

C'est une Flèche

Le sujet est connu comme la flèche anti-modèle. Il est appelé une flèche parce que la chaîne de imbriquée ifs forme de blocs de code qui s'élargissent de plus en plus loin vers la droite puis vers la gauche, formant un visuel flèche qui "points" sur le côté droit de l'éditeur de code volet.

Aplatir la Flèche avec la Garde

Quelques moyens communs d'éviter la Flèche sont discutés ici. La méthode la plus courante consiste à utiliser un garde - modèle, dans lequel le code gère l'exception des flux de première et gère le flux de base, par exemple, au lieu de

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

... que vous souhaitez utiliser....

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

Quand il y a une longue série de gardes cette aplatit le code considérablement comme tous les gardes apparaissent tout le chemin à gauche et à votre ifs ne sont pas imbriqués. En outre, vous êtes visuellement jumelage de la condition logique avec son associé de l'erreur, ce qui le rend beaucoup plus facile de dire ce qui se passe:

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

Ce qui est objectivement et quantitativement la plus facile à lire parce que

  1. Les caractères { et } pour une logique de bloc sont proches
  2. Le montant de mental contexte nécessaire pour comprendre une ligne particulière est plus petit
  3. L'intégralité de la logique associée à une condition if est plus susceptible d'être sur une page
  4. La nécessité pour le codeur pour faire défiler la page/l'œil de la piste est grandement diminuée

Comment ajouter le code commun à la fin

Le problème avec la garde de modèle est qu'il s'appuie sur ce qui est appelé "opportunistes retour" ou "opportunistes de sortie." En d'autres termes, il brise le modèle que chaque fonction doit avoir exactement un point de sortie. C'est un problème pour deux raisons:

  1. Elle se frotte à certaines personnes dans le mauvais sens, par exemple, des gens qui ont appris à code sur Pascal ont appris qu'une fonction = un point de sortie.
  2. Il ne fournit pas une section de code qui s'exécute à la sortie de n'importe quoi, qui est le sujet à portée de main.

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

L'Option 1. Vous ne pouvez pas le faire: utiliser l' finally

Malheureusement, en tant que développeur c++, vous ne pouvez pas faire cela. Mais c'est le nombre une seule réponse pour les langues qui contiennent un mot-clé finally, car c'est exactement ce qu'il est pour.

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

L'Option 2. Éviter le problème: Restructurer vos fonctions

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

Voici un exemple:

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

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

L'Option 3. Langue astuce: Utiliser une fausse boucle

Un autre truc commun, je vois est d'utiliser while(true) et l'introduction, comme indiqué dans les autres réponses.

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

Alors que c'est moins "honnête" que l'utilisation d' goto, il est moins enclin à être foiré lors d'un refactoring, car il marque clairement les limites de la logique portée. Un naïf codeur qui couper et coller vos étiquettes ou votre goto instructions peut entraîner de graves problèmes! (Et franchement, le modèle est si commun aujourd'hui, je pense qu'il communique clairement l'intention, et n'est donc pas "malhonnêtes").

Il existe d'autres variantes de ces options. Par exemple, on pourrait utiliser switch au lieu de while. Une construction du langage avec un break mot-clé serait probablement travailler.

L'Option 4. L'effet de levier de l'objet du cycle de vie

Une autre approche utilise l'objet du cycle de vie. Utiliser un objet de contexte de transporter vos paramètres (quelque chose qui notre naïf exemple étrangement manque) et de s'en débarrasser 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 comprendre l'objet du cycle de vie de la langue de votre choix. Vous avez besoin d'une sorte de déterministe de collecte des ordures pour que cela fonctionne, vous devez savoir quand le destructeur sera appelé. Dans certaines langues, vous devez utiliser Dispose au lieu d'un destructeur.

Option 4.1. L'effet de levier de l'objet du cycle de vie (wrapper modèle)

Si vous allez utiliser une approche orientée-objet, peut aussi bien le faire à droite. Cette option utilise une classe pour "encapsuler" les ressources qui nécessite de nettoyage, ainsi que de 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 comprendre l'objet de votre cycle de vie.

Option 5. Langue astuce: Utilisez évaluation de court-circuit

Une autre technique est de prendre avantage de l' évaluation de court-circuit.

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

Cette solution tire parti de la façon dont l' && opérateur fonctionne. Lorsque le côté gauche de && false, la droite n'est jamais évaluée.

Cette astuce est particulièrement utile lorsque compact code est nécessaire et lorsque le code n'est pas susceptible de voir beaucoup d'entretien, de l'e.g vous êtes à la mise en œuvre d'un algorithme bien connu. Pour plus générales de codage de la structure de ce code est trop fragile, même un changement mineur à la logique pourrait déclencher une réécriture totale.

61voto

Faites juste

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

C'est si simple.


En raison de trois modifications, chacune d'elles a fondamentalement changé la question (quatre si l'on compte la révision à la version n ° 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();
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X