Inverser la déclaration «if» pour réduire la nidification

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 la déclaration “if” pour réduire l’imbrication), et a suggéré la correction suivante:

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

Je voudrais 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”.

Un retour au milieu de la méthode n’est pas nécessairement mauvais. Il serait peut-être préférable de revenir immédiatement si cela rend plus claire 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 _isDead est vrai, nous pouvons immédiatement sortir de la méthode. Il serait peut-être préférable de le structurer de cette manière:

 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 nested par des clauses de garde.

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 cela facilite la compréhension des méthodes (et de nombreux outils d’ parsing statique en font l’un des indicateurs de la qualité du code).

D’un autre côté, votre méthode a également plusieurs points de sortie, ce que d’autres personnes pensent être un non-non.

Personnellement, je suis d’accord avec ReSharper et le premier groupe (dans un langage qui comporte des exceptions, je trouve idiot de discuter de «points de sortie multiples», presque tout peut être lancé, il existe donc de nombreux points de sortie potentiels).

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

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

La principale raison d’avoir moins d’imbrication est d’améliorer la lisibilité et la maintenabilité du code . Rappelez-vous que beaucoup d’autres développeurs auront besoin de 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 bon exemple de la possibilité de revenir tôt au début de la fonction. Pourquoi la lisibilité du rest de la fonction devrait-elle être affectée par la présence d’une vérification de pré-condition?

En ce qui concerne les points négatifs à propos du retour de 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 retourne.

Avoir plusieurs retours dans une fonction ne va pas affecter le travail du programmeur de maintenance.

La lisibilité du code sera mauvaise.

Comme d’autres l’ont mentionné, il ne devrait pas y avoir d’atteinte à la performance, mais il existe d’autres considérations. Mis à part ces préoccupations valables, cela peut aussi vous amener à des pièges dans certaines circonstances. Supposons que vous ayez plutôt un double :

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

Contrastez 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 inversé correctement peut ne pas l'être.

L’idée de ne revenir qu’à la fin d’une fonction est revenue des jours précédant la prise en charge des langues par les exceptions. Cela 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 provoquait le saut du code de nettoyage. . Le code de nettoyage ignoré peut entraîner une fuite de mémoire ou de ressource.

Cependant, dans un langage qui prend en charge les exceptions, il ne fournit 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 stream de contrôle qui entraîne la fin de la méthode. Cela signifie que le nettoyage doit être effectué en utilisant les mots-clés last ou using.

Quoi qu’il en soit, je pense que beaucoup de personnes citent la ligne direcsortingce «Un retour à la fin d’une méthode» sans comprendre pourquoi cela a toujours été une bonne chose, et que réduire la nidification pour améliorer la lisibilité est probablement un meilleur objective.

J’aimerais append qu’il y a un nom pour ceux qui sont inversés si c’est la clause de garde. Je l’utilise chaque fois que je peux.

Je déteste lire le code là où il y en a au début, deux écrans de code et rien 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

Cela n’affecte pas seulement l’esthétique, mais empêche également l’imbrication du code.

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

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

  • Il est maintenant évident que votre fonction n’a plus rien à faire si la condition est condition .
  • Il maintient le niveau d’imbrication vers le bas. L’imbrication blesse la lisibilité plus que vous ne le pensez.

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 récupération de place, try | finally construire et using blocs, 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.

Du sharepoint vue des performances, il n’y aura pas de différence notable entre les deux approches.

Mais le codage ne se limite pas à la performance. La clarté et la maintenabilité sont également très importantes. Et, dans de tels cas 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 les 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: lorsque vous n’avez plus rien à faire, vous pouvez aussi bien revenir tôt.

Un autre sharepoint vue, du sharepoint vue d’un style plus fonctionnel, est qu’une méthode ne devrait avoir qu’un seul sharepoint 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.

Vérifiez les clauses ou les conditions préalables (comme vous pouvez probablement le voir) pour vérifier si une condition donnée est remplie et interrompez le déroulement du programme. Ils sont parfaits pour les endroits où vous n’êtes vraiment intéressé que par l’un des résultats d’une déclaration if . Donc plutôt que de dire:

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

Vous inversez la condition et cassez 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 

return est loin d’être aussi sale que goto . Il vous permet de passer une valeur pour montrer le rest de votre code que la fonction ne peut pas exécuter.

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

 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 verrez que peu de gens se disputent que le premier est plus propre mais, bien sûr, il est complètement subjectif. Certains programmeurs aiment savoir dans quelles conditions quelque chose fonctionne par indentation, alors que je préfère plutôt garder le stream de méthode linéaire.

Je ne suggérerai pas un instant que les précons changeront votre vie ou que vous vous posiez, mais vous trouverez peut-être que votre code est un peu plus facile à lire.

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

Personnellement, je préfère seulement 1 sharepoint sortie. Il est facile à réaliser si vous gardez vos méthodes courtes et précises, et 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 voulez simplement vérifier les valeurs de certaines variables locales au sein d’une fonction avant qu’elle n’existe. Tout ce que vous devez faire est de placer un point d’arrêt sur le retour final et vous êtes assuré de le toucher (sauf si une exception est levée).

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 que souhaite un développeur est d’attendre quelque chose de sortingvial. En fin de compte, la compilation avec l’optimisation activée entraînera un code similaire. Donc, il est bon de connaître ces petites astuces qui portent leurs fruits dans les deux scénarios.

Le cas dans la question est clair, ReSharper est correct. Plutôt que d’imbriquer les instructions if et de créer une nouvelle scope dans le code, vous définissez une règle claire au début de votre méthode. Cela augmente la lisibilité, il est plus facile à maintenir et réduit le nombre de règles à parcourir pour trouver où ils veulent aller.

