Inversez l'instruction «if» pour réduire l'imbrication

272

Lorsque j'ai exécuté ReSharper sur mon code, par exemple:

    if (some condition)
    {
        Some code...            
    }

ReSharper m'a donné l'avertissement ci-dessus (inverser l'instruction "if" pour réduire l'imbrication) et a suggéré la correction suivante:

   if (!some condition) return;
   Some code...

J'aimerais comprendre pourquoi c'est mieux. J'ai toujours pensé que l'utilisation de "return" au milieu d'une méthode posait problème, un peu comme "goto".

Lea Cohen
la source
1
Je crois que la vérification et le retour d'exception au début sont corrects, mais je changerais la condition afin que vous vérifiiez l'exception directement plutôt que pas quelque chose (c'est-à-dire si (une certaine condition) revient).
bruceatk
36
Non, cela ne fera rien pour la performance.
Seth Carnegie
3
Je serais tenté de lancer une ArgumentException si ma méthode recevait des données incorrectes à la place.
avocat
1
@asawyer Oui, il y a ici toute une discussion parallèle sur les fonctions qui pardonnent trop les entrées absurdes - par opposition à l'utilisation d'un échec d'assertion. Écrire du code solide m'a ouvert les yeux. Dans ce cas, ce serait quelque chose comme ASSERT( exampleParam > 0 ).
Greg Hendershott
4
Les assertions concernent l'état interne et non les paramètres. Vous commenceriez par valider les paramètres, affirmer que votre état interne est correct, puis effectuer l'opération. Dans une version, vous pouvez ignorer les assertions ou les mapper à un arrêt de composant.
Simon Richter

Réponses:

296

Un retour au milieu de la méthode n'est pas forcément mauvais. Il pourrait être préférable de revenir immédiatement si cela rend plus clair l'intention du code. Par exemple:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

Dans ce cas, si _isDeadc'est vrai, nous pouvons immédiatement sortir de la méthode. Il serait peut-être préférable de le structurer de cette façon à la place:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

J'ai choisi ce code dans le catalogue de refactoring . Ce refactoring spécifique est appelé: Remplacer le conditionnel imbriqué par des clauses de garde.

jop
la source
13
Ceci est un très bel exemple! Le code refactorisé ressemble plus à une déclaration de cas.
Otherside
16
Probablement juste une question de goût: je suggère de changer le 2ème et le 3ème "si" en "sinon si" pour augmenter encore la lisibilité. Si l'on néglige la déclaration "retour", il serait toujours clair que le cas suivant n'est vérifié que si le précédent a échoué, c'est-à-dire que l'ordre des contrôles est important.
foraidt
2
Dans un exemple aussi simple, je suis d'accord pour dire que la 2e voie est meilleure, mais uniquement parce que ce qui est si évident se passe.
Andrew Bullock,
Maintenant, comment pouvons-nous prendre ce joli code jop écrit et empêcher Visual Studio de le décomposer et de mettre les retours sur des lignes distinctes? Cela me contrarie vraiment de reformater le code de cette façon. Rend ce code très lisible UGLY.
Mark T
@Mark T Il existe des paramètres dans Visual Studio pour l'empêcher de casser ce code.
Aaron Smith
333

Ce n'est pas seulement esthétique , mais cela réduit également le niveau d'imbrication maximal à l'intérieur de la méthode. Ceci est généralement considéré comme un plus car il facilite la compréhension des méthodes (et en effet, de nombreux outils d' analyse statique en fournissent une mesure comme l'un des indicateurs de la qualité du code).

D'un autre côté, cela fait également que votre méthode a plusieurs points de sortie, ce qu'un autre groupe de personnes considère comme un non-non.

Personnellement, je suis d'accord avec ReSharper et le premier groupe (dans une langue qui a des exceptions, je trouve idiot de discuter de "plusieurs points de sortie"; presque tout peut être lancé, il y a donc de nombreux points de sortie potentiels dans toutes les méthodes).

En ce qui concerne les performances : les deux versions devraient être équivalentes (sinon au niveau IL, puis certainement après que la gigue est terminée avec le code) dans toutes les langues. En théorie, cela dépend du compilateur, mais pratiquement tout compilateur largement utilisé d'aujourd'hui est capable de gérer des cas d'optimisation de code beaucoup plus avancés que cela.

Jon
la source
41
points de sortie uniques? Qui en a besoin?
sq33G
3
@ sq33G: Cette question sur SESE (et les réponses bien sûr) sont fantastiques. Merci pour le lien!
Jon
Je n'arrête pas d'entendre dans les réponses qu'il y a des gens qui plaident pour des points de sortie uniques, mais je n'ai jamais vu quelqu'un défendre cela, en particulier dans des langages comme C #.
Thomas Bonini
1
@AndreasBonini: L'absence de preuve n'est pas une preuve d'absence. :-)
Jon
Oui, bien sûr, je trouve juste étrange que tout le monde ressente le besoin de dire que certaines personnes aiment une autre approche si elles ne ressentent pas le besoin de le dire elles-mêmes =)
Thomas Bonini
102

