Devrais-je suivre le chemin normal ou échouer tôt?


73

La citation suivante est extraite du livre Code Complete :

"Mettez le cas normal après le ifplutôt que après le else"

Ce qui signifie que des exceptions / déviations par rapport au chemin standard doivent être mises en place else.

Mais le programmeur pragmatique nous apprend à "planter tôt" (p. 120).

Quelle règle devrais-je suivre?


15
@gnat not duplicate
BЈовић

16
les deux ne sont pas mutuellement exclusifs, il n'y a pas d'ambiguïté.
Je me suis rendu

6
Je ne suis pas sûr que la citation codée complète soit un si bon conseil. On peut supposer qu'il s'agit d'une tentative d'amélioration de la lisibilité, mais il existe certainement des situations dans lesquelles vous souhaiteriez que les cas inhabituels soient d'abord textuels. Notez que ce n'est pas une réponse; les réponses existantes expliquent déjà la confusion qui découle bien de votre question.
Eamon Nerbonne

14
Crash tôt, crash dur! Si l'une de vos ifbranches revient, utilisez-la d'abord. Et évitez le elsereste du code, vous êtes déjà retourné si les conditions préalables ont échoué. Le code est plus facile à lire, moins d'indentation ...
CodeAngry

5
Est-ce juste moi qui pense que cela n'a rien à voir avec la lisibilité, mais plutôt une tentative erronée d'optimiser la prédiction de branche statique?
Journée

Réponses:


189

"Crash early" n'indique pas quelle ligne de code est textuelle avant. Il vous indique de détecter les erreurs lors de la première étape du traitement , de manière à ne pas prendre par inadvertance de décisions et de calculs basés sur un état déjà défaillant.

Dans une construction if/ else, seul l'un des blocs est exécuté. Par conséquent, on ne peut pas dire qu'aucun d'entre eux constitue une étape "antérieure" ou "ultérieure". Comment les commander est donc une question de lisibilité, et "échouer tôt" n'entre pas dans la décision.


2
Votre remarque générale est excellente mais je vois un problème. Si vous avez (if / else if / else), l'expression évaluée dans "else if" est évaluée après l'expression dans "if". Comme vous l'avez fait remarquer, le point important est la lisibilité par rapport à l'unité de traitement conceptuelle. Un seul bloc de code est exécuté, mais plusieurs conditions peuvent être évaluées.
Encaitar

7
@Encaitar, ce niveau de granularité est beaucoup plus petit que ce qui est généralement prévu lorsque la phrase "échec précoce" est utilisée.
Riwalk

2
@Encaitar C'est l'art de la programmation. Face à de multiples vérifications, l’idée est d’essayer celle qui a le plus de chances d’être vraie en premier. Toutefois, ces informations peuvent ne pas être connues au moment de la conception mais au stade de l'optimisation, mais méfiez-vous de l'optimisation prématurée.
Du

Commentaires justes. C'était une bonne réponse et je voulais seulement essayer d'aider à le rendre meilleur pour référence future
Encaitar

Les langages de script tels que JavaScript, Python, Perl, PHP, Bash, etc. sont des exceptions, car ils sont interprétés de manière linéaire. Dans les petites if/elseconstructions, cela n'a probablement pas d'importance. Mais ceux qui sont appelés en boucle ou avec plusieurs instructions dans chaque bloc pourraient s'exécuter plus rapidement avec la condition la plus courante.
DocSalvager

116

Si votre elseinstruction ne contient que du code d'échec, alors, très probablement, elle ne devrait pas être là.

Au lieu de faire ceci:

if file.exists() :
  if validate(file) :
    # do stuff with file...
  else :
    throw foodAtMummy
else :
  throw toysOutOfPram

fais ça

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

Vous ne voulez pas imbriquer profondément votre code simplement pour inclure la vérification des erreurs.

Et, comme tout le monde l’a déjà dit, les deux conseils ne sont pas contradictoires. L'une concerne l' ordre d'exécution , l'autre l'ordre de code .


