Une fonction doit-elle avoir une seule instruction return?


780

Y a-t-il de bonnes raisons pour lesquelles il est préférable d'avoir une seule instruction de retour dans une fonction?

Ou est-il correct de revenir d'une fonction dès qu'il est logiquement correct de le faire, ce qui signifie qu'il peut y avoir de nombreuses instructions de retour dans la fonction?


25
Je ne suis pas d'accord pour dire que la question est indépendante de la langue. Avec certaines langues, avoir plusieurs retours est plus naturel et pratique qu'avec d'autres. Je serais plus susceptible de me plaindre des premiers retours dans une fonction C que dans une fonction C ++ qui utilise RAII.
Adrian McCarthy

3
Ceci est étroitement lié et a d'excellentes réponses: programmers.stackexchange.com/questions/118703/…
Tim Schmelter

indépendant de la langue? Expliquez à quelqu'un qui utilise un langage fonctionnel qu'il doit utiliser un retour par fonction: p
Boiethios

Réponses:


741

J'ai souvent plusieurs instructions au début d'une méthode pour revenir à des situations "faciles". Par exemple, ceci:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... peut être rendu plus lisible (à mon humble avis) comme ceci:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Alors oui, je pense que c'est bien d'avoir plusieurs "points de sortie" d'une fonction / méthode.


83
D'accord. Bien qu'avoir plusieurs points de sortie puisse devenir incontrôlable, je pense définitivement que c'est mieux que de mettre toute votre fonction dans un bloc IF. Utilisez return aussi souvent qu'il est logique de garder votre code lisible.
Joshua Carmody

172
Ceci est connu comme une «déclaration de garde» est le refactoring de Fowler.
Lars Westergren, le

12
Lorsque les fonctions sont maintenues relativement courtes, il n'est pas difficile de suivre la structure d'une fonction avec un point de retour proche du milieu.
KJAWolf

21
Énorme bloc d'instructions if-else, chacune avec un retour? Ce n'est rien. Une telle chose est généralement facilement refactorisée. (au moins la variation de sortie unique la plus courante avec une variable de résultat est, donc je doute que la variation de sortie multiple soit plus difficile.) Si vous voulez un vrai casse-tête, regardez une combinaison de boucles if-else et while (contrôlées par local booléens), où les variables de résultat sont définies, conduisant à un point de sortie final à la fin de la méthode. C'est l'idée de sortie unique devenue folle, et oui, je parle de l'expérience réelle ayant dû y faire face.
Marcus Andrén

7
'Imaginez: vous devez faire la méthode "IncreaseStuffCallCounter" à la fin de la fonction' DoStuff '. Que ferez-vous dans ce cas? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Jim Balter

355

Personne n'a mentionné ou cité Code Complete, alors je vais le faire.

17.1 retour

Minimisez le nombre de retours dans chaque routine . Il est plus difficile de comprendre une routine si, en la lisant en bas, vous n'êtes pas conscient de la possibilité qu'elle soit revenue quelque part au-dessus.

Utilisez un retour lorsqu'il améliore la lisibilité . Dans certaines routines, une fois que vous connaissez la réponse, vous souhaitez la retourner immédiatement à la routine d'appel. Si la routine est définie de telle manière qu'elle ne nécessite aucun nettoyage, ne pas retourner immédiatement signifie que vous devez écrire plus de code.


64
+1 pour la nuance de "minimiser" mais sans interdire les retours multiples.
Raedwald

13
"plus difficile à comprendre" est très subjectif, en particulier lorsque l'auteur ne fournit aucune preuve empirique pour étayer l'allégation générale ... ayant une seule variable qui doit être définie dans de nombreux emplacements conditionnels dans le code pour la déclaration de retour finale est également soumise à "ignorant la possibilité que la variable ait été affectée quelque part au-dessus de sa fonction!
Heston T. Holtmann

26
Personnellement, je préfère revenir tôt si possible. Pourquoi? Eh bien, lorsque vous voyez le mot-clé de retour pour un cas donné, vous savez instantanément "J'ai terminé" - vous n'avez pas besoin de continuer à lire pour savoir ce qui se passe, le cas échéant.
Mark Simpson

12
@ HestonT.Holtmann: Ce qui a rendu Code Complete unique parmi les livres de programmation, c'est que les conseils sont étayés par des preuves empiriques.
Adrian McCarthy

9
Cela devrait probablement être la réponse acceptée, car elle mentionne que le fait d'avoir plusieurs points de retour n'est pas toujours bon, mais parfois nécessaire.
Rafid

229

Je dirais qu'il serait incroyablement imprudent de décider arbitrairement contre plusieurs points de sortie car j'ai trouvé la technique utile dans la pratique à maintes reprises , en fait, j'ai souvent refactorisé le code existant en plusieurs points de sortie pour plus de clarté. On peut ainsi comparer les deux approches: -

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Comparez cela au code où plusieurs points de sortie sont autorisés: -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Je pense que ce dernier est beaucoup plus clair. Autant que je sache, la critique de plusieurs points de sortie est un point de vue plutôt archaïque de nos jours.


12
La clarté est dans l'œil du spectateur - je regarde une fonction et cherche un début un milieu et une fin. Lorsque la fonction est petite, c'est bien - mais lorsque vous essayez de comprendre pourquoi quelque chose est cassé et que "Rest of Code" s'avère non trivial, vous pouvez passer des heures à chercher pourquoi ret est
Murph

7
Tout d'abord, c'est un exemple artificiel. Deuxièmement, où va: chaîne ret; "aller dans la deuxième version? Troisièmement, Ret ne contient aucune information utile. Quatrièmement, pourquoi tant de logique dans une fonction / méthode? Cinquièmement, pourquoi ne pas séparer DidValuesPass (type res) puis RestOfCode () séparément ? sous - fonctions
Rick Minerich