C'est un peu un argument religieux, mais je suis d'accord avec ReSharper que vous devriez préférer moins d'imbrication. Je crois que cela l'emporte sur les inconvénients d'avoir plusieurs chemins de retour à partir d'une fonction.

La raison principale d'avoir moins d'imbrication est d'améliorer la lisibilité et la maintenabilité du code . N'oubliez pas que de nombreux autres développeurs devront lire votre code à l'avenir, et que le code avec moins d'indentation est généralement beaucoup plus facile à lire.

Les conditions préalables sont un excellent exemple de cas où il est correct de revenir tôt au début de la fonction. Pourquoi la lisibilité du reste de la fonction devrait-elle être affectée par la présence d'un contrôle de précondition?

En ce qui concerne les points négatifs concernant le retour plusieurs fois d'une méthode - les débogueurs sont assez puissants maintenant, et il est très facile de savoir exactement où et quand une fonction particulière revient.

Avoir plusieurs retours dans une fonction n'affectera pas le travail du programmeur de maintenance.

Une mauvaise lisibilité du code le sera.

LeopardSkinPillBoxHat
la source
2
Les retours multiples dans une fonction affectent le programmeur de maintenance. Lors du débogage d'une fonction, à la recherche de la valeur de retour escroc, le responsable doit placer des points d'arrêt à tous les endroits qui peuvent revenir.
EvilTeach
10
Je mettrais un point d'arrêt sur l'accolade d'ouverture. Si vous parcourez la fonction, non seulement vous pouvez voir les contrôles effectués dans l'ordre, mais il sera très clair sur quel contrôle la fonction a atterri pour cette exécution.
John Dunagan
3
Je suis d'accord avec John ici ... Je ne vois pas le problème de simplement parcourir toute la fonction.
Nailer
5
@Nailer Très tard, je suis particulièrement d'accord, car si la fonction est trop grande pour être exécutée, elle devrait de toute façon être séparée en plusieurs fonctions!
Aidiakapi
1
D'accord avec John aussi. Peut-être que c'est une vieille école de penser à mettre le point d'arrêt au fond, mais si vous êtes curieux de savoir ce qu'une fonction retourne, vous allez parcourir cette fonction pour voir pourquoi elle retourne ce qu'elle retourne de toute façon. Si vous voulez juste voir ce qu'il retourne, placez-le sur le dernier support.
user441521
70

Comme d'autres l'ont mentionné, il ne devrait pas y avoir de perte de performances, mais il y a d'autres considérations. Mis à part ces préoccupations valables, cela peut également vous ouvrir à des pièges dans certaines circonstances. Supposons que vous ayez doubleplutôt affaire à un :

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

