Pourquoi une fonction devrait-elle n'avoir qu'un seul point de sortie? [fermé]

97

J'ai toujours entendu parler d'une fonction de point de sortie unique comme une mauvaise façon de coder parce que vous perdez en lisibilité et en efficacité. Je n'ai jamais entendu personne discuter de l'autre côté.

Je pensais que cela avait quelque chose à voir avec CS, mais cette question a été posée à cstheory stackexchange.

hexagone bob-omb
la source
6
La réponse est qu'il n'y a pas de réponse qui soit toujours juste. Je trouve souvent plus facile de coder avec plusieurs sorties. J'ai également constaté (lors de la mise à jour du code ci-dessus) que la modification / l'extension du code était plus difficile en raison de ces mêmes sorties multiples. Prendre ces décisions au cas par cas est notre travail. Lorsqu'une décision a toujours une «meilleure» réponse, nous n'avons pas besoin de nous.
JS.
1
@finnw les mods fascistes ont supprimé les deux dernières questions, pour s'assurer qu'elles devront être répondues encore et encore, et encore
Maarten Bodewes
Malgré le mot «argumenter» dans la question, je ne pense vraiment pas que ce soit une question basée sur l'opinion. C'est tout à fait pertinent pour un bon design, etc. Je ne vois aucune raison pour qu'il soit fermé, mais w / e.
Ungeheuer
1
Un point de sortie unique simplifie le débogage, la lecture, la mesure et le réglage des performances, la refactorisation. Ceci est objectif et matériellement significatif. L'utilisation de retours anticipés (après de simples vérifications d'arguments) permet un mélange judicieux des deux styles. Étant donné les avantages d'un point de sortie unique, joncher votre code avec des valeurs de retour est simplement la preuve d'un programmeur paresseux, négligent et imprudent - et au moins peut-être n'aime pas les chiots.
Rick O'Shea

Réponses:

108

Il existe différentes écoles de pensée, et cela dépend en grande partie des préférences personnelles.

La première est qu'il est moins déroutant s'il n'y a qu'un seul point de sortie - vous avez un seul chemin à travers la méthode et vous savez où chercher la sortie. Du côté négatif, si vous utilisez l'indentation pour représenter l'imbrication, votre code finit par être massivement indenté vers la droite et il devient très difficile de suivre toutes les portées imbriquées.

Une autre est que vous pouvez vérifier les conditions préalables et quitter tôt au début d'une méthode, de sorte que vous sachiez dans le corps de la méthode que certaines conditions sont vraies, sans que le corps entier de la méthode soit en retrait de 5 miles vers la droite. Cela minimise généralement le nombre d'étendues dont vous devez vous soucier, ce qui rend le code beaucoup plus facile à suivre.

Un troisième est que vous pouvez quitter n'importe où. Auparavant, c'était plus déroutant, mais maintenant que nous avons des éditeurs et des compilateurs de coloration syntaxique qui détectent le code inaccessible, c'est beaucoup plus facile à gérer.

Je suis carrément dans le camp du milieu. Appliquer un point de sortie unique est une restriction inutile ou même contre-productive à mon humble avis, alors que quitter au hasard dans une méthode peut parfois conduire à une logique compliquée difficile à suivre, où il devient difficile de voir si un morceau de code donné sera ou ne sera pas réalisé. Mais "gating" votre méthode permet de simplifier considérablement le corps de la méthode.

Jason Williams
la source
1
L'imbrication profonde peut être évitée dans le singe exitparadigme à force d' go toénoncés. De plus, on a la possibilité d'effectuer un post-traitement sous l' Errorétiquette locale de la fonction , ce qui est impossible avec plusieurs returns.
Ant_222
2
Il existe généralement une bonne solution qui évite d'avoir à passer à. Je préfère de loin 'return (Fail (...))' et mettre le code de nettoyage partagé dans la méthode Fail. Cela peut nécessiter de passer quelques locaux pour permettre la libération de la mémoire, etc., mais à moins que vous ne soyez dans un morceau de code critique pour les performances, il s'agit généralement d'une solution beaucoup plus propre qu'un goto IMO. Il permet également à plusieurs méthodes de partager un code de nettoyage similaire.
Jason Williams
Il existe une approche optimale basée sur des critères objectifs, mais nous pouvons convenir qu'il existe des écoles de pensée (correctes et incorrectes) et cela revient à une préférence personnelle (une préférence pour ou contre une approche correcte).
Rick O'Shea
39

Ma recommandation générale est que les déclarations de retour devraient, lorsque cela est possible, être situées avant le premier code qui a des effets secondaires, ou après le dernier code qui a des effets secondaires. Je considérerais quelque chose comme:

  if (! argument) // Vérifie si non nul
    return ERR_NULL_ARGUMENT;
  ... traiter un argument non nul
  si (ok)
    return 0;
  autre
    return ERR_NOT_OK;

plus clair que:

  int return_value;
  if (argument) // Non nul
  {
    .. traiter un argument non nul
    .. définir le résultat de manière appropriée
  }
  autre
    résultat = ERR_NULL_ARGUMENT;
  résultat de retour;

Si une certaine condition devait empêcher une fonction de faire quoi que ce soit, je préfère revenir tôt hors de la fonction à un endroit au-dessus du point où la fonction ferait quelque chose. Une fois que la fonction a entrepris des actions avec des effets secondaires, je préfère revenir par le bas, pour préciser que tous les effets secondaires doivent être traités.

supercat
la source
Votre premier exemple, la gestion de la okvariable, me semble être l'approche à retour unique. De plus, le bloc if-else peut être simplement réécrit en:return ok ? 0 : ERR_NOT_OK;
Melebius
2
Le premier exemple a un returnau début avant tout le code qui fait tout. En ce qui concerne l'utilisation de l' ?:opérateur, l'écrire sur des lignes séparées permet à de nombreux IDE d'attacher plus facilement un point d'arrêt de débogage au scénario non correct. BTW, la vraie clé du "point de sortie unique" réside dans la compréhension que ce qui compte, c'est que pour chaque appel particulier à une fonction normale, le point de sortie est le point immédiatement après l'appel . De nos jours, les programmeurs tiennent cela pour acquis, mais les choses n'ont pas toujours été ainsi. Dans de rares cas, le code peut devoir se débrouiller sans espace de pile, conduisant à des fonctions ...
supercat
... qui sortent via des gotos conditionnels ou calculés. En général, tout ce qui a suffisamment de ressources pour être programmé dans autre chose que le langage d'assemblage pourra prendre en charge une pile, mais j'ai écrit du code d'assemblage qui devait fonctionner sous des contraintes très strictes (jusqu'à ZERO octets de RAM dans un cas), et avoir plusieurs points de sortie peut être utile dans de tels cas.
supercat du
1
L'exemple dit plus clair est beaucoup moins clair et difficile à lire. Un point de sortie est toujours plus facile à lire, plus facile à maintenir, plus facile à déboguer.
GuidoG
8
@GuidoG: Les deux modèles peuvent être plus lisibles, selon ce qui apparaît dans les sections omises. Utilisation de "return x;" indique clairement que si l'instruction est atteinte, la valeur de retour sera x. Utilisation de "result = x;" laisse ouverte la possibilité que quelque chose d'autre puisse changer le résultat avant qu'il ne soit renvoyé. Cela peut être utile s'il devenait en fait nécessaire de changer le résultat, mais les programmeurs examinant le code devraient l'inspecter pour voir comment le résultat pourrait changer même si la réponse est "il ne peut pas".
supercat
15

Le point d'entrée et de sortie unique était le concept original de la programmation structurée par rapport au codage spaghetti étape par étape. Il existe une croyance selon laquelle plusieurs fonctions de point de sortie nécessitent plus de code car vous devez nettoyer correctement les espaces mémoire alloués aux variables. Considérez un scénario dans lequel la fonction alloue des variables (ressources) et la sortie précoce de la fonction et sans nettoyage approprié entraînerait des fuites de ressources. De plus, la construction du nettoyage avant chaque sortie créerait beaucoup de code redondant.