4
Il vaut la peine de préciser que le conseil de mettre un flux normal dans le bloc après ifet un flux exceptionnel dans le bloc après elsene s'applique pas si vous n'en avez pas else! Les instructions Guard telles que celle-ci constituent la forme privilégiée de traitement des conditions d'erreur dans la plupart des styles de codage.
Jules

+1 c'est un bon point et donne en fait une réponse à la vraie question de savoir comment commander des choses avec des conditions d'erreur.
ashes999

Certainement beaucoup plus clair et facile à entretenir. C'est comme ça que j'aime le faire.
Jean

27

Vous devriez suivre les deux.

Le conseil "Crash early" / fail early signifie que vous devez tester vos entrées pour rechercher d'éventuelles erreurs le plus rapidement possible.
Par exemple, si votre méthode accepte une taille ou un nombre censé être positif (> 0), le conseil d'échec précoce signifie que vous testez cette condition dès le début de votre méthode, au lieu d'attendre que l'algorithme produise un non-sens. résultats.

Le conseil de mettre le cas normal en premier signifie que si vous testez une condition, le chemin le plus probable doit venir en premier. Cela contribue aux performances (car la prédiction de branche du processeur sera exacte plus souvent) et à la lisibilité, car vous n'avez pas à ignorer des blocs de code lorsque vous essayez de comprendre ce que la fonction fait dans les cas normaux.
Ce conseil ne s'applique pas vraiment lorsque vous testez une condition préalable et que vous effectuez une sauvegarde immédiate (à l'aide d'assertions ou de if (!precondition) throwconstructions), car il n'y a pas de gestion d'erreur à ignorer lors de la lecture du code.


1
Pouvez-vous élaborer sur la partie prédiction de branche? Je ne m'attendrais pas à ce que le code qui est le plus susceptible d'être utilisé dans le cas où l'exécution soit plus rapide que le code qui est plus susceptible d'être utilisé dans le cas contraire. Je veux dire, c'est tout le but de la prédiction de branche, n'est-ce pas?
Roman Reiner

@ user136712: Dans les processeurs modernes (rapides), les instructions sont extraites avant la fin du traitement de l'instruction précédente. La prédiction de branche est utilisée pour augmenter la probabilité que les instructions extraites lors de l'exécution d'une branche conditionnelle soient également les bonnes instructions à exécuter.
Bart van Ingen Schenau

2
Je sais ce que la prédiction de branche est. Si je lis correctement votre message, vous dites qu'il if(cond){/*more likely code*/}else{/*less likely code*/}est plus rapide que la if(!cond){/*less likely code*/}else{/*more likely code*/}prédiction de branche. Je penserais que la prédiction de branche n’est pas biaisée par ifl’ elseénoncé ou l’ énoncé et tient uniquement compte de l’histoire. Donc, si cela elseest plus susceptible de se produire, il devrait être en mesure de prédire cela tout aussi bien. Cette hypothèse est-elle fausse?
Roman Reiner

18

Je pense que @JackAidley en a déjà dit l'essentiel , mais laissez-moi le formuler comme suit :

sans exceptions (par exemple C)

Dans le flux de code normal, vous avez:

if (condition) {
    statement;
} else if (less_likely_condition) {
    less_likely_statement;
} else {
    least_likely_statement;
}
more_statements;

Dans le cas «erreur sur tôt», votre code lit tout à coup:

/* demonstration example, do NOT code like this */
if (condition) {
    statement;
} else {
    error_handling;
    return;
}

Si vous voyez ce modèle - un returndans un else(ou même ifbloc), retravailler immédiatement si le code en question ne pas avoir un elsebloc:

/* only code like this at University, to please structured programming professors */
function foo {
    if (condition) {
        lots_of_statements;
    }
    return;
}

Dans le monde réel…

/* code like this instead */
if (!condition) {
    error_handling;
    return;
}
lots_of_statements;