Comparez cela avec l' inversion apparemment équivalente:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Donc, dans certaines circonstances, ce qui semble être correctement inversé ifpeut ne pas l'être.

Michael McGowan
la source
4
Il convient également de noter que le correctif rapide de resharper inverse exampleParam> 0 en exampleParam <0 et non exampleParam <= 0, ce qui m'a rattrapé.
nickd
1
Et c'est pourquoi ce chèque devrait être à l'avance et retourné également étant donné que le développeur pense qu'un nan devrait entraîner un renflouement.
user441521
51

L'idée de ne revenir qu'à la fin d'une fonction est revenue des jours avant que les langues ne prennent en charge les exceptions. Il permettait aux programmes de compter sur la possibilité de mettre du code de nettoyage à la fin d'une méthode, puis d'être sûr qu'il serait appelé et qu'un autre programmeur ne cacherait pas un retour dans la méthode qui aurait fait sauter le code de nettoyage . Un code de nettoyage ignoré peut entraîner une fuite de mémoire ou de ressources.

Cependant, dans un langage qui prend en charge les exceptions, il n'offre aucune garantie de ce type. Dans un langage qui prend en charge les exceptions, l'exécution de toute instruction ou expression peut provoquer un flux de contrôle qui entraîne la fin de la méthode. Cela signifie que le nettoyage doit être effectué en utilisant finalement ou en utilisant des mots clés.

Quoi qu'il en soit, je dis que je pense que beaucoup de gens citent la directive `` seul retour à la fin d'une méthode '' sans comprendre pourquoi cela a jamais été une bonne chose à faire, et que réduire l'imbrication pour améliorer la lisibilité est probablement un meilleur objectif.

Scott Langham
la source
6
Vous venez de comprendre pourquoi les exceptions sont UglyAndEvil [tm] ... ;-) Les exceptions sont des gotos dans un déguisement sophistiqué et coûteux.
EricSchaefer
16
@Eric, vous devez avoir été exposé à un code exceptionnellement mauvais. C'est assez évident quand ils sont mal utilisés, et ils vous permettent généralement d'écrire du code de meilleure qualité.
Robert Paulson,
C'est la réponse que je cherchais vraiment! Il y a d'excellentes réponses ici, mais la plupart d'entre elles se concentrent sur des exemples, et non sur la véritable raison pour laquelle cette recommandation est née.
Gustavo Mori
C'est la meilleure réponse, car elle explique pourquoi tout cet argument a surgi en premier lieu.
Buttle Butkus
If you've got deep nesting, maybe your function is trying to do too many things.=> ce n'est pas une conséquence correcte de votre phrase précédente. Parce que juste avant de dire que vous pouvez refactoriser le comportement A avec le code C en comportement A avec le code D. le code D est plus propre, accordé, mais "trop ​​de choses" se réfèrent au comportement, qui n'a PAS changé. Par conséquent, vous n'avez aucun sens avec cette conclusion.
v.oddou
30

Je voudrais ajouter qu'il y a un nom pour ceux qui sont inversés - Clause de garde. Je l'utilise chaque fois que je le peux.

Je déteste lire du code là où il y a si au début, deux écrans de code et pas d'autre. Inversez simplement si et revenez. De cette façon, personne ne perdra son temps à faire défiler.

http://c2.com/cgi/wiki?GuardClause