PSS
la source
Ce n'est vraiment pas un problème avec RAII
BigSandwich
14

Avec presque tout, cela dépend des besoins du livrable. Dans «l'ancien temps», le code spaghetti avec plusieurs points de retour provoquait des fuites de mémoire, car les codeurs qui préféraient cette méthode ne nettoyaient généralement pas bien. Il y avait aussi des problèmes avec certains compilateurs "perdant" la référence à la variable de retour lorsque la pile était sautée pendant le retour, dans le cas d'un retour d'une portée imbriquée. Le problème le plus général était celui du code rentrant, qui tente de faire en sorte que l'état d'appel d'une fonction soit exactement le même que son état de retour. Les mutateurs de oop ont violé cela et le concept a été mis de côté.

Il existe des livrables, notamment des noyaux, qui ont besoin de la vitesse fournie par plusieurs points de sortie. Ces environnements ont normalement leur propre mémoire et leur propre gestion des processus, de sorte que le risque de fuite est minimisé.

Personnellement, j'aime avoir un point de sortie unique, car je l'utilise souvent pour insérer un point d'arrêt sur l'instruction de retour et effectuer une inspection du code de la façon dont le code a déterminé cette solution. Je pourrais simplement aller à l'entrée et passer à travers, ce que je fais avec des solutions largement imbriquées et récursives. En tant que réviseur de code, plusieurs retours dans une fonction nécessitent une analyse beaucoup plus approfondie - donc si vous le faites pour accélérer l'implémentation, vous volez Peter pour sauver Paul. Il faudra plus de temps pour les révisions de code, ce qui invalidera la présomption de mise en œuvre efficace.

- 2 cents

Veuillez consulter ce document pour plus de détails: NISTIR 5459

sscheider
la source
8
multiple returns in a function requires a much deeper analysisseulement si la fonction est déjà énorme (> 1 écran), sinon cela facilite l'analyse
dss539
2
les retours multiples ne facilitent jamais l'analyse, juste l'oposite
GuidoG
1
le lien est mort (404).
fusi
1
@fusi - l'a trouvé sur archive.org et a mis à jour le lien ici
sscheider
4

