web-dev-qa-db-fra.com

D'où vient la notion de "un seul retour"?

Je parle souvent aux programmeurs qui disent "Ne mettez pas plusieurs déclarations de retour dans la même méthode." Quand je leur demande de me dire les raisons, tout ce que je reçois est "Le codage standard le dit. "ou" C'est déroutant. "Quand ils me montrent des solutions avec une seule déclaration de retour, le code me semble plus laid. Par exemple:

if (condition)
   return 42;
else
   return 97;

"C'est moche, vous devez utiliser une variable locale!"

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

Comment ce gonflement de code de 50% rend le programme plus facile à comprendre? Personnellement, je trouve cela plus difficile, car l'espace d'état vient d'augmenter d'une autre variable qui aurait facilement pu être évitée.

Bien sûr, normalement j'écrirais simplement:

return (condition) ? 42 : 97;

Mais de nombreux programmeurs évitent l'opérateur conditionnel et préfèrent la forme longue.

D'où vient cette notion de "un seul retour"? Y a-t-il une raison historique à l'origine de cette convention?

1077
fredoverflow

"Entrée unique, sortie unique" a été écrit lorsque la plupart de la programmation était effectuée en langage assembleur, FORTRAN ou COBOL. Il a été largement mal interprété, car les langues modernes ne soutiennent pas les pratiques contre lesquelles Dijkstra mettait en garde.

"Entrée unique" signifiait "ne pas créer de points d'entrée alternatifs pour les fonctions". En langage d'assemblage, il est bien sûr possible de saisir une fonction à n'importe quelle instruction. FORTRAN a pris en charge plusieurs entrées de fonctions avec l'instruction ENTRY:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"Single Exit" signifiait qu'une fonction ne devait retourner to qu'un seul endroit: l'instruction suivant immédiatement l'appel. Cela pas signifiait qu'une fonction ne devait retourner de qu'un seul endroit. Lorsque Programmation structurée a été écrit, il était courant qu'une fonction indique une erreur en revenant à un autre emplacement. FORTRAN l'a soutenu via un "retour alternatif":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

Ces deux techniques étaient très sujettes aux erreurs. L'utilisation d'entrées alternatives laissait souvent une variable non initialisée. L'utilisation de retours alternatifs avait tous les problèmes d'une instruction GOTO, avec la complication supplémentaire que la condition de la branche n'était pas adjacente à la branche, mais quelque part dans le sous-programme.

1145
kevin cline

Cette notion de Entrée unique, sortie unique (SESE) vient de langues avec gestion explicite des ressources, comme C et Assembly. En C, un code comme celui-ci entraînera une fuite de ressources:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

Dans ces langues, vous avez essentiellement trois options:

  • Répliquez le code de nettoyage.
    Pouah. La redondance est toujours mauvaise.

  • Utilisez un goto pour accéder au code de nettoyage.
    Cela nécessite que le code de nettoyage soit la dernière chose dans la fonction. (Et c'est pourquoi certains soutiennent que goto a sa place. Et il a en effet - en C.)

  • Introduisez une variable locale et manipulez le flux de contrôle à travers cela.
    L'inconvénient est que le flux de contrôle manipulé via la syntaxe (pensez break, return, if, while) est beaucoup plus facile à suivre que contrôler le flux manipulé à travers l'état des variables (car ces variables n'ont pas d'état lorsque vous regardez l'algorithme).

Dans Assembly, c'est encore plus étrange, car vous pouvez accéder à n'importe quelle adresse d'une fonction lorsque vous appelez cette fonction, ce qui signifie que vous disposez d'un nombre presque illimité de points d'entrée pour n'importe quelle fonction. (Parfois, cela est utile. De tels thunks sont une technique courante pour les compilateurs pour implémenter l'ajustement du pointeur this nécessaire pour appeler les fonctions virtual dans les scénarios d'héritage multiple en C++.)

Lorsque vous devez gérer les ressources manuellement, exploiter les options d'entrée ou de sortie d'une fonction n'importe où conduit à un code plus complexe, et donc à des bugs. Par conséquent, une école de pensée est apparue qui a propagé SESE, afin d'obtenir un code plus propre et moins de bogues.


Cependant, lorsqu'un langage comporte des exceptions, (presque) n'importe quelle fonction peut être fermée prématurément à (presque) n'importe quel moment, vous devez donc prendre des dispositions pour un retour prématuré de toute façon. (Je pense que finally est principalement utilisé pour cela dans Java et using (lors de l'implémentation de IDisposable, finally sinon)) en C #; C++ emploie à la place RAII .) Une fois que vous avez fait cela, vous ne pouvez pas échouer à nettoyer après vous en raison à une première déclaration return, donc quel est probablement l'argument le plus fort en faveur de SESE a disparu.

Cela laisse la lisibilité. Bien sûr, une fonction 200 LoC avec une demi-douzaine d'instructions return saupoudrées au hasard n'est pas un bon style de programmation et ne rend pas le code lisible. Mais une telle fonction ne serait pas facile à comprendre sans ces retours prématurés non plus.

Dans les langues où les ressources ne sont pas ou ne devraient pas être gérées manuellement, l'adhésion à l'ancienne convention SESE a peu ou pas de valeur. OTOH, comme je l'ai expliqué ci-dessus, SESE rend souvent le code plus complexe. C'est un dinosaure qui (à l'exception de C) ne s'intègre pas bien dans la plupart des langues d'aujourd'hui. Au lieu d'aider à la compréhensibilité du code, il l'entrave.


Pourquoi les programmeurs Java s'en tiennent-ils à cela? Je ne sais pas, mais de mon POV (extérieur), Java a pris beaucoup de conventions de C (où ils ont du sens) et les ont appliqués à son OO monde (où ils sont inutiles ou carrément mauvais), où il s'en tient maintenant à eux, quels que soient les coûts. (Comme la convention pour définir tout vos variables au début de la portée.)

Les programmeurs s'en tiennent à toutes sortes de notations étranges pour des raisons irrationnelles. (Les déclarations structurelles profondément imbriquées - les "pointes de flèches" - étaient, dans des langages comme Pascal, autrefois considérées comme du beau code.) L'application d'un raisonnement logique pur à cela ne semble pas convaincre la majorité d'entre elles de s'écarter de leurs voies établies. La meilleure façon de changer de telles habitudes est probablement de leur apprendre tôt à faire ce qui est le mieux, pas ce qui est conventionnel. En tant que professeur de programmation, vous l'avez en main. :)

921
sbi

D'une part, les instructions de retour unique facilitent la journalisation, ainsi que les formes de débogage qui reposent sur la journalisation. Je me souviens de nombreuses fois où j'ai dû réduire la fonction en un seul retour juste pour imprimer la valeur de retour en un seul point.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

D'un autre côté, vous pouvez refactoriser ceci dans function() qui appelle _function() et enregistre le résultat.

82
perreal

"Single Entry, Single Exit" est né de la révolution de la programmation structurée du début des années 1970, qui a été lancée par la lettre d'Edsger W. Dijkstra à l'éditeur " Déclaration GOTO considérée comme nuisible ". Les concepts derrière la programmation structurée ont été exposés en détail dans le livre classique "Structured Programming" par Ole Johan-Dahl, Edsger W. Dijkstra et Charles Anthony Richard Hoare.

"Déclaration GOTO considérée comme nuisible" est une lecture obligatoire, même aujourd'hui. La "programmation structurée" est datée, mais toujours très, très enrichissante, et devrait être en haut de la liste des développeurs à lire absolument, bien au-dessus de tout, par exemple Steve McConnell. (La section de Dahl présente les bases des classes dans Simula 67, qui sont le fondement technique des classes en C++ et de toute la programmation orientée objet.)

53
John R. Strohm

Il est toujours facile de relier Fowler.

L'un des principaux exemples qui vont à l'encontre de SESE sont les clauses de garde:

Remplacer le conditionnel imbriqué par des clauses de garde

Utilisez des clauses de garde pour tous les cas spéciaux

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

 http://www.refactoring.com/catalog/arrow.gif

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

