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 executeStepX
doivent être exécutées si et seulement si la précédente réussit. Dans tous les cas, la fonction executeThisFunctionInAnyCase
devrait être appelée à la fin. Je suis un débutant en programmation, désolé pour la question très fondamentale: existe-t-il un moyen (en C/C++ par exemple) d’éviter cette longue chaîne if
produisant ce genre de "pyramide de code", à la frais de lisibilité du code?
Je sais que si nous pouvions ignorer l'appel de fonction executeThisFunctionInAnyCase
, le code pourrait être simplifié de la manière suivante:
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
Mais la contrainte est l'appel de fonction executeThisFunctionInAnyCase
. L'instruction break
peut-elle être utilisée d'une manière ou d'une autre?
Vous pouvez utiliser un &&
(logique AND):
if (executeStepA() && executeStepB() && executeStepC()){
...
}
executeThisFunctionInAnyCase();
cela satisfera à vos deux besoins:
executeStep<X>()
devrait n'évaluer que si le précédent a réussi (on appelle cela l'évaluation du court-circuit )executeThisFunctionInAnyCase()
sera exécuté dans tous les casUtilisez 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();
}
L'utilisation de ifs 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.
Les programmeurs C de la vieille école utilisent goto
dans ce cas. C’est l’utilisation unique de goto
qui est encouragée par le guide de style Linux, elle est appelée la sortie centralisée de la fonction:
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 goto
en enroulant le corps dans une boucle puis en se séparant, mais les deux approches font la même chose. L'approche goto
est préférable si vous avez besoin d'un autre nettoyage uniquement si 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 obtiendrez deux niveaux de boucle dans ce cas.
Il s’agit d’une situation courante et il existe de nombreux moyens d’y remédier. Voici ma tentative de réponse canonique. Veuillez commenter si quelque chose me manque et je garderai ce message à jour.
Ce que vous discutez est connu sous le nom de anti-motif de flèche . C'est ce qu'on appelle une flèche parce que la chaîne de ifs imbriqués forme des blocs de code qui s'étendent de plus en plus à droite, puis de nouveau à gauche, formant une flèche visuelle qui "pointe" sur le côté droit du volet de l'éditeur de code.
Quelques manières courantes d'éviter la flèche sont discutées ici . La méthode la plus courante consiste à utiliser un modèle guard , dans lequel le code gère d'abord les flux d'exception, puis 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
Quand il y a une longue série de gardes, cela aplatit considérablement le code car tous les gardes apparaissent complètement à gauche et vos ifs ne sont pas imbriqués. De plus, vous associez visuellement la condition logique à l'erreur associée, ce qui facilite grandement la lecture de 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;
}
C’est objectivement et quantitativement plus facile à lire car
Le problème avec le modèle de garde est qu’il repose sur ce que l’on appelle le "retour opportuniste" ou "sortie opportuniste". En d’autres termes, cela brise le schéma selon lequel chaque fonction doit avoir exactement un point de sortie. Ceci est un problème pour deux raisons:
Ci-dessous, j'ai fourni quelques options pour contourner cette limitation, en utilisant des fonctionnalités de langage ou en évitant le problème.
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, puisque c’est exactement ce à quoi il est destiné.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
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 peut en outre réduire complexité cyclomatique , ce qui est un moyen éprouvé de réduire votre taux de défauts et d'améliorer la spécificité de tout test unitaire automatisé.
Voici un exemple:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Une autre astuce courante 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 d'utiliser goto
, il est moins sujet aux erreurs lors de la refactorisation, car il définit clairement les limites de la portée logique. Un codeur naïf qui coupe et colle vos étiquettes ou vos déclarations goto
peut causer de gros problèmes! (Et franchement, le schéma est si courant maintenant, je pense qu’il communique clairement l’intention et n’est donc pas "malhonnête" du tout).
Il existe d'autres variantes de cette option. Par exemple, on pourrait utiliser switch
au lieu de while
. Toute construction de langage avec un mot clé break
fonctionnerait probablement.
Une autre approche exploite le cycle de vie de l'objet. Utilisez un objet de contexte pour transporter vos paramètres (quelque chose qui manque étrangement à notre exemple naïf) et supprimez-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 récupération de place déterministe pour que cela fonctionne, c’est-à-dire que vous devez savoir quand le destructeur sera appelé. Dans certaines langues, vous devrez utiliser Dispose
à la place d'un destructeur.
Si vous utilisez une approche orientée objet, vous pouvez également le faire correctement. Cette option utilise une classe pour "encapsuler" les ressources nécessitant 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.
Une autre technique consiste à tirer parti de évaluation de court-circuit .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Cette solution tire parti du fonctionnement de l’opérateur &&. Lorsque le côté gauche de && est évalué à false, le côté droit n'est jamais évalué.
Cette astuce est particulièrement utile lorsque du code compact est requis et que le code ne nécessitera pas 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 un changement mineur de la logique peut déclencher une réécriture totale.
Il suffit de faire
if( executeStepA() && executeStepB() && executeStepC() )
{
// ...
}
executeThisFunctionInAnyCase();
C'est si simple.
En raison de trois modifications que chacune a fondamentalement modifiée la question (quatre si l'on compte la révision à 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();
Il existe en réalité un moyen de différer des actions en C++: utiliser le destructeur d'un objet.
En supposant que vous ayez 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
Il existe une technique de Nice 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 pseudo-boucle do while(0)
. La while (0)
s'assure qu'il ne s'agit pas d'une boucle mais d'une seule exécution. Cependant, la syntaxe de la boucle autorise l'utilisation de l'instruction break.
void foo()
{
// ...
do {
if (!executeStepA())
break;
if (!executeStepB())
break;
if (!executeStepC())
break;
}
while (0);
// ...
}
Vous pouvez aussi 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) Ce n'est pas un grand fan parce que vous perdez en 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();
Cela fonctionnerait-il? Je pense que cela est équivalent avec votre code.
bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
En supposant que le code souhaité soit 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 la bonne approche, en ce sens qu’elle est la plus simple à lire et à maintenir, comporterait moins d’indentations, ce qui est (actuellement) le but 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 toute nécessité de goto
s, exceptions, boucles factices while
ou autres constructions difficiles et continue simplement avec le travail simple à accomplir.
Vous pouvez mettre toutes les conditions if
, formatées à votre guise dans une fonction qui leur est propre, au retour, exécuter la fonction executeThisFunctionInAnyCase()
.
À partir de l'exemple de base dans l'OP, le test et l'exécution de la condition 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 ensuite 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), nous pouvons alors renoncer à la fonction séparée et la résumer 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();
L'instruction break peut-elle être utilisée d'une manière ou d'une autre?
Ce n’est peut-être pas la meilleure solution, mais vous pouvez placer vos instructions dans une boucle do .. while (0)
et utiliser les instructions break
au lieu de return
.
Si vous n'aimez pas goto
et n'aimez pas do { } while (0);
boucles et 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();
Tu fais 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.
Jamais, jamais, jamais essayer de faire quelque chose de "délicat" en code informatique.
En passant, je suis à peu près sûr que ce qui suit est le la solution que vous aviez à l’esprit ...
L'instruction continue est critique en programmation algorithmique. (Dans la plupart des cas, l'instruction goto est essentielle en programmation algorithmique.)
Dans de nombreux langages de programmation, vous pouvez faire ceci:
-(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");
}
(Notez tout d’abord: les blocs nus comme ceux-ci sont une partie essentielle de l’écriture de beaux codes, en particulier s’il s’agit de programmation "algorithmique".)
Encore une fois, c'est exactement ce que vous aviez dans la tête, non? Et c'est la belle façon de l'écrire, vous avez donc un bon instinct.
Cependant, tragiquement, dans le version actuelle de objective-c (à part - je ne sais pas pour Swift, désolé), il existe une fonction risible qui permet de vérifier si le bloc englobant est une boucle.
-(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'oublie pas ça ..
signifie simplement "faire ce bloc une fois".
c'est-à-dire qu'il n'y a aucune différence entre l'écriture do{}while(false);
et la simple écriture {}
.
Cela fonctionne maintenant parfaitement comme vous le souhaitiez ... voici la sortie ...
Il est donc possible que vous voyiez cet algorithme dans votre tête. Vous devriez toujours essayer d'écrire ce que vous avez dans la tête. (Surtout si vous n'êtes pas sobre, car c'est là que la jolie sort! :))
Dans les projets "algorithmiques" où cela se produit souvent, dans objective-c, nous avons toujours une macro comme ...
#define RUNONCE while(false)
... alors tu peux faire ça ...
-(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:
même s’il est stupide que objective-c vérifie le type de blocage d’une déclaration continue, il est troublant de "combattre cela". C'est donc une décision difficile.
b, il y a la question si vous indenter, dans l'exemple, ce bloc? Je perds mon sommeil à cause de questions comme celle-là, alors je ne peux pas vous conseiller.
J'espère que ça aide.
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 capable de re-factoriser ou de réécrire votre programme, je vous suggère de regarder dans Design Patterns ( http://sourcemaking.com/design_patterns ) pour trouver une meilleure solution. .
Habituellement, lorsque vous voyez beaucoup de IF & else 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'elles puissent changer quoi que ce soit si ce n'est que la chaîne vous semblera jolie (Cependant, la beauté est dans l'oeil du spectateur, ça s'applique toujours au code aussi:-) ) . Vous devriez être préoccupé par des choses telles que (dans 6 mois, quand je serai dans une nouvelle condition et que je ne me souviendrai de rien de ce code, pourrai-je l'ajouter facilement? Ou si la chaîne change, avec quelle rapidité et sans erreur vais-je être le mettre en œuvre)
Beaucoup de bonnes réponses déjà, mais la plupart d’entre elles semblent se tromper sur certaines (certes très peu) de la flexibilité. Une approche commune qui n'exige pas ce compromis consiste à ajouter une variable status/keep-going. Le prix est bien sûr une valeur supplémentaire à garder en mémoire:
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;
Demandez à vos fonctions d'exécution de générer 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 initial, l'étape d'exécution ne renverrait que faux dans le cas d'une erreur survenant à l'intérieur de l'étape?
En C++ (la question est balisée en 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'assistance telle que
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 la syntaxe sophistiquée, vous pouvez le faire fonctionner via une distribution explicite:
struct function_failed {};
struct attempt
{
attempt(bool retval)
{
if (!retval)
throw function_failed();
}
};
Ensuite, vous pouvez écrire votre code en tant que
try
{
(attempt) executeStepA();
(attempt) executeStepB();
(attempt) executeStepC();
}
catch (function_failed)
{
// -- this block intentionally left empty --
}
executeThisFunctionInAnyCase();
Pour C++ 11 et au-delà, une approche de Nice pourrait consister à implémenter un système exit de la portée similaire à le mécanisme de la portée de D (exit) .
Une façon possible de le mettre en œuvre consiste à utiliser des lambdas C++ 11 et des macros auxiliaires:
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 rapidement 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.
Si votre code est aussi simple que votre exemple et que votre langage prend en charge les évaluations de court-circuit, vous pouvez essayer ceci:
StepA() && StepB() && StepC() && StepD();
DoAlways();
Si vous transmettez 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, de nombreuses autres réponses seraient mieux adaptées au problème.
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 contenant une méthode execute et encapsulant 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();
En supposant que vous n'ayez pas besoin de variables de condition individuelles, inverser les tests et utiliser l'option 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();
Omettre la variable en échec rend le code un peu trop obscur à l’OMI.
Déclarer les variables internes est correct, ne vous inquiétez pas à propos 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();
Cela ressemble à une machine à états, ce qui est pratique car vous pouvez facilement l'implémenter avec un state-pattern .
Dans 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 condition big if par:
Step toDo = new StepA();
while(toDo != null)
toDo = toDo.performStep();
executeThisFunctionInAnyCase();
Comme Rommik l'a mentionné, vous pouvez appliquer un modèle de conception à cela, mais j'utiliserais plutôt le modèle Décorateur que Stratégie, car vous souhaitez enchaîner les appels. Si le code est simple, je choisirais alors 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 :
Voici un exemple LinqPad Programme 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, IMHO, est Head First Design Patterns .
Plusieurs réponses ont fait allusion à un modèle que j'ai souvent vu et utilisé, notamment dans la programmation réseau. Dans les piles de réseau, il existe souvent une longue séquence de demandes, chacune d’elles pouvant é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)
pour le rendre do { } once;
Le modèle 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 l’utilisation d’objets destructifs, mais évitait également les retours multiples, ce qui simplifiait un peu le pas et le débogage.
Pour améliorer la réponse de Mathieu à C++ 11 et éviter les coûts d'exécution liés à 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èles simple acceptera n'importe quel foncteur pouvant être appelé sans paramètre, sans allocation de mémoire dynamique et se conformant ainsi mieux à l'objectif d'abstraction de C++ sans surcharge inutile. Le modèle de fonction supplémentaire est là pour simplifier l'utilisation de la déduction de paramètre 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 executeThisFunctionInAnyCase
sera appelé dans tous les cas. Si executeThisFunctionInAnyCase
est lui-même lancé, les destructeurs sont marqués implicitement noexcept
et, par conséquent, un appel à std::terminate
sera émis au lieu de provoquer une exception lors du déroulement de la pile.
Une autre approche - la boucle do - while
, bien que cela ait été mentionné auparavant, il n’y avait pas d’exemple qui montrerait à quoi cela 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 la boucle while
mais la boucle do - while
ne vérifie pas de manière redondante la valeur true (au début) mais à la fin xD (ceci peut être ignoré, cependant)).
Il semble que vous souhaitiez faire tout votre appel à partir d'un seul bloc. Comme d'autres l'ont proposé, vous devriez utiliser soit une boucle while
et le laisser avec break
, soit une nouvelle fonction que vous pouvez laisser avec return
(peut être plus propre).
Personnellement, je bannis goto
, même pour quitter la 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 à l'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();
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 dans la mauvaise direction. Je ne le verrai pas comme "le problème" d'avoir un tel code, mais d'avoir une "architecture" aussi compliquée.
Astuce: discutez de ces cas avec un développeur expérimenté en qui vous avez confiance ;-)
Une solution alternative consisterait à définir un idiome par le biais de macro-hacks.
#define block for(int block = 0; !block; block++)
Maintenant, un "bloc" peut être quitté avec break
, de la même manière que les boucles for(;;)
et while()
. Exemple:
int main(void) {
block {
if (conditionA) {
// Do stuff A...
break;
}
if (conditionB) {
// Do stuff B...
break;
}
if (conditionC) {
// Do stuff C...
break;
}
else {
// Do default stuff...
}
} /* End of "block" statement */
/* ---> The "break" sentences jump here */
return 0;
}
En dépit de la construction "pour (;;)", l'instruction "block" est exécutée une seule fois.
Ces "blocs" peuvent être sortis avec break
phrases.
Ainsi, les chaînes de if else if else if...
phrases sont évitées.
Au plus, une dernière else
peut être suspendue à la fin du "bloc" pour traiter les cas "par défaut".
Cette technique a pour but d’éviter la méthode typique et laide do { ... } while(0)
.
Dans la macro block
est définie une variable également nommée block
définie de manière à exécuter exactement 1 itération de for. Selon les règles de substitution pour les macros, l'identifiant block
de la définition de la macro block
n'est pas remplacé de manière récursive. Par conséquent, block
devient un identificateur inaccessible au programmeur, mais fonctionne bien en interne pour control de "hidden" for(;;)
loop.
De plus: ces "blocs" peuvent être imbriqués, car la variable cachée int block
aurait différentes portées.
Juste une note de côté; quand une étendue if
provoque toujours un return
(ou une rupture dans une boucle), n'utilisez pas d'instruction else
. Cela peut vous épargner beaucoup d'indentation.
Parce que vous avez également [... 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 si l'une des fonctions retournera false.
Dans ce cas, le meilleur de ce que j'avais dans mon expérience (quand je travaillais avec CryptoAPI) était de créer de petites classes, en constructeur vous initialisez vos données, en destructeur vous les désinitialisez. Chaque classe de fonction suivante doit être enfant de la classe de fonction précédente. Si quelque chose ne va pas - jeter 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é.
Une solution simple consiste à utiliser une variable booléenne conditionnelle. Cette dernière peut être réutilisée encore et encore afin de vérifier tous les résultats des étapes en cours d'exécution dans l'ordre:
bool cond = executeStepA();
if(cond) cond = executeStepB();
if(cond) cond = executeStepC();
if(cond) cond = executeStepD();
executeThisFunctionInAnyCase();
Ce n’est pas qu’il n’était pas nécessaire de le faire au préalable: bool cond = true;
... puis suivi de if (cond) cond = executeStepA (); La variable cond
peut être immédiatement affectée au résultat de executeStepA()
, rendant ainsi le code encore plus court et plus simple à lire.
Une autre approche plus particulière mais amusante serait la suivante (certains pourraient penser que c’est un bon candidat pour le IOCCC, mais quand même):
!executeStepA() ? 0 :
!executeStepB() ? 0 :
!executeStepC() ? 0 :
!executeStepD() ? 0 : 1 ;
executeThisFunctionInAnyCase();
Le résultat est exactement le même que si nous faisions ce que le PO posté, à savoir:
if(executeStepA()){
if(executeStepB()){
if(executeStepC()){
if(executeStepD()){
}
}
}
}
executeThisFunctionInAnyCase();
Voici un truc que j'ai utilisé à plusieurs reprises, à la fois en C-what et en Java:
do {
if (!condition1) break;
doSomething();
if (!condition2) break;
doSomethingElse()
if (!condition3) break;
doSomethingAgain();
if (!condition4) break;
doYetAnotherThing();
} while(FALSE); // Or until(TRUE) or whatever your language likes
Je le préfère aux ifs imbriqués pour la clarté de celui-ci, en particulier lorsqu'il est correctement formaté avec des commentaires clairs pour chaque condition.
Comme @Jefffrey a dit, vous pouvez utiliser la fonctionnalité de court-circuit conditionnel dans presque toutes les langues, je n'aime personnellement pas les instructions conditionnelles avec plus de 2 conditions (plus d'un seul &&
ou ||
), juste un problème de la classe. Ce code fait la même chose (et devrait probablement compiler la même chose) et il me semble un peu plus propre. Vous n'avez pas besoin d'accolades, de sauts, de retours, de fonctions, de lambdas (uniquement c ++ 11), d'objets, etc. tant que chaque fonction de executeStepX()
renvoie une valeur pouvant être convertie en true
si la prochaine instruction doit être exécutée ou false
sinon.
if (executeStepA())
if (executeStepB())
if (executeStepC())
//...
if (executeStepN()); // <-- note the ';'
executeThisFunctionInAnyCase();
À chaque fois que l'une des fonctions renvoie false
, aucune des fonctions suivantes n'est appelée.
J'ai aimé la réponse de @Mayerz, car vous pouvez faire varier les fonctions à appeler (et leur ordre) au moment de l'exécution. Cela ressemble au type observateur où vous avez un groupe d’abonnés (fonctions, objets, peu importe) appelés et exécutés chaque fois qu’une condition arbitraire donnée est remplie. Cela peut être une tuerie dans de nombreux cas, alors utilisez-le à bon escient :)
[&]{
bool conditionA = executeStepA();
if (!conditionA) return; // break
bool conditionB = executeStepB();
if (!conditionB) return; // break
bool conditionC = executeStepC();
if (!conditionC) return; // break
}();
executeThisFunctionInAnyCase();
Nous créons une fonction lambda anonyme avec une capture de référence implicite et l'exécutons. Le code qu'il contient s'exécute immédiatement.
Quand il veut s’arrêter, il ne fait que return
s.
Ensuite, après son exécution, nous exécutons executeThisFunctionInAnyCase
.
return
dans le lambda est un break
à la fin du bloc. Tout autre type de contrôle de flux fonctionne.
Les exceptions sont laissées à elles-mêmes - si vous voulez les attraper, faites-le explicitement. Soyez prudent lorsque vous exécutez executeThisFunctionInAnyCase
si des exceptions sont générées. En règle générale, vous ne souhaitez pas exécuter executeThisFunctionInAnyCase
s'il peut générer une exception dans un gestionnaire d'exceptions, car cela entraînerait un désordre (ce qui dépend la langue).
Une propriété intéressante de telles fonctions inline basées sur la capture est que vous pouvez refactoriser le code existant à la place. Si votre fonction est vraiment longue, il est judicieux de la décomposer en composants.
Une variante de cela qui fonctionne dans plusieurs langues est:
bool working = executeStepA();
working = working && executeStepB();
working = working && executeStepC();
executeThisFunctionInAnyCase();
où vous écrivez des lignes individuelles que chaque court-circuit. Le code peut être injecté entre ces lignes, ce qui vous donne plusieurs "dans tous les cas", ou vous pouvez utiliser if(working) { /* code */ }
entre les étapes d'exécution pour inclure le code qui doit s'exécuter si et seulement si vous n'avez pas encore été sauvé.
Une bonne solution à ce problème devrait être robuste face à l'ajout d'un nouveau contrôle de flux.
En C++, une meilleure solution est de réunir une classe rapide scope_guard:
#ifndef SCOPE_GUARD_H_INCLUDED_
#define SCOPE_GUARD_H_INCLUDED_
template<typename F>
struct scope_guard_t {
F f;
~scope_guard_t() { f(); }
};
template<typename F>
scope_guard_t<F> scope_guard( F&& f ) { return {std::forward<F>(f)}; }
#endif
puis dans le code en question:
auto scope = scope_guard( executeThisFunctionInAnyCase );
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;
et le destructeur de scope
automatiquelaly exécute executeThisFunctionInAnyCase
. Vous pouvez en injecter de plus en plus "en fin de champ" (en leur attribuant un nom différent) chaque fois que vous créez une ressource non-RAII qui doit être nettoyée. Il peut également prendre lambdas, vous pouvez donc manipuler des variables locales.
Les gardes de domaine sophistiqués peuvent prendre en charge l'abandon de l'appel dans le destructeur (avec un garde bool
), bloquer/autoriser la copie et le déplacement et prendre en charge les gardes de portée "portables" effacés par type qui peuvent être renvoyés à partir de contextes internes.
Dans certaines situations spéciales, une arborescence d'héritage virtuel et des appels de méthodes virtuelles peuvent gérer votre logique d'arbre de décision.
objectp -> DoTheRightStep();
J'ai rencontré des situations où cela fonctionnait comme une baguette magique. Bien entendu, cela a du sens si votre ConditionX peut être traduit systématiquement en "objet est un" conditions.
Après avoir lu toutes les réponses, je souhaite proposer une nouvelle approche, qui pourrait être tout à fait claire et lisible dans les bonnes circonstances: Un modèle d’état.
Si vous regroupez toutes les méthodes (executeStepX) dans une classe d'objet, celle-ci peut avoir un attribut getState ()
class ExecutionChain
{
public:
enum State
{
Start,
Step1Done,
Step2Done,
Step3Done,
Step4Done,
FinalDone,
};
State getState() const;
void executeStep1();
void executeStep2();
void executeStep3();
void executeStep4();
void executeFinalStep();
private:
State _state;
};
Cela vous permettrait d’aplatir votre code d’exécution à ceci:
void execute
{
ExecutionChain chain;
chain.executeStep1();
if ( chain.getState() == Step1Done )
{
chain.executeStep2();
}
if ( chain.getState() == Step2Done )
{
chain.executeStep3();
}
if ( chain.getState() == Step3Done )
{
chain.executeStep4();
}
chain.executeFinalStep();
}
Ainsi, il est facilement lisible, facile à déboguer, vous disposez d’un contrôle de flux clair et vous pouvez également insérer de nouveaux comportements plus complexes (par exemple, exécuter une étape spéciale uniquement si au moins l'étape 2 est exécutée) ...
Mon problème avec d'autres approches comme ok = execute (); et si (execute ()), votre code doit être clair et lisible comme un organigramme de ce qui se passe. Dans l'organigramme, vous auriez deux étapes: 1. exécuter 2. une décision basée sur le résultat
Donc, vous ne devriez pas cacher vos importantes méthodes de levage de poids lourds dans des déclarations if ou similaires, elles devraient être autonomes!
Très simple.
if ((bool conditionA = executeStepA()) &&
(bool conditionB = executeStepB()) &&
(bool conditionC = executeStepC())) {
...
}
executeThisFunctionInAnyCase();
Cela préservera également les variables booléennes conditionnelles, conditionnelles et conditionnelles.
Vous pouvez utiliser une "instruction switch"
switch(x)
{
case 1:
//code fires if x == 1
break;
case 2:
//code fires if x == 2
break;
...
default:
//code fires if x does not match any case
}
est équivalent à:
if (x==1)
{
//code fires if x == 1
}
else if (x==2)
{
//code fires if x == 2
}
...
else
{
//code fires if x does not match any of the if's above
}
Cependant, je dirais qu'il n'est pas nécessaire d'éviter les chaînes if-else-. Une limitation des instructions de commutateur est qu'elles ne font que tester l'égalité exacte; c’est-à-dire que vous ne pouvez pas tester le "cas x <3" --- en C++ qui génère une erreur et en C cela peut fonctionner, mais vous comportez de manière inattendue, ce qui est pire que de générer une erreur, car votre programme fonctionnera de manière inattendue. façons.
while(executeStepA() && executeStepB() && executeStepC() && 0);
executeThisFunctionInAnyCase();
executeThisFunctionInAnyCase () devait être exécuté dans tous les cas, même si les autres fonctions ne se terminaient pas.
La déclaration while:
while(executeStepA() && executeStepB() && executeStepC() && 0)
exécutera toutes les fonctions et ne fera pas de boucle car sa déclaration est fausse. Cela peut également être fait pour réessayer un certain temps avant de quitter.
Pourquoi utiliser la POO? en pseudocode:
abstract class Abstraction():
function executeStepA(){...};
function executeStepB(){...};
function executeStepC(){...};
function executeThisFunctionInAnyCase(){....}
abstract function execute():
class A(Abstraction){
function execute(){
executeStepA();
executeStepB();
executeStepC();
}
}
class B(Abstraction){
function execute(){
executeStepA();
executeStepB();
}
}
class C(Abstraction){
function execute(){
executeStepA();
}
}
de cette façon, vos ifs disparaissent
item.execute();
item.executeThisFunctionInAnyCase();
Habituellement, les si peuvent être évités en utilisant la POO.
Qu'en est-il de déplacer le contenu conditionnel vers le reste comme dans:
if (!(conditionA = executeStepA()){}
else if (!(conditionB = executeStepB()){}
else if (!(conditionC = executeStepC()){}
else if (!(conditionD = executeStepD()){}
Ceci résout le problème d'indentation.
Les fausses boucles ont déjà été mentionnées, mais je n'ai pas vu l'astuce suivante dans les réponses données jusqu'à présent: vous pouvez utiliser une do { /* ... */ } while( evaulates_to_zero() );
pour mettre en œuvre un disjoncteur à déclenchement anticipé bidirectionnel. L'utilisation de break
termine la boucle sans passer par l'évaluation de l'instruction de condition, alors qu'un continue
évacuera l'instruction de condition.
Vous pouvez l'utiliser si vous avez deux types de finalisation, où un chemin doit faire un peu plus de travail que l'autre:
#include <stdio.h>
#include <ctype.h>
int finalize(char ch)
{
fprintf(stdout, "read a character: %c\n", (char)toupper(ch));
return 0;
}
int main(int argc, char *argv[])
{
int ch;
do {
ch = fgetc(stdin);
if( isdigit(ch) ) {
fprintf(stderr, "read a digit (%c): aborting!\n", (char)ch);
break;
}
if( isalpha(ch) ) {
continue;
}
fprintf(stdout, "thank you\n");
} while( finalize(ch) );
return 0;
}
L'exécution de ceci donne le protocole de session suivant:
dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak
-
thank you
read a character: -
dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak
a
read a character: A
dw@narfi ~/misc/test/fakeloopbreak $ ./fakeloopbreak
1
read a digit (1): aborting!
Eh bien, plus de 50 réponses à ce jour et personne n’a mentionné ce que je fais habituellement dans cette situation! (c’est-à-dire une opération qui comporte plusieurs étapes, mais il serait excessif d’utiliser une machine à états ou une table de pointeurs de fonctions):
if ( !executeStepA() )
{
// error handling for "A" failing
}
else if ( !executeStepB() )
{
// error handling for "B" failing
}
else if ( !executeStepC() )
{
// error handling for "C" failing
}
else
{
// all steps succeeded!
}
executeThisFunctionInAnyCase();
Avantages:
Désavantages: