web-dev-qa-db-fra.com

Inverser l'instruction "if" pour réduire l'imbrication

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

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

ReSharper m'a donné l'avertissement ci-dessus (Inverser "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 était problématique, un peu comme "goto".

250
Lea Cohen

Un retour au milieu de la méthode n'est pas forcément mauvais. Il serait peut-être préférable de revenir immédiatement si cela clarifie 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 comme suit:

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.

274
jop

Ce n'est pas seulement esthétique , mais cela réduit également le niveau d'imbrication maximum à l'intérieur de la méthode. Ceci est généralement considéré comme un avantage, car il facilite la compréhension des méthodes (et même, plusieursstatiqueanalyseoutils fournir une mesure de ce comme l'un des indicateurs de la qualité du code).

D'autre part, votre méthode dispose également de 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 un langage comportant des exceptions, il est ridicule de parler de "points de sortie multiples"; presque tout peut être projeté, il existe donc de nombreux points de sortie potentiels dans toutes les méthodes).

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

326
Jon

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

La principale raison d'avoir moins d'imbrication est d'améliorer lisibilité du code et maintenabilité. N'oubliez pas que de nombreux 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.

Preconditions sont un bon exemple de la possibilité de revenir plus 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 préalable?

En ce qui concerne les inconvénients du renvoi 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 revient.

Le fait d'avoir plusieurs déclarations dans une fonction n'affectera pas le travail du programmeur de maintenance.

Mauvaise lisibilité du code.

101

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

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

Comparez cela avec l'inversion équivalente apparemment:

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

Ainsi, dans certaines circonstances, ce qui semble être un if correctement inversé pourrait ne pas l'être.

68
Michael McGowan

L'idée de revenir uniquement à la fin d'une fonction est revenue des jours avant que les langues ne prennent en charge les exceptions. Cela permettait aux programmes de pouvoir mettre du code de nettoyage à la fin d'une méthode, puis de s'assurer qu'elle serait appelée et qu'un autre programmeur ne cacherait pas un retour dans la méthode qui a entraîné l'omission du code de nettoyage. . Le code de nettoyage omis peut entraîner une fuite de mémoire ou de ressources.

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 flux de contrôle entraînant la fin de la méthode. Cela signifie que le nettoyage doit être effectué en utilisant les mots-clés finally ou.

Quoi qu'il en soit, je pense que beaucoup de gens citent le principe du "seul retour à la fin d'une méthode" sans comprendre pourquoi c'était toujours une bonne chose à faire et que réduire le nid pour améliorer la lisibilité est probablement un meilleur objectif.

50
Scott Langham

J'aimerais ajouter qu'il y a un nom pour ces si inversés - Guard Clause. Je l'utilise chaque fois que je peux.

Je déteste lire du code là où il y a si au début, deux écrans de code et aucun autre. Il suffit d'inverser si et de revenir. De cette façon, personne ne perdra du temps à faire défiler.

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

27
Piotr Perak

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

Cela peut en fait fonctionner comme une condition préalable pour garantir la validité de vos données.

21
Rion Williams

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 condition est valide.
  • Il maintient le niveau de nidification bas. Nidifier nuit à la lisibilité plus qu'on ne le pense.
18
Deestan

Les points de retour multiples constituaient 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, le try | finally construct et using blocs, il n'y a vraiment aucune raison pour laquelle vous devriez en avoir peur.

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

14
Richard Poole

En termes de 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 facilité de maintenance 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 courants de pensée divergents 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. Cela est naturel dans un style impératif: quand vous n'avez plus rien à faire, vous pouvez aussi bien revenir plus tôt.

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

12
Jeffrey Sax

Des clauses de garde ou des conditions préalables (comme vous pouvez probablement le constater) vérifient si une condition donnée est remplie et interrompt ensuite 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. 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

return n'est pas aussi sale que goto. Il vous permet de transmettre une valeur pour montrer le reste de votre code que la fonction n'a pas pu exécuter.

Vous verrez les meilleurs exemples d'utilisation de cette option 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 prétendent que le premier est plus propre mais bien sûr, c'est complètement subjectif. Certains programmeurs aiment connaître les conditions dans lesquelles une indentation est utilisée, alors que je préférerais de beaucoup garder une méthode linéaire.

Je ne suggérerai pas un seul instant que les préconfigurations vont changer votre vie ou vous mettre en demeure, mais vous trouverez peut-être votre code un peu plus facile à lire.

11
Oli

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

9
Jon Limjap

La performance est en deux parties. Vous avez des performances lorsque le logiciel est en production, mais vous voulez également avoir des performances lors du développement et du débogage. La dernière chose que veut un développeur, c'est "attendre" quelque chose de trivial. En fin de compte, compiler ceci avec l'optimisation activée donnera un code similaire. Il est donc bon de connaître ces petites astuces qui sont rentables dans les deux scénarios.

Le cas dans la question est clair, ReSharper est correct. Plutôt que d'imbriquer des instructions if et de créer une nouvelle portée dans le code, vous définissez une règle claire au début de votre méthode. Cela améliore la lisibilité, facilitera la maintenance et réduira le nombre de règles à parcourir pour déterminer où elles veulent aller.

8
Ryan Ternier

Personnellement, je préfère un seul point de sortie. Il est facile à accomplir si vos méthodes sont 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 qu'elle ne se termine. Tout ce que vous devez faire est de placer un point d'arrêt sur la déclaration finale et vous êtes assuré de le frapper (à moins qu'une exception ne soit levée).

7
ilitirit

Beaucoup de bonnes raisons à propos de à quoi ressemble le code. Mais qu'en est-il de 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(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 quel est le résultat. Je ne posterai pas tout ce qui concerne l'assembleur mais je décrirai les résultats.

Le code IL généré a les effets suivants:

  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: allez à Console.WriteLine si false ou à la fin si c'est vrai.
  4. Imprimez le message et revenez.

Il semble donc que le code va sauter à la fin. Que faire si nous faisons une normale si 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 y avait des sauts par condition: si false aller au code suivant, si true aller à end puis end. Et maintenant, le code IL coule mieux et comporte 3 sauts (le compilateur l'a optimisé un peu): 1. Premier saut: lorsque Longueur est égal à 0, le code saute à nouveau (troisième saut) jusqu'à 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, sautez à la fin.

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

5
graffic

C'est simplement controversé. Il n'y a pas "d'accord entre les programmeurs" sur la question du retour rapide. C'est toujours subjectif, 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 aussi dire 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.

4
unwind

En théorie, l'inversion de if pourrait améliorer les performances si elle augmentait le taux de réussite des prévisions de branche. En pratique, je pense qu'il est très difficile de savoir exactement comment la prédiction de branche se comportera, en particulier après la compilation, aussi je ne le ferais pas dans mon développement quotidien, sauf si j'écris du code Assembly.

Plus sur la prédiction de branche ici .

4
jfg956

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 placé au-dessus d'une fonction. Pensez à une initialisation pas à pas, où avoir à vérifier chaque étape pour réussir et ensuite passer à la 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. Il y avait huit niveaux de profondeur et je ne vois pas de défaut de conception, comme l'a mentionné Andrew Bullock ci-dessus. Bien sûr, j'aurais pu intégrer du code interne à une autre fonction, puis imbriquer les niveaux restants pour le rendre plus lisible, mais cela me semble plutôt aléatoire.

En remplaçant les clauses imbriquées avec gardes, j'ai même découvert une idée fausse de la mienne concernant une partie du code de nettoyage qui aurait dû apparaître beaucoup plus tôt dans la fonction plutôt qu'à la fin. Avec des branches imbriquées, je n’aurais jamais vu cela, vous pourriez même dire que cela a conduit à ma conception erronée.

Il peut donc s'agir d'une autre situation où les si inversés peuvent contribuer à un code plus clair.

3
Ingo Schalk-Schupp

Éviter plusieurs points de sortie peut conduire à des gains de performance. 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 de la présence d'un seul point de sortie. Cette optimisation évite que la copie ne construise votre valeur de retour (peu importe dans votre exemple). 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 appelée.

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

3
shibumi

À mon avis, un retour anticipé est acceptable si vous ne renvoyez que vide (ou un code retour inutile que vous ne contrôlerez jamais) et cela pourrait améliorer la lisibilité, car vous évitiez l'imbrication et vous expliquiez en même temps que votre fonction était remplie.

Si vous renvoyez réellement une valeur retournée - imbrication est généralement une meilleure façon de procéder car vous renvoyez votre valeur retournée juste à un endroit (à la fin - duh), et cela pourrait rendre votre code plus facile à gérer dans de nombreux cas.

2
JohnIdol

C'est une question d'opinion.

Mon approche normale serait d'éviter les ifs simples et retourne au milieu d'une méthode.

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

2
Colin Pickard

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 saut à 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 ni de cache de déploiement N1, mais le saut à 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 # tente de remplacer les sauts de loin par des sauts courts et dans ce cas, la probabilité que toutes les instructions soient déjà en cache est plus grande.

0

Je pense que cela dépend de ce que vous préférez, comme mentionné, il n’ya pas d’accord général sur le fond. Pour réduire les ennuis, vous pouvez réduire ce type d’avertissement à "Indice"

0
Bluenuance

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:

 function do_something (data) {
 
 if (! is_valid_data (data)) 
 renvoie false; 
 
 
 fait_quelquechose_que_prendre_une_heure (données); 
 
 istance = nouvel objet_avec_tout_painful_constructeur (données); 
 
 si (istance n'est pas valide) {
 error_message ( ); 
 return; 
 
} 
 connect_to_database (); 
 get_some_other_data (); 
 return; 
 } 

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

0
Joshi Spawnbrood

Ce type de codage présente plusieurs avantages, mais pour moi le gros gain est que, si vous pouvez revenir rapidement, vous pouvez améliorer la vitesse de votre application. IE Je sais qu'en raison de la Précondition X, je peux revenir rapidement avec une erreur. Cela supprime les cas d'erreur en premier et réduit la complexité de votre code. Dans de nombreux cas, le cpu le pipeline peut être maintenant plus propre, il peut arrêter les interruptions ou le blocage du pipeline. Deuxièmement, si vous êtes dans une boucle, interrompre ou sortir rapidement peut vous faire économiser beaucoup de ressources processeur. Certains programmeurs utilisent des invariants de boucle pour effectuer ce type de sortie rapide. 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 être chargé à partir du cache extérieur. Mais en gros, je pense que vous devriez faire ce que vous vouliez, c'est-à-dire que la fin de 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, alors tout ressemble à un clou.

0