Beaucoup de bonnes raisons sur la façon dont le code ressemble . Mais qu’en est-il des résultats ?

Jetons un coup d’oeil à du code C # et à sa forme compilée IL:

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

Cet extrait simple peut être compilé. Vous pouvez ouvrir le fichier .exe généré avec ildasm et vérifier le résultat. Je ne posterai pas toutes les choses de 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 seconde condition, la même chose se produit après le calcul du résultat. Comparez et: accédez à Console.WriteLine si false ou à la fin si cela est vrai.
  4. Imprimez le message et revenez.

Il semble donc que le code saute à la fin. Que se passe-t-il si nous faisons une normale si avec du code nested?

 using System; public class Test { public static void Main(ssortingng[] 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 que, avant, il y avait des sauts par condition: si faux, passez au morceau de code suivant, si c’est vrai, allez à la fin. Et maintenant, le code IL coule mieux et a 3 sauts (le compilateur optimisé un peu): 1. Premier saut: lorsque Longueur est 0 à une partie où le code saute à nouveau (Troisième saut) à la fin. 2. Deuxièmement: 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 du programme sautera toujours.

C’est 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 est préférable d’avoir des conditions écrites pour qu’elles soient le plus souvent vraies; On peut aussi soutenir que c’est plus clair. En revanche, il crée des tests nesteds.

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

En théorie, inverser if peut conduire à de meilleures performances si elle augmente le taux de succès de la prévision de la twig. En pratique, je pense qu’il est très difficile de savoir exactement comment se comportera la prédiction de twig, en particulier après la compilation, donc je ne le ferais pas dans mon développement quotidien, sauf si j’écris du code d’assemblage.

Plus d’informations sur la prédiction de la twig ici .

Éviter les points de sortie multiples peut entraîner des gains de performance. Je ne suis pas sûr à propos de C #, mais en C ++, l’optimisation de la valeur de retour nommée (Elision de la copie, ISO C ++ ’03 12.8 / 15) dépend d’un sharepoint sortie unique. Cette optimisation évite de copier la construction de votre valeur de retour (dans votre exemple spécifique, peu importe). Cela peut 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 appelée.

Mais pour 99% des cas, la sauvegarde des appels de constructeur et de destructeur supplémentaires ne vaut pas la perte de lisibilité nestede if blocs sont introduits (comme d’autres l’ont fait remarquer).

C’est une question d’opinion.

Mon approche normale serait d’éviter les ifs sur une seule ligne et de 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 que votre travail réel si elles réussissent toutes.

A mon avis, un retour rapide est correct si vous retournez juste un void (ou un code de retour inutile que vous ne vérifierez jamais) et cela pourrait améliorer la lisibilité car vous évitez d’implanter et en même temps vous expliquez que votre fonction est terminée.

Si vous retournez réellement une returnValue – 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), et cela peut rendre votre code plus facile à gérer dans de nombreux cas.

Il y a déjà beaucoup de réponses perspicaces, mais je voudrais tout de même aborder une situation légèrement différente: au lieu de condition préalable, cela devrait être une fonction, pensez à une initialisation pas à pas, où vous doit vérifier chaque étape pour réussir et ensuite continuer avec le prochain. Dans ce cas, vous ne pouvez pas tout vérifier en haut.

J’ai trouvé mon code vraiment illisible en écrivant une application hôte ASIO avec ASIOSDK de Steinberg, car je suivais le paradigme d’imbrication. Cela s’est passé comme huit niveaux de profondeur, et je ne peux pas voir un défaut de conception là-bas, comme mentionné par Andrew Bullock ci-dessus. Bien sûr, j’aurais pu mettre du code interne dans une autre fonction, puis 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 fausse idée de ma part concernant une partie du code de nettoyage qui aurait dû se produire beaucoup plus tôt dans la fonction plutôt qu’à la fin. Avec des twigs nestedes, je n’aurais jamais vu cela, vous pourriez même dire qu’elles ont conduit à une idée fausse.

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

Je pense que cela dépend de ce que vous préférez, comme mentionné, il n’ya pas d’accord général. Pour réduire les désagréments, vous pouvez réduire ce type d’avertissement à “Hint”

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)) 
             retourner faux;


        do_something_that_take_an_hour (données);

        istance = new object_with_very_painful_constructor (data);

           si (istance 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 économisant. Il y a trop d’idées sur les codes propres, qui nécessitent simplement plus de pratique pour perdre leurs mauvaises idées “subjectives”.

Il y a plusieurs avantages à ce type de codage, mais pour moi, le grand avantage est que si vous pouvez revenir rapidement, vous pouvez améliorer la vitesse de votre application. IE je le sais à cause de la condition préalable X que je peux revenir rapidement avec une erreur. Cela élimine d’abord les cas d’erreur et réduit la complexité de votre code. Dans de nombreux cas, parce que le pipeline de processeurs peut être maintenant plus propre, il peut arrêter les pannes de pipeline ou les commutateurs. Deuxièmement, si vous êtes dans une boucle, casser ou retourner rapidement peut vous faire économiser beaucoup de cpu. Certains programmeurs utilisent des invariants de boucle pour effectuer ce type de sortie rapide, mais vous pouvez briser votre pipeline cpu et même créer un problème de recherche de mémoire, ce qui signifie que le processeur doit être chargé depuis le cache externe. Mais fondamentalement, je pense que vous devriez faire ce que vous vouliez, c’est-à-dire mettre fin à la boucle ou à la fonction et ne pas créer un chemin de code complexe pour implémenter une notion abstraite de code correct. Si le seul outil que vous avez est un marteau, tout ressemble à un clou.