25
@Rick 1. Pas artificiel d'après mon expérience, c'est en fait un modèle que j'ai rencontré plusieurs fois, 2. Il est attribué dans le «reste du code», ce n'est peut-être pas clair. 3. Hum? C'est un exemple? 4. Eh bien, je suppose que c'est artificiel à cet égard, mais il est possible de justifier cela, 5. pourrait faire ...
ljs

5
@Rick, le fait est qu'il est souvent plus facile de revenir tôt que d'envelopper le code dans une énorme instruction if. Cela revient beaucoup dans mon expérience, même avec une refactorisation appropriée.
ljs

5
Le point de ne pas avoir plusieurs instructions de retour est de faciliter le débogage, une seconde loin pour la lisibilité. Un point d'arrêt à la fin vous permet de voir la valeur de sortie, sans exception. Quant à la lisibilité, les 2 fonctions présentées ne se comportent pas de la même manière. Le retour par défaut est une chaîne vide si! R.Passed mais celui "plus lisible" change cela pour retourner null. L'auteur a mal lu qu'il y avait auparavant un défaut après seulement quelques lignes. Même dans des exemples triviaux, il est facile d'avoir des retours par défaut peu clairs, quelque chose qu'un seul retour à la fin aide à appliquer.
Mitch

191

Je travaille actuellement sur une base de code où deux des personnes qui y travaillent souscrivent aveuglément à la théorie du «point de sortie unique» et je peux vous dire que par expérience, c'est une horrible pratique horrible. Cela rend le code extrêmement difficile à maintenir et je vais vous montrer pourquoi.

Avec la théorie du «point de sortie unique», vous vous retrouvez inévitablement avec du code qui ressemble à ceci:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

Non seulement cela rend le code très difficile à suivre, mais dites maintenant plus tard que vous devez revenir en arrière et ajouter une opération entre 1 et 2. Vous devez mettre en retrait à peu près toute la fonction de panique, et bonne chance pour vous assurer que tout vos conditions et accolades if / else sont correctement mises en correspondance.

Cette méthode rend la maintenance du code extrêmement difficile et sujette aux erreurs.


5
@ Murph: Vous n'avez aucun moyen de savoir que rien d'autre ne se produit après chaque condition sans avoir lu chacune d'elles. Normalement, je dirais que ces types de sujets sont subjectifs, mais c'est clairement faux. Avec un retour sur chaque erreur, vous avez terminé, vous savez exactement ce qui s'est passé.
GEOCHET

6
@ Murph: J'ai vu ce type de code utilisé, abusé et surutilisé. L'exemple est assez simple, car il n'y a pas de vrai if / else à l'intérieur. Tout ce que ce type de code doit décomposer est un "autre" oublié. AFAIK, ce code a vraiment besoin d'exceptions.
paercebal