Cela évite les imbrications trop profondes et remplit le cas de «rupture précoce» (aide à garder l'esprit et le flux de code propres) et ne viole pas la «mise de la chose la plus probable dans la ifpièce», car il n'y a tout simplement aucune elsepartie .

C et nettoyage

Inspiré par une réponse à une question similaire (qui s’est mal compris), voici comment vous effectuez le nettoyage avec C. Vous pouvez utiliser un ou deux points de sortie, voici un pour deux points de sortie:

struct foo *
alloc_and_init(size_t arg1, int arg2)
{
    struct foo *res;

    if (!(res = calloc(sizeof(struct foo), 1)))
        return (NULL);

    if (foo_init1(res, arg1))
        goto err;
    res.arg1_inited = true;
    if (foo_init2(&(res->blah), arg2))
        goto err;
    foo_init_complete(res);
    return (res);

 err:
    /* safe because we use calloc and false == 0 */
    if (res.arg1_inited)
        foo_dispose1(res);
    free(res);
    return (NULL);
}

Vous pouvez les réduire en un point de sortie s'il y a moins de nettoyage à faire:

char *
NULL_safe_strdup(const char *arg)
{
    char *res = NULL;

    if (arg == NULL)
        goto out;

    /* imagine more lines here */
    res = strdup(arg);

 out:
    return (res);
}

Cette utilisation de gotoest parfaitement bien, si vous pouvez vous en occuper; Le conseil de ne pas utiliser gotoest destiné aux personnes qui ne peuvent pas encore décider par elles-mêmes si une utilisation est bonne, acceptable, mauvaise, code spaghetti ou autre chose.

Exceptions

Ce qui précède parle de langues sans exceptions, ce que je préfère vraiment (je peux utiliser la gestion explicite des erreurs beaucoup mieux et avec beaucoup moins de surprise). Pour citer igli:

<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)

Mais voici une suggestion pour savoir comment le faire dans une langue avec des exceptions et quand vous voulez bien l'utiliser:

erreur de retour face aux exceptions

Vous pouvez remplacer la plupart des premiers temps returnpar une exception. Cependant , votre flux de programme normal , c’est-à-dire tout flux de code dans lequel le programme n’a pas rencontré, eh bien, une exception… une condition d’erreur ou une telle erreur ne doit pas déclencher d’exception.

Cela signifie que…

# this page is only available to logged-in users
if not isLoggedIn():
    # this is Python 2.5 style; insert your favourite raise/throw here
    raise "eh?"

… Ça va, mais…

/* do not code like this! */
try {
    openFile(xyz, "rw");
} catch (LockedException e) {
    return "file is locked";
}
closeFile(xyz);
return "file is not locked";

… n'est pas. Fondamentalement, une exception n'est pas un élément de flux de contrôle . Cela rend également les opérations étranges («ces programmeurs Java ™ nous disent toujours que ces exceptions sont normales») et peuvent entraver le débogage (par exemple, demander à l'EDI d'interrompre toute exception). Les exceptions nécessitent souvent que l'environnement d'exécution décompresse la pile pour produire une trace, etc. Il existe probablement d'autres raisons.