Piotr Perak
la source
2
Exactement. Il s'agit plutôt d'un refactoring. Cela aide à lire le code plus facilement comme vous l'avez mentionné.
rpattabi
le «retour» n'est pas suffisant et horrible pour une clause de garde. Je ferais: si (ex <= 0) jetez WrongParamValueEx ("[MethodName] Bad input param 1 value {0} ... et vous devrez intercepter l'exception et écrire dans le journal de votre application
JohnJohnGa
3
@John. Vous avez raison s'il s'agit en fait d'une erreur. Mais souvent ce n'est pas le cas. Et au lieu de vérifier quelque chose à chaque endroit où méthode est appelée méthode, il suffit de le vérifier et de retourner ne rien faire.
Piotr Perak
3
Je détesterais lire une méthode qui est deux écrans de code, point, ifs ou non. lol
jpmc26
1
Personnellement, je préfère les retours précoces pour cette raison (aussi: éviter l'imbrication). Cependant, cela n'est pas lié à la question initiale sur les performances et devrait probablement être un commentaire.
Patrick M
22

Cela n'affecte pas seulement l'esthétique, mais il empêche également l'imbrication de code.

Il peut en fait fonctionner comme une condition préalable pour garantir que vos données sont également valides.

Rion Williams
la source
18

C'est bien sûr subjectif, mais je pense que cela s'améliore fortement sur deux points:

  • Il est maintenant immédiatement évident que votre fonction n'a plus rien à faire si condition est maintenue.
  • Il maintient le niveau d'imbrication bas. L'imbrication nuit plus à la lisibilité que vous ne le pensez.
Deestan
la source
15

Plusieurs points de retour étaient un problème en C (et dans une moindre mesure en C ++) car ils vous obligeaient à dupliquer le code de nettoyage avant chacun des points de retour. Avec la collecte des ordures, le try| finallyconstruire et usingbloquer, il n'y a vraiment aucune raison pour que vous en ayez peur.

En fin de compte, cela revient à ce que vous et vos collègues trouvez plus facile à lire.

Richard Poole
la source
Ce n'est pas la seule raison. Il y a une raison académique se référant au pseudo-code qui n'a rien à voir avec des considérations pratiques comme le nettoyage des trucs. Cette raison académique a à voir avec le respect des formes de base des constructions impératives. De la même manière, vous ne placez pas d'exitateurs de boucle en son milieu. De cette façon, les invariants peuvent être rigoureusement détectés et le comportement peut être prouvé. Ou la résiliation peut être prouvée.
v.oddou
1
nouvelles: en fait, j'ai trouvé une raison très pratique pour laquelle les ruptures et les retours précoces sont mauvais, et c'est une conséquence directe de ce que j'ai dit, l'analyse statique. vous voilà, papier guide d'utilisation du compilateur C ++: d3f8ykwhia686p.cloudfront.net/1live/intel/… . Phrase clé:a loop that is not vectorizable due to a second data-dependent exit
v.oddou
12

En termes de performances, il n'y aura pas de différence notable entre les deux approches.

Mais le codage ne se limite pas aux performances. La clarté et la maintenabilité sont également très importantes. Et, dans des cas comme celui-ci où cela n'affecte pas les performances, c'est la seule chose qui compte.

Il existe des écoles de pensée concurrentes quant à l'approche préférable.

Une vue est celle que d'autres ont mentionnée: la deuxième approche réduit le niveau d'imbrication, ce qui améliore la clarté du code. C'est naturel dans un style impératif: quand vous n'avez plus rien à faire, vous pouvez aussi bien revenir tôt.

Un autre point de vue, du point de vue d'un style plus fonctionnel, est qu'une méthode ne devrait avoir qu'un seul point de sortie. Tout dans un langage fonctionnel est une expression. Donc, si les instructions doivent toujours avoir une clause else. Sinon, l'expression if n'aurait pas toujours de valeur. Donc dans le style fonctionnel, la première approche est plus naturelle.

Jeffrey Sax
la source
11

Les clauses de garde ou les conditions préalables (comme vous pouvez probablement le voir) vérifient si une certaine condition est remplie, puis interrompent le déroulement du programme. Ils sont parfaits pour les endroits où vous n'êtes vraiment intéressé que par un seul résultat d'une ifdéclaration. Alors plutôt que de dire:

if (something) {
    // a lot of indented code
}

Vous inversez la condition et rompez si cette condition inversée est remplie

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

returnest loin d'être aussi sale que goto. Il vous permet de passer une valeur pour montrer au reste de votre code que la fonction n'a pas pu s'exécuter.

Vous verrez les meilleurs exemples où cela peut être appliqué dans des conditions imbriquées:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

contre:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

Vous trouverez peu de gens qui soutiennent que le premier est plus propre mais bien sûr, c'est complètement subjectif. Certains programmeurs aiment savoir dans quelles conditions quelque chose fonctionne par indentation, alors que je préfère de loin garder un flux de méthode linéaire.

Je ne dirai pas un seul instant que les précons changeront votre vie ou vous rendront fou, mais vous pourriez trouver votre code juste un peu plus facile à lire.

Oli
la source
6
Je suppose que je suis l'un des rares à l'époque. Je trouve plus facile de lire la première version. Les if imbriqués rendent l'arbre de décision plus évident. D'un autre côté, s'il y a quelques conditions préalables, je conviens qu'il vaut mieux les mettre toutes en haut de la fonction.
Otherside
@Otherside: tout à fait d'accord. les retours écrits en mode série obligent votre cerveau à sérialiser les chemins possibles. Lorsque l'arborescence if peut être mappée directement dans une logique transicente, par exemple, elle peut être compilée pour lisp. mais la mode de retour en série nécessiterait un compilateur beaucoup plus difficile à faire. Le point ici n'a évidemment rien à voir avec ce scénario hypothétique, il a à voir avec l'analyse de code, les chances d'optimisation, les preuves de correction, les preuves de terminaison et la détection d'invariants.
v.oddou
1
Personne ne trouve plus facile de lire du code avec plus de nids. Plus un bloc ressemble à quelque chose, plus il est facile à lire en un coup d'œil. Il n'y a aucun moyen que le premier exemple ici soit plus facile à lire et il ne fait que 2 nids lorsque la plupart du code comme celui-ci dans le monde réel va plus loin et à chaque nid, il nécessite plus de puissance cérébrale pour suivre.
user441521
1
Si l'imbrication était deux fois plus profonde, combien de temps cela vous prendrait-il pour répondre à la question: "Quand faites-vous" autre chose "?" Je pense que vous auriez du mal à y répondre sans stylo ni papier. Mais vous pourriez y répondre assez facilement si tout était mis en garde.
David Storfer
9

Il y a plusieurs bons points ici, mais plusieurs points de retour peuvent également être illisibles , si la méthode est très longue. Cela étant dit, si vous allez utiliser plusieurs points de retour, assurez-vous simplement que votre méthode est courte, sinon le bonus de lisibilité de plusieurs points de retour pourrait être perdu.

Jon Limjap
la source
8

La performance est en deux parties. Vous avez des performances lorsque le logiciel est en production, mais vous souhaitez également avoir des performances lors du développement et du débogage. La dernière chose qu'un développeur souhaite, c'est "d'attendre" quelque chose de trivial. En fin de compte, la compilation avec l'optimisation activée entraînera un code similaire. Il est donc bon de connaître ces petites astuces payantes dans les deux scénarios.

Le cas de la question est clair, ReSharper a raison. Plutôt que d'imbriquer des ifinstructions et de créer une nouvelle étendue dans le code, vous définissez une règle claire au début de votre méthode. Il augmente la lisibilité, il sera plus facile à maintenir et il réduit la quantité de règles à parcourir pour trouver où ils veulent aller.

Ryan Ternier
la source
7

Personnellement, je préfère un seul point de sortie. C'est facile à réaliser si vous gardez vos méthodes courtes et précises, et cela fournit un modèle prévisible pour la prochaine personne qui travaille sur votre code.

par exemple.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

Ceci est également très utile si vous souhaitez simplement vérifier les valeurs de certaines variables locales dans une fonction avant sa fermeture. Tout ce que vous devez faire est de placer un point d'arrêt sur le retour final et vous êtes assuré de l'atteindre (sauf si une exception est levée).

ilitirit
la source
4
bool PerformDefaultOperation () {DataStructure defaultParameters = this.GetApplicationDefaults (); return (defaultParameters! = NULL && this.DoSomething (defaultParameters);} Voilà, corrigé pour vous. :)
tchen
1
Cet exemple est très basique et manque le point de ce modèle. Ayez environ 4 autres vérifications à l'intérieur de cette fonction, puis dites-nous qu'elle est plus lisible, car ce n'est pas le cas.
user441521
5

Beaucoup de bonnes raisons sur l'apparence du code . Mais qu'en est-il des résultats ?

Jetons un coup d'œil à du code C # et à sa forme compilée IL:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Ce simple extrait peut être compilé. Vous pouvez ouvrir le fichier .exe généré avec ildasm et vérifier le résultat. Je ne publierai pas tout ce qui concerne l'assembleur mais je décrirai les résultats.

Le code IL généré effectue les opérations suivantes:

  1. Si la première condition est fausse, passe au code où se trouve la seconde.
  2. Si c'est vrai, saute à la dernière instruction. (Remarque: la dernière instruction est un retour).
  3. Dans la deuxième condition, la même chose se produit après le calcul du résultat. Comparez et: arrivé à la Console.WriteLine si faux ou à la fin si c'est vrai.
  4. Imprimez le message et revenez.

Il semble donc que le code ira jusqu'au bout. Et si nous faisons un if normal avec du code imbriqué?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

Les résultats sont assez similaires dans les instructions IL. La différence est qu'avant il devait y avoir des sauts par condition: si faux, passez au morceau de code suivant, si vrai, allez à la fin. Et maintenant, le code IL circule mieux et a 3 sauts (le compilateur l'a optimisé un peu): 1. Premier saut: lorsque la longueur est 0 à une partie où le code saute à nouveau (troisième saut) jusqu'à la fin. 2. Deuxième: au milieu de la deuxième condition pour éviter une instruction. 3. Troisièmement: si la deuxième condition est fausse, passez à la fin.