Pour plus d'informations, voir page 250 de Refactoring ...

36
Pieter B

J'ai écrit un article de blog sur ce sujet il y a quelque temps.

L'essentiel est que cette règle vient de l'âge des langues qui n'ont pas de récupération de place ou de gestion des exceptions. Aucune étude formelle ne montre que cette règle conduit à un meilleur code dans les langages modernes. N'hésitez pas à l'ignorer chaque fois que cela conduira à un code plus court ou plus lisible. Les gars de Java insistant là-dessus sont aveuglément et ne remettent pas en cause une règle obsolète et inutile.

Cette question a également été posée sur Stackoverflow

11
Anthony

Un retour facilite le refactoring. Essayez d'exécuter la "méthode d'extraction" dans le corps interne d'une boucle for qui contient un retour, une interruption ou une poursuite. Cela échouera car vous avez interrompu votre flux de contrôle.

Le fait est: je suppose que personne ne prétend écrire du code parfait. Le code est donc régulièrement en cours de refactorisation pour être "amélioré" et étendu. Mon objectif serait donc de garder mon code aussi refactoring que possible.

Souvent, je suis confronté au problème que je dois reformuler complètement les fonctions si elles contiennent des disjoncteurs de contrôle et si je veux ajouter seulement peu de fonctionnalités. Ceci est très sujet aux erreurs lorsque vous modifiez l'ensemble du flux de contrôle au lieu d'introduire de nouveaux chemins vers des imbrications isolées. Si vous n'avez qu'un seul retour à la fin ou si vous utilisez des gardes pour quitter une boucle, vous avez bien sûr plus d'imbrication et plus de code. Mais vous gagnez le compilateur et IDE capacités de refactorisation prises en charge.

6
oopexpert

Considérez le fait que plusieurs déclarations de retour équivalent à avoir des GOTO à une seule déclaration de retour. C'est le même cas avec les instructions break. Ainsi, certains, comme moi, les considèrent comme GOTO à toutes fins utiles.

Cependant, je ne considère pas ces types de GOTO nuisibles et n'hésiterai pas à utiliser un GOTO réel dans mon code si j'en trouve une bonne raison.

Ma règle générale est que les GOTO sont uniquement destinés au contrôle de flux. Ils ne devraient jamais être utilisés pour une boucle, et vous ne devriez jamais GOTO "vers le haut" ou "vers l'arrière". (comment fonctionnent les pauses/retours)

Comme d'autres l'ont mentionné, ce qui suit est une lecture incontournable Déclaration GOTO considérée comme nuisible
Cependant, gardez à l'esprit que cela a été écrit en 1970 lorsque les GOTO étaient bien trop utilisés. Tous les GOTO ne sont pas nocifs et je ne découragerais pas leur utilisation tant que vous ne les utilisez pas à la place des constructions normales, mais plutôt dans le cas étrange, l'utilisation de constructions normales serait très gênante.

Je trouve que leur utilisation dans les cas d'erreur où vous devez échapper à une zone en raison d'une défaillance qui ne devrait jamais se produire dans des cas normaux est utile à certains moments. Mais vous devriez également envisager de mettre ce code dans une fonction distincte afin que vous puissiez simplement revenir tôt au lieu d'utiliser un GOTO ... mais parfois c'est aussi gênant.

5
user606723

Complexité cyclomatique

J'ai vu SonarCube utiliser plusieurs déclarations de retour pour déterminer la complexité cyclomatique. Donc, plus les déclarations de retour, plus la complexité cyclomatique est élevée

Changement de type de retour

Les retours multiples signifient que nous devons changer à plusieurs endroits de la fonction lorsque nous décidons de changer notre type de retour.

Sortie multiple

Il est plus difficile à déboguer car la logique doit être soigneusement étudiée conjointement avec les instructions conditionnelles pour comprendre la cause de la valeur renvoyée.

Solution refactorisée

La solution à plusieurs instructions de retour consiste à les remplacer par un polymorphisme ayant un seul retour après avoir résolu l'objet d'implémentation requis.

4
Sorter