15
Vous pouvez le refactoriser en ceci, conserve sa "pureté": if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (error! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (error! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } //etc.
Joe Pineda

6
Ce code n'a pas qu'un seul point de sortie: chacune de ces instructions "error =" se trouve sur un chemin de sortie. Il ne s'agit pas seulement de quitter des fonctions, il s'agit de quitter n'importe quel bloc ou séquence.
Jay Bazuzi

6
Je ne suis pas d'accord pour dire que le retour unique entraîne «inévitablement» une imbrication profonde. Votre exemple peut être écrit comme une fonction simple et linéaire avec un seul retour (sans gotos). Et si vous ne comptez pas ou ne pouvez pas compter exclusivement sur RAII pour la gestion des ressources, les retours précoces finissent par provoquer des fuites ou du code en double. Plus important encore, les retours précoces ne permettent pas de faire valoir des post-conditions.
Adrian McCarthy

72

La programmation structurée indique que vous ne devriez avoir qu'une seule instruction de retour par fonction. C'est pour limiter la complexité. Beaucoup de gens comme Martin Fowler soutiennent qu'il est plus simple d'écrire des fonctions avec plusieurs déclarations de retour. Il présente cet argument dans le livre de refactorisation classique qu'il a écrit. Cela fonctionne bien si vous suivez ses autres conseils et écrivez de petites fonctions. Je suis d'accord avec ce point de vue et seuls les puristes de la programmation structurée stricte adhèrent à des déclarations de retour uniques par fonction.


44
La programmation structurée ne dit rien. Certaines personnes (mais pas toutes) qui se décrivent comme des défenseurs d'une programmation structurée le disent.
wnoise

15
"Cela fonctionne bien si vous suivez ses autres conseils et écrivez de petites fonctions." C'est le point le plus important. Les petites fonctions nécessitent rarement de nombreux points de retour.

6
@wnoise +1 pour le commentaire, c'est vrai. Toute la "programmation structurée" dit qu'il ne faut pas utiliser GOTO.
paxos1977

1
@ceretullis: à moins que ce ne soit nécessaire. Bien sûr, ce n'est pas essentiel, mais peut être utile en C. Le noyau Linux l'utilise, et pour de bonnes raisons. GOTO considère que Harmful a parlé de l'utilisation GOTOpour déplacer le flux de contrôle même lorsque la fonction existe. Il ne dit jamais "ne jamais utiliser GOTO".
Esteban Küber

1
"il s'agit d'ignorer la 'structure' du code." - Non, bien au contraire. "il est logique de dire qu'ils devraient être évités" - non, ce n'est pas le cas.
Jim Balter

62

Comme Kent Beck le note lors de l'examen des clauses de garde dans les modèles d'implémentation, la création d'une routine a un point d'entrée et de sortie unique ...

"était d'empêcher la confusion possible en sautant dans et hors de nombreux endroits dans la même routine. Cela avait du bon sens lorsqu'il était appliqué à FORTRAN ou à des programmes en langage assembleur écrits avec beaucoup de données globales où même comprendre quelles instructions étaient exécutées était un travail difficile." . avec de petites méthodes et surtout des données locales, il est inutilement conservateur. "

Je trouve une fonction écrite avec des clauses de garde beaucoup plus facile à suivre qu'un long groupe d' if then elseinstructions imbriquées .


Bien sûr, "un long groupe imbriqué d'instructions if-then-else" n'est pas la seule alternative aux clauses de garde.
Adrian McCarthy

@AdrianMcCarthy avez-vous une meilleure alternative? Ce serait plus utile que le sarcasme.
shinzou

@kuhaku: Je ne suis pas sûr d'appeler ça du sarcasme. La réponse suggère qu'il s'agit d'une situation soit / ou: soit des clauses de garde, soit de longues grappes imbriquées de if-then-else. De nombreux langages de programmation (la plupart?) Offrent de nombreuses façons de factoriser une telle logique en plus des clauses de garde.
Adrian McCarthy

61

Dans une fonction qui n'a pas d'effets secondaires, il n'y a aucune bonne raison d'avoir plus d'un seul retour et vous devez les écrire dans un style fonctionnel. Dans une méthode avec des effets secondaires, les choses sont plus séquentielles (indexées dans le temps), vous écrivez donc dans un style impératif, en utilisant l'instruction return comme commande pour arrêter l'exécution.

En d'autres termes, si possible, privilégiez ce style

return a > 0 ?
  positively(a):
  negatively(a);

sur ceci

if (a > 0)
  return positively(a);
else
  return negatively(a);

Si vous vous retrouvez en train d'écrire plusieurs couches de conditions imbriquées, il existe probablement un moyen de refactoriser cela, en utilisant la liste de prédicats par exemple. Si vous trouvez que vos ifs et elses sont très éloignés syntaxiquement, vous voudrez peut-être les décomposer en fonctions plus petites. Un bloc conditionnel qui s'étend sur plus d'un écran de texte est difficile à lire.

Il n'y a pas de règle stricte qui s'applique à toutes les langues. Quelque chose comme avoir une seule déclaration de retour ne rendra pas votre code bon. Mais un bon code aura tendance à vous permettre d'écrire vos fonctions de cette façon.


6
+1 "Si vous trouvez que vos ifs et elses sont très éloignés syntaxiquement, vous voudrez peut-être les décomposer en fonctions plus petites."
Andres Jaan Tack

4
+1, si cela pose problème, cela signifie généralement que vous en faites trop dans une seule fonction. Cela me déprime vraiment que ce ne soit pas la réponse la plus votée
Matt Briggs

1
Les déclarations des gardes n'ont pas non plus d'effets secondaires, mais la plupart des gens les jugent utiles. Il peut donc y avoir des raisons d'arrêter l'exécution tôt même s'il n'y a pas d'effets secondaires. Cette réponse ne résout pas complètement le problème à mon avis.
Maarten Bodewes

@ MaartenBodewes-owlstead Voir "Strictness and Laziness"
Apocalisp

43

Je l'ai vu dans les normes de codage pour C ++ qui étaient un blocage de C, comme si vous n'avez pas RAII ou une autre gestion automatique de la mémoire, vous devez nettoyer à chaque retour, ce qui signifie soit couper-coller du nettoyage ou d'un goto (logiquement le même que «finalement» dans les langages gérés), les deux étant considérés comme de mauvaise forme. Si vos pratiques consistent à utiliser des pointeurs intelligents et des collections en C ++ ou un autre système de mémoire automatique, il n'y a pas de raison valable pour cela, et tout devient question de lisibilité, et plus d'un appel de jugement.


Bien dit, même si je crois qu'il est préférable de copier les suppressions lorsque vous essayez d'écrire du code hautement optimisé (comme les maillages 3D complexes de skinning de logiciels!)
Grant Peters

1
Qu'est-ce qui vous fait croire cela? Si vous avez un compilateur avec une mauvaise optimisation où il y a des frais généraux dans le déréférencement de la auto_ptr, vous pouvez utiliser des pointeurs simples en parallèle. Bien qu'il serait étrange d'écrire du code «optimisé» avec un compilateur non optimisant en premier lieu.
Pete Kirkham

Cela constitue une exception intéressante à la règle: si votre langage de programmation ne contient pas quelque chose qui est automatiquement appelé à la fin de la méthode (comme try... finallyen Java) et que vous devez faire la maintenance des ressources, vous pouvez le faire avec un seul retour à la fin d'une méthode. Avant de faire cela, vous devriez sérieusement envisager de refactoriser le code pour vous débarrasser de la situation.
Maarten Bodewes

@PeteKirkham pourquoi goto pour le nettoyage est-il mauvais? oui goto peut être mal utilisé, mais cette utilisation particulière n'est pas mauvaise.
q126y

1
@ q126y en C ++, contrairement à RAII, il échoue lorsqu'une exception est levée. En C, c'est une pratique parfaitement valable. Voir stackoverflow.com/questions/379172/use-goto-or-not
Pete Kirkham

40

Je penche pour l'idée que les instructions de retour au milieu de la fonction sont mauvaises. Vous pouvez utiliser des retours pour construire quelques clauses de garde en haut de la fonction, et bien sûr dire au compilateur quoi retourner à la fin de la fonction sans problème, mais les retours au milieu de la fonction peuvent être facilement manqués et peuvent rendre la fonction plus difficile à interpréter.


38

Y a-t-il de bonnes raisons pour lesquelles il est préférable d'avoir une seule instruction de retour dans une fonction?

Oui , il y a:

  • Le point de sortie unique offre un excellent endroit pour affirmer vos post-conditions.
  • Il est souvent utile de pouvoir mettre un point d'arrêt du débogueur sur le seul retour à la fin de la fonction.
  • Moins de retours signifie moins de complexité. Le code linéaire est généralement plus simple à comprendre.
  • Si essayer de simplifier une fonction en un seul retour provoque de la complexité, c'est une incitation à refactoriser des fonctions plus petites, plus générales et plus faciles à comprendre.
  • Si vous êtes dans une langue sans destructeurs ou si vous n'utilisez pas RAII, un seul retour réduit le nombre d'emplacements à nettoyer.
  • Certaines langues nécessitent un seul point de sortie (par exemple, Pascal et Eiffel).

La question est souvent posée comme une fausse dichotomie entre plusieurs déclarations ou des déclarations si profondément imbriquées. Il y a presque toujours une troisième solution qui est très linéaire (pas d'imbrication profonde) avec un seul point de sortie.

Mise à jour : les directives MISRA semblent également promouvoir la sortie unique .

Pour être clair, je ne dis pas qu'il est toujours faux d'avoir plusieurs déclarations. Mais étant donné des solutions par ailleurs équivalentes, il existe de nombreuses bonnes raisons de préférer celle avec un seul retour.


2
une autre bonne raison, probablement la meilleure de nos jours, pour avoir une seule déclaration de retour est la journalisation. si vous souhaitez ajouter la journalisation à une méthode, vous pouvez placer une seule instruction log qui transmet ce que la méthode renvoie.
2013

La déclaration FORTRAN ENTRY était-elle courante? Voir docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . Et si vous êtes aventureux, vous pouvez enregistrer des méthodes avec AOP et après-conseil
Eric Jablow

1
+1 Les 2 premiers points ont suffi à me convaincre. Cela avec l'avant-dernier paragraphe. Je ne suis pas d'accord avec l'élément de journalisation pour la même raison que je décourage les conditions imbriquées profondes car elles encouragent la violation de la règle de responsabilité unique qui est la principale raison pour laquelle le polymorphisme a été introduit dans les POO.
Francis Rodgers

Je voudrais juste ajouter qu'avec C # et les contrats de code, le problème de post-condition n'est pas un problème, car vous pouvez toujours l'utiliser Contract.Ensuresavec plusieurs points de retour.
julealgon

1
@ q126y: Si vous utilisez gotopour accéder au code de nettoyage commun, vous avez probablement simplifié la fonction afin qu'il y en ait un returnà la fin du code de nettoyage. Vous pourriez donc dire que vous avez résolu le problème avec goto, mais je dirais que vous l'avez résolu en simplifiant à un seul return.
Adrian McCarthy

33

Avoir un seul point de sortie offre un avantage dans le débogage, car il vous permet de définir un seul point d'arrêt à la fin d'une fonction pour voir quelle valeur va réellement être renvoyée.


6
BRAVO! Vous êtes la seule personne à mentionner cette raison objective . C'est la raison pour laquelle je préfère les points de sortie uniques aux points de sortie multiples. Si mon débogueur pouvait définir un point d'arrêt sur n'importe quel point de sortie, je préférerais probablement plusieurs points de sortie. Mon opinion actuelle est que les personnes qui codent plusieurs points de sortie le font pour leur propre bénéfice au détriment des autres qui doivent utiliser un débogueur sur leur code (et oui je parle de tous vous contributeurs open-source qui écrivent du code avec plusieurs points de sortie.)
MikeSchinkel

3
OUI. J'ajoute du code de journalisation à un système qui se comporte de manière intermittente en production (où je ne peux pas passer à travers). Si le codeur précédent avait utilisé une sortie unique, cela aurait été BEAUCOUP plus facile.
Michael Blackburn

3
Il est vrai que le débogage est utile. Mais dans la pratique, j'ai pu dans la plupart des cas définir le point d'arrêt dans la fonction appelante, juste après l'appel - effectivement au même résultat. (Et cette position se trouve sur la pile des appels, bien sûr.) YMMV.
foo

C'est à moins que votre débogueur ne fournisse une fonction de sortie ou de retour d'étape (et chaque débogueur le fait pour autant que je sache), qui affiche la valeur retournée juste après son retour. La modification de la valeur par la suite peut être un peu délicate si elle n'est pas affectée à une variable.
Maarten Bodewes

7
Je n'ai pas vu de débogueur depuis longtemps qui ne vous permettrait pas de définir un point d'arrêt à la "fermeture" de la méthode (fin, accolade droite, quelle que soit votre langue) et d'atteindre ce point d'arrêt quel que soit l'endroit ou le nombre , les statemets de retour sont dans la méthode. De plus, même si votre fonction n'a qu'un seul retour, cela ne signifie pas que vous ne pouvez pas quitter la fonction avec une exception (explicite ou héritée). Donc, je pense que ce n'est pas vraiment un point valable.
Scott Gartner

19

En général, j'essaie de n'avoir qu'un seul point de sortie d'une fonction. Il arrive cependant que cela finisse par créer un corps de fonction plus complexe que nécessaire, auquel cas il est préférable d'avoir plusieurs points de sortie. Il doit vraiment s'agir d'un "appel au jugement" basé sur la complexité qui en résulte, mais l'objectif doit être le moins de points de sortie possible sans sacrifier la complexité et la compréhensibilité.


"En général, j'essaie de n'avoir qu'un seul point de sortie d'une fonction" - pourquoi? "l'objectif devrait être le moins de points de sortie possible" - pourquoi? Et pourquoi 19 personnes ont-elles voté contre cette non-réponse?
Jim Balter

@JimBalter En fin de compte, cela se résume à vos préférences personnelles. Plus de points de sortie conduisent généralement à une méthode plus complexe (mais pas toujours) et la rendent plus difficile à comprendre pour quelqu'un.
Scott Dorman

"cela se résume à une préférence personnelle." - En d'autres termes, vous ne pouvez pas donner de raison. "Plus de points de sortie conduisent généralement à une méthode plus complexe (mais pas toujours)" - Non, en fait, ils ne le font pas. Étant donné deux fonctions qui sont logiquement équivalentes, une avec des clauses de garde et une avec une seule sortie, cette dernière aura une complexité cyclomatique plus élevée, ce que de nombreuses études montrent que le code est plus sujet aux erreurs et difficile à comprendre. Vous auriez avantage à lire les autres réponses ici.
Jim Balter

14

Non, parce que nous ne vivons plus dans les années 1970 . Si votre fonction est suffisamment longue pour que plusieurs retours soient un problème, elle est trop longue.

(Indépendamment du fait que toute fonction multiligne dans une langue avec des exceptions aura de toute façon plusieurs points de sortie.)


14

Ma préférence serait pour une sortie unique à moins que cela ne complique vraiment les choses. J'ai constaté que dans certains cas, plusieurs points existants peuvent masquer d'autres problèmes de conception plus importants:

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

En voyant ce code, je demanderais immédiatement:

  • 'Foo' est-il jamais nul?
  • Si oui, combien de clients de «DoStuff» appellent la fonction avec un «foo» nul?

Selon les réponses à ces questions, il se pourrait que

  1. la vérification est inutile car elle n'est jamais vraie (c'est-à-dire qu'elle devrait être une assertion)
  2. la vérification est très rarement vraie et il peut donc être préférable de modifier ces fonctions spécifiques de l'appelant car elles devraient probablement prendre une autre mesure de toute façon.

Dans les deux cas ci-dessus, le code peut probablement être retravaillé avec une assertion pour s'assurer que «foo» n'est jamais nul et que les appelants concernés ont changé.

Il y a deux autres raisons (spécifiques je pense au code C ++) où plusieurs existent peuvent avoir un effet négatif . Ce sont la taille du code et les optimisations du compilateur.

Un objet non POD C ++ dans la portée à la sortie d'une fonction aura son destructeur appelé. Lorsqu'il y a plusieurs instructions de retour, il se peut qu'il y ait différents objets dans la portée et donc la liste des destructeurs à appeler sera différente. Le compilateur doit donc générer du code pour chaque instruction de retour:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

Si la taille du code est un problème - cela peut être quelque chose à éviter.

L'autre problème concerne "l'optimisation de la valeur de retour nommée" (alias Copy Elision, ISO C ++ '03 12.8 / 15). C ++ permet à une implémentation d'ignorer l'appel du constructeur de copie s'il peut:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

En prenant simplement le code tel quel, l'objet 'a1' est construit dans 'foo' et ensuite sa construction de copie sera appelée pour construire 'a2'. Cependant, la copie élision permet au compilateur de construire «a1» au même endroit sur la pile que «a2». Il n'est donc pas nécessaire de "copier" l'objet lorsque la fonction revient.

Plusieurs points de sortie compliquent le travail du compilateur pour essayer de détecter cela, et au moins pour une version relativement récente de VC ++, l'optimisation n'a pas eu lieu lorsque le corps de la fonction avait plusieurs retours. Voir Optimisation de la valeur de retour nommée dans Visual C ++ 2005 pour plus de détails.


1
Si vous supprimez tout sauf le dernier dtor de votre exemple C ++, le code pour détruire B et plus tard C et B devra toujours être généré lorsque la portée de l'instruction if se terminera, donc vous ne gagnerez vraiment rien en n'ayant pas plusieurs retours .
Eclipse

4
+1 Et waaaaay en bas de la liste, nous avons la VRAIE raison pour laquelle cette pratique de codage existe - NRVO. Il s'agit cependant d'une micro-optimisation; et, comme toutes les pratiques de micro-optimisation, a probablement été lancée par un "expert" de 50 ans qui est habitué à programmer sur un PDP-8 à 300 kHz et ne comprend pas l'importance d'un code propre et structuré. En général, suivez les conseils de Chris S et utilisez plusieurs déclarations de retour chaque fois que cela est logique.
BlueRaja - Danny Pflughoeft

Bien que je ne sois pas d'accord avec votre préférence (dans mon esprit, votre suggestion d'affirmation est également un point de retour, comme c'est le cas throw new ArgumentNullException()en C # dans ce cas), j'ai vraiment aimé vos autres considérations, elles sont toutes valables pour moi et pourraient être critiques dans certains contextes de niche.
julealgon

C'est plein à craquer de paillassons. La question de savoir pourquoi fooest testé n'a rien à voir avec le sujet, qui est de savoir s'il faut le faire if (foo == NULL) return; dowork; ouif (foo != NULL) { dowork; }
Jim Balter

11

Avoir un seul point de sortie réduit la complexité cyclomatique et donc, en théorie , réduit la probabilité que vous introduisiez des bogues dans votre code lorsque vous le modifiez. Cependant, la pratique tend à suggérer qu'une approche plus pragmatique est nécessaire. J'ai donc tendance à viser à avoir un seul point de sortie, mais autoriser mon code à en avoir plusieurs si c'est plus lisible.


Très perspicace. Bien que je pense que jusqu'à ce qu'un programmeur sache quand utiliser plusieurs points de sortie, ils devraient être limités à un.
Rick Minerich

5
Pas vraiment. La complexité cyclomatique de "if (...) return; ... return;" est identique à "if (...) {...} return;". Ils ont tous deux deux chemins à travers eux.
Steve Emmerson

11

Je m'oblige à n'utiliser qu'une seule returninstruction, car cela va en quelque sorte générer une odeur de code. Laisse-moi expliquer:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(Les conditions sont arbritaires ...)

Plus il y a de conditions, plus la fonction est grande, plus elle est difficile à lire. Donc, si vous êtes à l'écoute de l'odeur du code, vous vous en rendrez compte et voudrez refactoriser le code. Deux solutions possibles sont:

  • Retours multiples
  • Refactorisation en fonctions distinctes

Retours multiples

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Fonctions séparées

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

Certes, c'est plus long et un peu désordonné, mais dans le processus de refactorisation de la fonction de cette façon, nous avons

  • créé un certain nombre de fonctions réutilisables,
  • rendu la fonction plus lisible par l'homme, et
  • les fonctions se concentrent sur la raison pour laquelle les valeurs sont correctes.

5
-1: mauvais exemple. Vous avez omis la gestion des messages d'erreur. Si cela n'était pas nécessaire, l'isCorrect pourrait être exprimé simplement comme return xx && yy && zz; où xx, yy et z sont les expressions isEqual, isDouble et isThird.
kauppi

10

Je dirais que vous devriez en avoir autant que nécessaire, ou tout ce qui rend le code plus propre (comme les clauses de garde ).

Personnellement, je n'ai jamais entendu / vu de "meilleures pratiques" disant que vous ne devriez avoir qu'une seule déclaration de retour.

Pour la plupart, j'ai tendance à quitter une fonction dès que possible sur la base d'un chemin logique (les clauses de garde en sont un excellent exemple).


10

Je crois que les retours multiples sont généralement bons (dans le code que j'écris en C #). Le style à retour unique est un maintien de C. Mais vous ne codez probablement pas en C.

Il n'y a pas de loi exigeant un seul point de sortie pour une méthode dans tous les langages de programmation . Certaines personnes insistent sur la supériorité de ce style, et parfois elles l'élèvent à une «règle» ou «loi», mais cette croyance n'est étayée par aucune preuve ou recherche.

Plus d'un style de retour peut être une mauvaise habitude dans le code C, où les ressources doivent être explicitement désallouées, mais des langages tels que Java, C #, Python ou JavaScript qui ont des constructions telles que le garbage collection automatique et les try..finallyblocs (et les usingblocs en C # ), et cet argument ne s'applique pas - dans ces langues, il est très rare d'avoir besoin d'une désallocation manuelle centralisée des ressources.

Il y a des cas où un seul retour est plus lisible et des cas où il ne l'est pas. Voyez si cela réduit le nombre de lignes de code, rend la logique plus claire ou réduit le nombre d'accolades et de retraits ou de variables temporaires.

Par conséquent, utilisez autant de retours que vous le souhaitez, car il s'agit d'un problème de mise en page et de lisibilité, pas technique.

J'en ai parlé plus longuement sur mon blog .


10

Il y a de bonnes choses à dire sur le fait d'avoir un seul point de sortie, tout comme il y a de mauvaises choses à dire sur l'inévitable programmation "en flèche" qui en résulte.

Si j'utilise plusieurs points de sortie lors de la validation des entrées ou de l'allocation des ressources, j'essaie de mettre très visiblement toutes les «sorties d'erreur» en haut de la fonction.

L' article sur la programmation spartiate de "SSDSLPedia" et l' article sur le point de sortie de fonction unique du "Wiki de Portland Pattern Repository" contiennent des arguments perspicaces à ce sujet. Bien sûr, il y a aussi ce post à considérer.

Si vous voulez vraiment un seul point de sortie (dans n'importe quelle langue sans exception) par exemple afin de libérer des ressources en un seul endroit, je trouve que l'application soignée de goto est bonne; voir par exemple cet exemple plutôt artificiel (compressé pour économiser l'écran immobilier):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

Personnellement, en général, je n'aime pas plus la programmation des flèches que je n'aime plusieurs points de sortie, bien que les deux soient utiles lorsqu'ils sont appliqués correctement. Le mieux, bien sûr, est de structurer votre programme pour ne demander ni l'un ni l'autre. Décomposer votre fonction en plusieurs morceaux aide généralement :)

Bien que ce faisant, je trouve que je me retrouve de toute façon avec plusieurs points de sortie comme dans cet exemple, où une fonction plus grande a été décomposée en plusieurs fonctions plus petites:

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

Selon le projet ou les directives de codage, la plupart du code de la plaque de la chaudière pourrait être remplacé par des macros. En remarque, le décomposer de cette façon rend les fonctions g0, g1, g2 très faciles à tester individuellement.

De toute évidence, dans un langage OO et activé par exception, je n'utiliserais pas des instructions if comme ça (ou pas du tout, si je pouvais m'en tirer avec assez peu d'effort), et le code serait beaucoup plus simple. Et non fléché. Et la plupart des retours non définitifs seraient probablement des exceptions.

En bref;

  • Peu de retours valent mieux que de nombreux retours
  • Plus d'un retour vaut mieux que d'énormes flèches, et les clauses de garde sont généralement correctes.
  • Des exceptions pourraient / devraient probablement remplacer la plupart des «clauses de garde» lorsque cela est possible.

L'exemple se bloque pour y <0, car il essaie de libérer le pointeur NULL ;-)
Erich Kitzmueller

2
opengroup.org/onlinepubs/009695399/functions/free.html "Si ptr est un pointeur nul, aucune action ne doit se produire."
Henrik Gustafsson le

1
Non, il ne plantera pas, car passer NULL à free est un no-op défini. C'est une idée fausse très courante que vous devez d'abord tester pour NULL.
hlovdal

Le motif en "flèche" n'est pas une alternative inévitable. C'est une fausse dichotomie.
Adrian McCarthy

9

Vous connaissez l'adage - la beauté est aux yeux du spectateur .

Certaines personnes ne jurent que par NetBeans et certaines par IntelliJ IDEA , certaines par Python et d'autres par PHP .

Dans certains magasins, vous pourriez perdre votre emploi si vous insistez pour le faire:

public void hello()
{
   if (....)
   {
      ....
   }
}

La question concerne la visibilité et la maintenabilité.

Je suis accro à l'utilisation de l'algèbre booléenne pour réduire et simplifier la logique et l'utilisation des machines à états. Cependant, certains collègues ont estimé que mon emploi des "techniques mathématiques" dans le codage n'était pas approprié, car il ne serait ni visible ni maintenable. Et ce serait une mauvaise pratique. Désolé les gens, les techniques que j'emploie sont très visibles et maintenables pour moi - parce que lorsque je reviens au code six mois plus tard, je comprendrais clairement le code plutôt que de voir un gâchis de spaghettis proverbiaux.

Hé mon pote (comme disait un ancien client) fais ce que tu veux tant que tu sais comment le réparer quand j'ai besoin de toi pour le réparer.

Je me souviens il y a 20 ans, un de mes collègues a été licencié pour avoir utilisé ce que l'on appelle aujourd'hui une stratégie de développement agile . Il avait un plan progressif méticuleux. Mais son manager lui criait: «Vous ne pouvez pas libérer progressivement les fonctionnalités pour les utilisateurs! Vous devez vous en tenir à la cascade ." Sa réponse au directeur était que le développement progressif serait plus précis aux besoins du client. Il croyait au développement pour les besoins des clients, mais le directeur croyait au codage selon «l'exigence du client».

Nous sommes fréquemment coupables d'avoir enfreint la normalisation des données, les limites MVP et MVC . Nous inline au lieu de construire une fonction. Nous prenons des raccourcis.

Personnellement, je pense que PHP est une mauvaise pratique, mais que sais-je? Tous les arguments théoriques se résument à essayer de respecter un ensemble de règles

qualité = précision, maintenabilité et rentabilité.

Toutes les autres règles disparaissent en arrière-plan. Et bien sûr, cette règle ne s'estompe jamais:

La paresse est la vertu d'un bon programmeur.


1
"Hé mon pote (comme disait un ancien client) fais ce que tu veux tant que tu sais comment le réparer quand j'ai besoin que tu le répares." Problème: ce n'est souvent pas vous qui le «corrigez».
Dan Barron

+1 sur cette réponse parce que je suis d'accord avec où vous allez mais pas nécessairement comment vous y arrivez. Je dirais qu'il existe des niveaux de compréhension. c'est-à-dire que la compréhension que l'employé A a après 5 ans d'expérience en programmation et 5 ans dans une entreprise est très différente de celle de l'employé B, un nouveau diplômé d'université qui vient de commencer avec une entreprise. Mon point serait que si l'employé A est la seule personne qui peut comprendre le code, il n'est PAS maintenable et nous devrions donc tous nous efforcer d'écrire du code que l'employé B peut comprendre. C'est là que le véritable art est dans le logiciel.
Francis Rodgers

9

Je penche pour l'utilisation de clauses de garde pour revenir tôt et sinon quitter à la fin d'une méthode. La règle d'entrée et de sortie unique a une signification historique et était particulièrement utile lorsqu'il s'agissait de code hérité qui comptait 10 pages A4 pour une seule méthode C ++ avec plusieurs retours (et de nombreux défauts). Plus récemment, la bonne pratique acceptée consiste à conserver des méthodes réduites, ce qui rend les sorties multiples moins gênantes pour la compréhension. Dans l'exemple Kronoz suivant copié ci-dessus, la question est de savoir ce qui se passe dans // Reste de code ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Je me rends compte que l'exemple est quelque peu artificiel, mais je serais tenté de refactoriser le boucle foreach dans une instruction LINQ qui pourrait alors être considérée comme une clause de garde. Encore une fois, dans un exemple artificiel, l'intention du code n'est pas apparente et someFunction () peut avoir un autre effet secondaire ou le résultat peut être utilisé dans le // reste du code ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Donner la fonction refactorisée suivante:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}

Le C ++ n'a-t-il pas d'exceptions? Alors pourquoi voudriez-vous revenir nullau lieu de lever une exception indiquant que l'argument n'est pas accepté?
Maarten Bodewes

1
Comme je l'ai indiqué, l'exemple de code est copié à partir d'une réponse précédente ( stackoverflow.com/a/36729/132599 ). L'exemple d'origine a renvoyé des valeurs nulles et le refactoring pour lever des exceptions d'argument n'était pas significatif au point que j'essayais de faire ou à la question d'origine. Par bonne pratique, alors oui, je lancerais normalement (en C #) une ArgumentNullException dans une clause guard plutôt que de retourner une valeur nulle.
David Clarke

7

Une bonne raison à laquelle je peux penser est pour la maintenance du code: vous avez un seul point de sortie. Si vous voulez changer le format du résultat, ..., c'est juste beaucoup plus simple à implémenter. De plus, pour le débogage, vous pouvez simplement y coller un point d'arrêt :)

Cela dit, j'ai dû une fois travailler dans une bibliothèque où les normes de codage imposaient «une déclaration de retour par fonction», et j'ai trouvé cela assez difficile. J'écris beaucoup de code de calcul numérique, et il y a souvent des 'cas spéciaux', donc le code a fini par être assez difficile à suivre ...


Cela ne fait pas vraiment de différence. Si vous modifiez le type de la variable locale renvoyée, vous devrez alors corriger toutes les affectations de cette variable locale. Et il est probablement préférable de définir une méthode avec une signature différente de toute façon, car vous devrez également corriger tous les appels de méthode.
Maarten Bodewes

@ MaartenBodewes-owlstead - Cela peut faire la différence. Pour ne nommer que deux exemples où vous n'auriez pas à corriger toutes les affectations de la variable locale ou à modifier les appels de méthode, la fonction peut renvoyer une date sous forme de chaîne (la variable locale serait une date réelle, formatée sous forme de chaîne uniquement à le dernier moment), ou il peut retourner un nombre décimal et vous souhaitez modifier le nombre de décimales.
nnnnnn

@nnnnnn OK, si vous voulez faire du post-traitement sur la sortie ... Mais je voudrais simplement générer une nouvelle méthode qui fait le post-traitement et laisser l'ancienne seule. Ce n'est que légèrement plus difficile à refactoriser, mais vous devrez quand même vérifier si les autres appels sont compatibles avec le nouveau format. Mais c'est néanmoins une raison valable.
Maarten Bodewes

7

Plusieurs points de sortie conviennent pour des fonctions suffisamment petites - c'est-à-dire une fonction qui peut être visualisée sur une seule longueur d'écran sur son intégralité. Si une fonction longue comprend également plusieurs points de sortie, c'est un signe que la fonction peut être hachée davantage.

Cela dit, j'évite les fonctions à sorties multiples, sauf si cela est absolument nécessaire . J'ai ressenti la douleur de bugs qui sont dus à un retour errant dans une ligne obscure dans des fonctions plus complexes.


6

J'ai travaillé avec des normes de codage terribles qui vous ont imposé un seul chemin de sortie et le résultat est presque toujours des spaghettis non structurés si la fonction est tout sauf triviale - vous vous retrouvez avec beaucoup de pauses et continue qui ne fait qu'empêcher.


Sans oublier d'avoir à dire à votre esprit de sauter la ifdéclaration devant chaque appel de méthode qui renvoie le succès ou non :(
Maarten Bodewes

6

Un point de sortie unique - toutes choses égales par ailleurs - rend le code beaucoup plus lisible. Mais il y a un hic: la construction populaire

resulttype res;
if if if...
return res;

est un faux, "res =" n'est pas beaucoup mieux que "return". Il a une déclaration de retour unique, mais plusieurs points où la fonction se termine réellement.

Si vous avez une fonction avec plusieurs retours (ou "res =" s), c'est souvent une bonne idée de la diviser en plusieurs fonctions plus petites avec un seul point de sortie.


6

Ma politique habituelle est de n'avoir qu'une seule instruction de retour à la fin d'une fonction, à moins que la complexité du code ne soit considérablement réduite en ajoutant plus. En fait, je suis plutôt un fan d'Eiffel, qui applique la seule règle de retour en n'ayant pas de déclaration de retour (il y a juste une variable 'result' créée automatiquement pour mettre votre résultat).

Il y a certainement des cas où le code peut être rendu plus clair avec plusieurs retours que la version évidente sans eux. On pourrait dire que plus de retouches sont nécessaires si vous avez une fonction trop complexe pour être compréhensible sans plusieurs déclarations de retour, mais parfois il est bon d'être pragmatique à propos de telles choses.


5

Si vous vous retrouvez avec plus de quelques retours, il peut y avoir un problème avec votre code. Sinon, je conviens que parfois, il est agréable de pouvoir revenir de plusieurs endroits dans un sous-programme, surtout quand il rend le code plus propre.

Perl 6: mauvais exemple

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

serait mieux écrit comme ça

Perl 6: bon exemple

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Notez que ce n'était qu'un exemple rapide


Ok Pourquoi cela a-t-il été rejeté? Ce n'est pas comme si ce n'était pas une opinion.
Brad Gilbert

5

Je vote pour le retour unique à la fin comme ligne directrice. Cela permet une gestion courante du nettoyage du code ... Par exemple, jetez un œil au code suivant ...

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}

Vous votez pour un seul retour - en code C. Mais que se passe-t-il si vous codiez dans une langue qui avait la collecte des ordures et essayez .. enfin des blocs?
Anthony

4

C'est probablement une perspective inhabituelle, mais je pense que quiconque croit que plusieurs déclarations de retour sont à privilégier n'a jamais eu à utiliser un débogueur sur un microprocesseur qui ne prend en charge que 4 points d'arrêt matériels. ;-)

Bien que les problèmes de "code de flèche" soient complètement corrects, un problème qui semble disparaître lors de l'utilisation de plusieurs instructions de retour se trouve dans la situation où vous utilisez un débogueur. Vous n'avez pas de position fourre-tout pratique pour mettre un point d'arrêt pour garantir que vous allez voir la sortie et donc la condition de retour.


5
C'est juste une sorte d'optimisation prématurée différente. Vous ne devriez jamais optimiser pour le cas spécial. Si vous vous retrouvez souvent à déboguer une section spécifique de code, il y a plus de mal à cela que le nombre de points de sortie qu'il contient.
Wedge

Cela dépend aussi de votre débogueur.
Maarten Bodewes

4

Plus vous avez d'instructions de retour dans une fonction, plus la complexité de cette méthode est élevée. Si vous vous demandez si vous avez trop de déclarations de retour, vous voudrez peut-être vous demander si vous avez trop de lignes de code dans cette fonction.

Mais, non, il n'y a rien de mal avec une / plusieurs déclarations de retour. Dans certains langages, c'est une meilleure pratique (C ++) que dans d'autres (C).

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.