Quoi qu'il en soit, le compteur de programmes sautera toujours.

graffic
la source
5
C'est une bonne info - mais personnellement, je m'en fiche si l'IL "coule mieux" parce que les gens qui vont gérer le code ne verront pas d'IL
JohnIdol
1
Vous ne pouvez vraiment pas anticiper le fonctionnement de l'optimisation dans la prochaine mise à jour du compilateur C # par rapport à ce que vous voyez aujourd'hui. Personnellement, je ne passerais AUCUN temps à essayer de modifier le code C # afin de produire un résultat différent dans l'IL, à moins que les tests ne montrent que vous avez un problème de performances GRAVE dans une boucle serrée quelque part et que vous n'avez plus d'autres idées . Même alors, je réfléchirais plus sérieusement aux autres options. Dans le même ordre d'idées, je doute que vous ayez une idée des types d'optimisations que le compilateur JIT fait avec l'IL qui y est appliqué pour chaque plate-forme ...
Craig
5

En théorie, l'inversion ifpourrait conduire à de meilleures performances si elle augmente le taux de succès de prédiction de branche. Dans la pratique, je pense qu'il est très difficile de savoir exactement comment se comportera la prédiction de branche, en particulier après la compilation, donc je ne le ferais pas dans mon développement quotidien, sauf si j'écris du code assembleur.