Cela revient à: dans un langage prenant en charge les exceptions, utilisez ce qui correspond à la logique et au style existants et vous semble naturel. Si vous écrivez quelque chose à partir de zéro, faites-le d’accord tôt. Si vous écrivez une bibliothèque à partir de zéro, pensez à vos consommateurs. (N'utilisez jamais non plus abort()dans une bibliothèque…). Mais quoi que vous fassiez, en règle générale, aucune exception n'est levée si l'opération se poursuit (plus ou moins) normalement après.

conseils généraux Exceptions

Essayez d’obtenir l’utilisation en premier lieu des exceptions approuvées par toute l’équipe de développement. Fondamentalement, planifiez-les. Ne les utilisez pas en abondance. Parfois, même en C ++, Java ™, Python, un retour d'erreur est préférable. Parfois ce n'est pas; utilisez-les avec pensée.


En général, je considère les retours anticipés comme une odeur de code. À la place, je lancerais une exception si le code suivant échouait parce qu'une condition préalable n'était pas remplie. Il suffit de dire
DanMan

1
@DanMan: mon article a été écrit en pensant à C… Je n'utilise normalement pas d'exceptions. Mais j'ai prolongé l'article avec une suggestion (oups, elle a été plutôt longue). Exceptions; incidemment, nous avions la même question sur la liste de diffusion interne de la société hier…
mirabilos

En outre, utilisez des accolades même sur des ifs et des fors d’une seule ligne. Vous ne voulez pas une autre goto fail;identité cachée.
Bruno Kim

1
@BrunoKim: cela dépend entièrement du style / de la convention de codage du projet avec lequel vous travaillez. Je travaille avec BSD, où cela est mal vu (plus d'encombrement optique et de perte d'espace vertical); à $ dayjob cependant je les place comme convenu (moins difficile pour les débutants, moins de risque d'erreur, etc. comme vous l'avez dit).
mirabilos

3

À mon avis, la "condition de garde" est l’un des moyens les plus simples et les plus faciles de rendre le code lisible. Je déteste vraiment quand je vois ifau début de la méthode et ne vois pas le elsecode car il est hors de l'écran. Je dois faire défiler juste pour voir throw new Exception.

Placez les vérifications au début de sorte que la personne qui lit le code ne soit pas obligée de sauter partout dans la méthode pour le lire, mais au contraire de toujours le scanner de haut en bas.


2

(La réponse de @mirabilos est excellente, mais voici comment je réfléchis à la question pour parvenir à la même conclusion :)

Je pense à moi-même (ou à quelqu'un d'autre) en train de lire le code de ma fonction plus tard. Lorsque je lis la première ligne, je ne peux faire aucune hypothèse à propos de mon entrée (sauf celles que je ne vérifierai pas de toute façon). Donc, ma pensée est "Ok, je sais que je vais faire des choses avec mes arguments. Mais d'abord, nettoyons-les", c'est-à-dire supprimons les chemins de contrôle dans lesquels ils ne me plaisent pas. "Mais en même temps , Je ne vois pas le cas normal comme quelque chose qui est conditionné; je tiens à souligner que cela est normal.

int foo(int* bar, int baz) {

   if (bar == NULL) /* I don't like you, leave me alone */;
   if (baz < 0) /* go away */;

   /* there, now I can do the work I came into this function to do,
      and I can safely forget about those if's above and make all 
      the assumptions I like. */

   /* etc. */
}

-3

Ce type de classement dépend de la criticité de la section de code en question et de l’existence éventuelle de valeurs par défaut.

En d'autres termes:

A. section critique et pas de valeurs par défaut => Fail Early

B. section non critique et valeurs par défaut => Utiliser les valeurs par défaut dans la partie else

C. entre cas => décider au cas par cas


S'agit-il simplement de votre avis ou pouvez-vous l'expliquer / le sauvegarder d'une manière ou d'une autre?
moucher

comment exactement cela n'est-il pas sauvegardé, chaque option expliquant (sans beaucoup de mots en fait) pourquoi elle est utilisée?
Nikos M.

Je ne voudrais pas dire cela, mais les avis négatifs de (ma) cette réponse sont hors contexte :). Telle est la question posée par OP, savoir si vous avez une autre réponse est un tout autre problème
Nikos M.

Honnêtement, je ne vois pas l'explication ici. Dites, si quelqu'un écrit une autre opinion, comme "section critique et pas de valeurs par défaut => n'échoue pas tôt" , comment cette réponse aiderait-elle le lecteur à choisir deux opinions opposées? Pensez à le modifier pour qu'il corresponde mieux aux directives de réponse .
Gnat

ok, je vois, cela pourrait bien être une autre explication, mais au moins vous comprenez le mot "section critique" et "pas de défaut" qui peut impliquer une stratégie pour échouer tôt et c'est vraiment une expanation, même si elle est minimaliste
Nikos M.
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.