À mon avis, le conseil de quitter une fonction (ou une autre structure de contrôle) à un seul moment est souvent survendu. Deux raisons sont généralement données pour quitter à un seul moment:

  1. Le code à sortie unique est censé être plus facile à lire et à déboguer. (J'avoue que je ne pense pas beaucoup à cette raison, mais elle est donnée. Ce qui est considérablement plus facile à lire et à déboguer est le code à entrée unique .)
  2. Le code à sortie unique lie et renvoie plus proprement.

La deuxième raison est subtile et a un certain mérite, surtout si la fonction renvoie une grande structure de données. Cependant, je ne m'en soucierais pas trop, sauf ...

Si vous êtes étudiant, vous souhaitez obtenir les meilleures notes dans votre classe. Faites ce que l'instructeur préfère. Il a probablement une bonne raison de son point de vue; alors, à tout le moins, vous apprendrez son point de vue. Cela a une valeur en soi.

Bonne chance.

thb
la source
4

J'étais un partisan du style à sortie unique. Mon raisonnement venait principalement de la douleur ...

La sortie unique est plus facile à déboguer.

Compte tenu des techniques et des outils dont nous disposons aujourd'hui, il s'agit d'une position beaucoup moins raisonnable à prendre car les tests unitaires et la journalisation peuvent rendre la sortie unique inutile. Cela dit, lorsque vous avez besoin de regarder le code s'exécuter dans un débogueur, il était beaucoup plus difficile de comprendre et de travailler avec du code contenant plusieurs points de sortie.

Cela devenait particulièrement vrai lorsque vous deviez interjecter des affectations afin d'examiner l'état (remplacé par des expressions de surveillance dans les débogueurs modernes). Il était également trop facile de modifier le flux de contrôle de manière à masquer le problème ou à interrompre complètement l'exécution.

Les méthodes à sortie unique étaient plus faciles à parcourir dans le débogueur et plus faciles à séparer sans rompre la logique.

Michael Murphree
la source
0

La réponse est très dépendante du contexte. Si vous créez une interface graphique et que vous avez une fonction qui initialise les API et ouvre les fenêtres au début de votre main, il sera plein d'appels qui peuvent générer des erreurs, dont chacune entraînerait la fermeture de l'instance du programme. Si vous utilisiez des instructions IF imbriquées et indentez votre code pourrait rapidement devenir très biaisé vers la droite. Revenir sur une erreur à chaque étape pourrait être mieux et en fait plus lisible tout en étant tout aussi facile à déboguer avec quelques indicateurs dans le code.

Si, cependant, vous testez différentes conditions et renvoyez des valeurs différentes en fonction des résultats de votre méthode, il peut être préférable d'avoir un seul point de sortie. J'avais l'habitude de travailler sur des scripts de traitement d'images dans MATLAB qui pouvaient devenir très volumineux. Plusieurs points de sortie peuvent rendre le code extrêmement difficile à suivre. Les déclarations Switch étaient beaucoup plus appropriées.

La meilleure chose à faire serait d'apprendre au fur et à mesure. Si vous écrivez du code pour quelque chose, essayez de trouver le code d'autres personnes et voyez comment elles l'implémentent. Décidez quels morceaux vous aimez et quels morceaux vous n'aimez pas.

Flash_Steel
la source
-5

Si vous sentez que vous avez besoin de plusieurs points de sortie dans une fonction, la fonction est trop volumineuse et en fait trop.

Je recommanderais de lire le chapitre sur les fonctions dans le livre de Robert C. Martin, Clean Code.

Essentiellement, vous devriez essayer d'écrire des fonctions avec 4 lignes de code ou moins.

Quelques notes du blog de Mike Long :

  • La première règle des fonctions: elles doivent être petites
  • La deuxième règle des fonctions: elles doivent être plus petites que cela
  • Les blocs dans les instructions if, les instructions while, les boucles for, etc. doivent faire une ligne
  • … Et cette ligne de code sera généralement un appel de fonction
  • Il ne devrait pas y avoir plus d'un ou peut-être deux niveaux d'indentation
  • Les fonctions doivent faire une chose
  • Les instructions de fonction doivent toutes être au même niveau d'abstraction
  • Une fonction ne doit pas avoir plus de 3 arguments
  • Les arguments de sortie sont une odeur de code
  • Passer un drapeau booléen dans une fonction est vraiment horrible. Vous faites par définition deux choses dans la fonction.
  • Les effets secondaires sont des mensonges.
Roger Dahl
la source
29
4 lignes? Que codez-vous qui vous permet une telle simplicité? Je doute vraiment que le noyau Linux ou git par exemple le fasse.
shinzou
7
"Passer un drapeau booléen dans une fonction est vraiment horrible. Vous faites par définition deux - choses dans la fonction." Par définition? Non ... ce booléen pourrait n'affecter qu'une de vos quatre lignes. Aussi, même si je suis d'accord pour garder la taille de la fonction petite, quatre est un peu trop restrictif. Cela devrait être considéré comme une directive très vague.
Jesse
12
L'ajout de restrictions comme celles-ci entraînera inévitablement un code déroutant. Il s'agit davantage de méthodes propres et concises et de ne faire que ce qu'elles sont censées faire sans effets secondaires inutiles.
Jesse
10
Une des rares réponses sur SO que j'aimerais pouvoir voter plusieurs fois.
Steven Rands
8
Malheureusement, cette réponse fait plusieurs choses et devait probablement être divisée en plusieurs autres, toutes en moins de quatre lignes.
Eli