Plus d'informations sur la prédiction des branches ici .

jfg956
la source
4

C'est tout simplement controversé. Il n'y a pas "d'accord entre les programmeurs" sur la question du retour anticipé. C'est toujours subjectif, pour autant que je sache.

Il est possible de faire un argument de performance, car il vaut mieux avoir des conditions écrites pour qu'elles soient le plus souvent vraies; on peut également affirmer que c'est plus clair. En revanche, il crée des tests imbriqués.

Je ne pense pas que vous obtiendrez une réponse concluante à cette question.

se détendre
la source
Je ne comprends pas votre argument de performance. Les conditions sont booléennes donc quel que soit le résultat, il y a toujours deux résultats ... Je ne vois pas pourquoi inverser une déclaration (ou pas) changerait un résultat. (Sauf si vous dites que l'ajout d'un "NON" à la condition ajoute une quantité mesurable de traitement ...
Oli
2
L'optimisation fonctionne comme ceci: si vous vous assurez que le cas le plus courant se trouve dans le bloc if au lieu du bloc else, le CPU aura généralement déjà les instructions du bloc if chargées dans son pipeline. Si la condition retourne false, le CPU doit vider son pipeline et ...
Otherside
J'aime aussi faire ce que les autres disent juste pour la lisibilité, je déteste avoir des cas négatifs dans le bloc "alors" plutôt que dans "le reste". Il est logique de le faire même sans savoir ou considérer ce que fait le CPU
Andrew Bullock
3

Il y a déjà beaucoup de réponses perspicaces, mais je voudrais quand même m'orienter vers une situation légèrement différente: au lieu de la condition préalable, cela devrait être mis en tête d'une fonction, pensez à une initialisation pas à pas, où vous devez vérifier chaque étape pour réussir, puis passez à l'étape suivante. Dans ce cas, vous ne pouvez pas tout vérifier en haut.

J'ai trouvé mon code vraiment illisible lors de l'écriture d'une application hôte ASIO avec ASIOSDK de Steinberg, alors que je suivais le paradigme d'imbrication. Cela a atteint huit niveaux de profondeur, et je ne vois pas de défaut de conception, comme mentionné par Andrew Bullock ci-dessus. Bien sûr, j'aurais pu compresser du code interne dans une autre fonction, puis y imbriquer les niveaux restants pour le rendre plus lisible, mais cela me semble plutôt aléatoire.

En remplaçant l'imbrication par des clauses de garde, j'ai même découvert une méprise sur la partie du code de nettoyage qui aurait dû se produire beaucoup plus tôt dans la fonction plutôt qu'à la fin. Avec des branches imbriquées, je n'aurais jamais vu ça, on pourrait même dire qu'elles ont conduit à ma méprise.

Donc, cela pourrait être une autre situation où des if inversés peuvent contribuer à un code plus clair.

Ingo Schalk-Schupp
la source
3

Éviter plusieurs points de sortie peut entraîner des gains de performances. Je ne suis pas sûr de C # mais en C ++, l'optimisation de la valeur de retour nommée (Copy Elision, ISO C ++ '03 12.8 / 15) dépend du fait d'avoir un seul point de sortie. Cette optimisation évite la copie de construire votre valeur de retour (dans votre exemple spécifique, cela n'a pas d'importance). Cela pourrait entraîner des gains de performances considérables dans les boucles serrées, car vous enregistrez un constructeur et un destructeur à chaque fois que la fonction est invoquée.

Mais pour 99% des cas, la sauvegarde des appels de constructeur et de destructeur supplémentaires ne vaut pas la perte de lisibilité des ifblocs imbriqués (comme d'autres l'ont souligné).

shibumi
la source
2
Là. il m'a fallu 3 longues lectures de dizaines de réponses en 3 questions demandant la même chose (dont 2 gelées), pour finalement trouver quelqu'un mentionnant NRVO. gee ... merci.
v.oddou
2

C'est une question d'opinion.

Mon approche normale consisterait à éviter les ifs à une seule ligne et à revenir au milieu d'une méthode.

Vous ne voudriez pas de lignes comme cela suggère partout dans votre méthode, mais il y a quelque chose à dire pour vérifier un tas d'hypothèses en haut de votre méthode, et ne faire votre travail réel que si elles réussissent toutes.

Colin Pickard
la source
2

À mon avis, un retour précoce est bien si vous retournez simplement void (ou un code de retour inutile que vous ne vérifierez jamais) et cela pourrait améliorer la lisibilité car vous évitez l'imbrication et en même temps, vous expliquez que votre fonction est terminée.

Si vous renvoyez réellement une valeur de retour, l'imbrication est généralement une meilleure façon de procéder, car vous retournez votre valeur de retour juste au même endroit (à la fin - duh), et cela pourrait rendre votre code plus maintenable dans de nombreux cas.

JohnIdol
la source
1

Je ne suis pas sûr, mais je pense que R # essaie d'éviter les sauts lointains. Lorsque vous avez IF-ELSE, le compilateur fait quelque chose comme ceci:

Condition false -> far jump to false_condition_label

true_condition_label: instruction1 ... instruction_n

false_condition_label: instruction1 ... instruction_n

bloc d'extrémité

Si la condition est vraie, il n'y a pas de saut et pas de cache L1 de déploiement, mais le saut vers false_condition_label peut être très loin et le processeur doit déployer son propre cache. La synchronisation du cache coûte cher. R # essaie de remplacer les sauts lointains par des sauts courts et dans ce cas, il y a une plus grande probabilité que toutes les instructions soient déjà dans le cache.

Marcin Gołembiewski
la source
0

Je pense que cela dépend de ce que vous préférez, comme mentionné, il n'y a pas d'accord général afaik. Pour réduire la gêne, vous pouvez réduire ce type d'avertissement à "Astuce"

Bluenuance
la source
0

Mon idée est que le retour "au milieu d'une fonction" ne devrait pas être aussi "subjectif". La raison est assez simple, prenez ce code:

    fonction do_something (data) {

      if (! is_valid_data (data)) 
            retour faux;


       do_something_that_take_an_hour (données);

       istance = new object_with_very_painful_constructor (données);

          si (la distance n'est pas valide) {
               Message d'erreur( );
                revenir ;

          }
       connect_to_database ();
       get_some_other_data ();
       revenir;
    }

Peut-être que le premier "retour" n'est pas si intuitif, mais c'est vraiment une économie. Il y a trop d'idées sur les codes propres, qui ont simplement besoin de plus de pratique pour perdre leurs mauvaises idées "subjectives".

Joshi Spawnbrood
la source
Avec un langage d'exception, vous n'avez pas ces problèmes.
user441521
0

Il existe plusieurs avantages à ce type de codage, mais pour moi, le gros avantage est que si vous pouvez revenir rapidement, vous pouvez améliorer la vitesse de votre application. IE Je sais qu'en raison de la condition X, je peux retourner rapidement avec une erreur. Cela supprime d'abord les cas d'erreur et réduit la complexité de votre code. Dans de nombreux cas, le pipeline du processeur peut désormais être plus propre, il peut arrêter les plantages ou les commutateurs du pipeline. Deuxièmement, si vous êtes dans une boucle, une rupture ou un retour rapide peut vous faire économiser beaucoup de CPU. Certains programmeurs utilisent des invariants de boucle pour effectuer ce type de sortie rapide, mais dans ce cas, vous pouvez casser votre pipeline de processeur et même créer un problème de recherche de mémoire et signifier que le processeur doit charger à partir du cache extérieur. Mais au fond, je pense que vous devriez faire ce que vous vouliez, c'est-à-dire que la boucle ou la fonction ne crée pas un chemin de code complexe juste pour implémenter une notion abstraite de code correct. Si le seul outil dont vous disposez est un marteau, tout ressemble à un clou.

David Allan Finch
la source
En lisant la réponse de graffic, on dirait plutôt que le code imbriqué a été optimisé par le compilateur de manière à ce que le code exécuté soit plus rapide que l'utilisation de plusieurs retours. Cependant, même si plusieurs retours étaient plus rapides, cette optimisation
n'accélérera
1
Je ne suis pas d'accord - La première loi consiste à obtenir l'algorithme correct. Mais nous sommes tous des ingénieurs, ce qui consiste à résoudre le problème correct avec les ressources que nous avons et que nous faisons dans le budget disponible. Une application trop lente n'est pas adaptée à l'usage.
David Allan Finch