Pourquoi une boucle while (true) dans un constructeur est-elle réellement mauvaise?

47

Bien que la question soit générale, mon domaine d’application est plutôt C #, étant donné que des langages tels que C ++ ont une sémantique différente en ce qui concerne l’exécution des constructeurs, la gestion de la mémoire, le comportement non défini, etc.

Quelqu'un m'a posé une question intéressante à laquelle il n'a pas été facile de répondre.

Pourquoi (ou est-ce du tout?) Considéré comme une mauvaise conception permettant à un constructeur de classe de commencer une boucle sans fin (c.-à-d. Une boucle de jeu)?

Il y a quelques concepts qui sont cassés par ceci:

  • Comme pour le principe du moindre étonnement, l'utilisateur ne s'attend pas à ce que le constructeur se comporte de la sorte.
  • Les tests unitaires sont plus difficiles car vous ne pouvez pas créer cette classe ni l'injecter car elle ne quitte jamais la boucle.
  • La fin de la boucle (fin du jeu) est alors conceptuellement l'heure à laquelle le constructeur se termine, ce qui est également étrange.
  • Techniquement, une telle classe n'a aucun membre public, à l'exception du constructeur, ce qui rend la compréhension plus difficile (en particulier pour les langues où aucune implémentation n'est disponible).

Et puis il y a des problèmes techniques:

  • Le constructeur ne finit jamais, alors que se passe-t-il avec GC ici? Cet objet est-il déjà dans la génération 0?
  • Dériver d'une telle classe est impossible ou du moins très compliqué car le constructeur de base ne renvoie jamais

Y a-t-il quelque chose de plus manifestement mauvais ou sournois avec une telle approche?

Samuel
la source
61
Pourquoi est-il bon? Si vous déplacez simplement votre boucle principale vers une méthode (refactoring très simple), l'utilisateur peut écrire un code non surprenant comme celui-ci:var g = new Game {...}; g.MainLoop();
Brandin le
61
Votre question indique déjà 6 raisons de ne pas l'utiliser et j'aimerais en voir une seule en sa faveur. Vous pouvez également demander pourquoi il est une mauvaise idée de mettre une while(true)boucle dans un setter de propriété: new Game().RunNow = true?
Groo
38
Analogie extrême: pourquoi nous disons que Hitler a eu tort? À l'exception de la discrimination raciale, du début de la Seconde Guerre mondiale, du meurtre de millions de personnes, de la violence [etc. etc. pour plus de 50 autres raisons], il n'a rien fait de mal. Bien sûr, si vous supprimez une liste arbitraire de raisons pour lesquelles quelque chose ne va pas, vous pouvez aussi bien conclure que cette chose est bonne, pour littéralement n'importe quoi.
Bakuriu
4
En ce qui concerne le ramasse-miettes (votre problème technique), il n’y aurait rien de spécial à ce sujet. L'instance existe avant que le code constructeur ne soit réellement exécuté. Dans ce cas, l'instance est toujours accessible et ne peut donc pas être réclamée par GC. Si GC se met en place, cette instance survivra et, à chaque occurrence de la configuration de GC, l'objet sera promu à la génération suivante, conformément à la règle habituelle. Que la GC ait lieu ou non dépend de si (suffisamment) de nouveaux objets sont créés ou non.
Jeppe Stig Nielsen
8
Je voudrais aller un peu plus loin et dire que while (true) est mauvais, peu importe où il est utilisé. Cela implique qu'il n'y a pas de moyen clair et propre pour arrêter la boucle. Dans votre exemple, la boucle ne devrait-elle pas s'arrêter à la sortie du jeu ou perdre le focus?
Jon Anderson

Réponses:

250

Quel est le but d'un constructeur? Il retourne un objet nouvellement construit. Que fait une boucle infinie? Ça ne revient jamais. Comment le constructeur peut-il retourner un objet nouvellement construit s'il ne retourne pas du tout? Ça ne peut pas.

Ergo, une boucle infinie rompt le contrat fondamental du constructeur: construire quelque chose.

Jörg W Mittag
la source
11
Peut-être qu'ils voulaient mettre tout ce qui est (vrai) avec une pause au centre? Risqué, mais légitime.
John Dvorak
2
Je suis d'accord, c'est correct - au moins. d'un point de vue académique. Même chose pour chaque fonction qui retourne quelque chose.
Doc Brown le
61
Imaginez le pauvre gazon qui crée une sous-classe de ladite classe ...
Newtopian
5
@ JanDvorak: Eh bien, c'est une question différente. L'OP a explicitement spécifié "jamais terminé" et a mentionné une boucle d'événement.
Jörg W Mittag
4
@ JörgWMittag bien, alors, ouais, ne mettez pas cela dans un constructeur.
John Dvorak
45

Y a-t-il quelque chose de plus manifestement mauvais ou sournois avec une telle approche?

Oui bien sûr. C'est inutile, inattendu, inutile, sans élégance. Il viole les concepts modernes de conception de classe (cohésion, couplage). Il rompt le contrat de méthode (un constructeur a un travail défini et n'est pas simplement une méthode aléatoire). Ce n’est certainement pas très facile à maintenir, les futurs programmeurs passeront beaucoup de temps à essayer de comprendre ce qui se passe et à deviner les raisons pour lesquelles cela a été fait.

Rien de tout cela n'est un "bug" dans le sens où votre code ne fonctionne pas. Mais à long terme, cela entraînera probablement des coûts secondaires énormes (par rapport au coût d’écriture du code) en rendant le code plus difficile à maintenir (c’est-à-dire difficile à ajouter des tests, difficile à réutiliser, difficile à déboguer, difficile à étendre, etc. .)

La plupart des améliorations, parmi les plus modernes, des méthodes de développement logiciel sont apportées spécifiquement pour faciliter le processus réel d’écriture / test / débogage / maintenance du logiciel. Tout cela est contourné par des choses comme celle-ci, où le code est placé de manière aléatoire parce qu'il "fonctionne".

Malheureusement, vous rencontrerez régulièrement des programmeurs complètement ignorants de tout cela. Ça marche, c'est ça.

Pour terminer avec une analogie (un autre langage de programmation, le problème est de calculer 2+2):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

Quel est le problème avec la première approche? Il renvoie 4 après un laps de temps raisonnablement court. c'est correct. Les objections (avec toutes les raisons habituelles qu'un développeur peut donner pour les réfuter) sont les suivantes:

  • Plus difficile à lire (mais bon, un bon programmeur peut le lire aussi facilement, et nous l'avons toujours fait comme ça; si vous voulez, je peux le reformuler dans une méthode!)
  • Plus lent (mais ce n'est pas dans un endroit crucial pour le chronométrage, donc nous pouvons l'ignorer, et de plus, il vaut mieux optimiser uniquement lorsque c'est nécessaire!)
  • Utilise beaucoup plus de ressources (nouveau processus, plus de mémoire vive, etc., mais notre serveur est plus que rapide)
  • Introduit des dépendances sur bash(mais il ne fonctionnera jamais sous Windows, Mac ou Android de toute façon)
  • Et toutes les autres raisons mentionnées ci-dessus.
AnoE
la source
5
"GC pourrait être dans un état de verrouillage exceptionnel lors de l'exécution d'un constructeur" - pourquoi? L'allocateur alloue d'abord, puis lance le constructeur en tant que méthode ordinaire. Il n'y a rien de vraiment spécial à ce sujet, n'est-ce pas? De plus, l’allocateur doit autoriser de nouvelles attributions (et celles-ci pourraient déclencher des situations de MOO) au sein du constructeur.
John Dvorak
1
@JanDvorak, je voulais juste souligner le fait qu'il pourrait y avoir un comportement spécial "quelque part" pendant que vous êtes dans un constructeur, mais vous avez raison, l'exemple que j'ai choisi était irréaliste. Enlevé.
AnoE
J'aime beaucoup votre explication et j'aimerais marquer les deux réponses comme des réponses (avoir au moins un vote positif). L'analogie est bonne et votre piste de pensée et d'explication des coûts secondaires l'est aussi, mais je les considère comme des exigences auxiliaires du code (les mêmes que mes arguments). L'état actuel de la technique consiste à tester le code. La testabilité, etc., est donc une exigence de facto. Mais la déclaration de @ JörgWMittag est parfaite fundamental contract of a constructor: to construct something.
Samuel
L'analogie n'est pas si bonne. Si je vois cela, je m'attendrais à voir un commentaire quelque part sur la raison pour laquelle vous utilisez Bash pour faire le calcul, et selon votre raison, cela peut être parfaitement correct. La même chose ne fonctionnerait pas pour l'OP, car vous auriez essentiellement à mettre des excuses dans les commentaires partout où vous avez utilisé le constructeur "// Remarque: la construction de cet objet démarre une boucle d'événement qui ne retourne jamais".
Brandin
4
"Malheureusement, vous rencontrerez régulièrement des programmeurs qui ignorent complètement tout cela. Cela fonctionne, c'est tout." Si triste mais tellement vrai. Je pense que je peux parler au nom de tous lorsque je dis que le seul fardeau substantiel dans notre vie professionnelle est, eh bien, ces personnes.
Courses de légèreté avec Monica
8

Dans votre question, vous donnez suffisamment de raisons pour écarter cette approche, mais la question qui se pose est la suivante: "Y a-t-il quelque chose de plus manifestement mauvais ou sournois avec une telle approche?"

La première chose que j'ai faite ici, c'est que c'est inutile. Si votre constructeur ne finit jamais, aucune autre partie du programme ne peut obtenir une référence à l' objet construit . Quelle est donc la logique derrière le placer dans un constructeur au lieu d'une méthode régulière? Je me suis alors rendu compte que la seule différence serait que, dans votre boucle, vous pouviez permettre aux références de thiss’échapper partiellement de la boucle. Bien que cela ne soit pas strictement limité à cette situation, il est garanti que si vous autorisez thisà vous échapper, ces références pointeront toujours vers un objet qui n'est pas entièrement construit.

Je ne sais pas si la sémantique autour de ce genre de situation est bien définie en C #, mais je dirais que cela n'a pas d'importance, car ce n'est pas quelque chose que la plupart des développeurs voudraient essayer d'explorer.

JimmyJames
la source
Je suppose que le constructeur est exécuté après la création de l'objet. Par conséquent, dans un langage sain qui laisse filtrer des données, ce comportement doit être parfaitement défini.
Mars
@mroman: une fuite thisdans le constructeur peut être correcte , mais uniquement si vous êtes dans le constructeur de la classe la plus dérivée. Si vous avez deux classes, Aet Bavec B : A, si le constructeur de Afuit this, les membres de Bne sont toujours pas initialisés (et les appels de méthodes virtuelles ou les envois vers Bpeuvent provoquer des dégâts sur cette base).
hoffmale
1
Je suppose qu'en C ++, cela peut être fou, oui. Dans d'autres langues, les membres reçoivent une valeur par défaut avant que leurs valeurs réelles ne leur soient affectées. Ainsi, même s'il est stupide d'écrire un tel code, le comportement est toujours bien défini. Si vous appelez des méthodes qui utilisent ces membres, vous pouvez vous heurter à NullPointerExceptions ou à des calculs incorrects (parce que les nombres sont définis par défaut à 0) ou à des choses du même genre, mais le résultat est techniquement déterministe.
dimanche
@mroman Pouvez-vous trouver une référence définitive pour le CLR qui montrerait que c'est le cas?
JimmyJames
Eh bien, vous pouvez affecter des valeurs de membres avant l’exécution du constructeur afin que l’objet existe dans un état valide avec les valeurs par défaut attribuées aux membres ou avant leur exécution. Vous n'avez pas besoin du constructeur pour initialiser les membres. Laissez-moi voir si je peux trouver cela quelque part dans la documentation.
dimanche
1

+1 Pour le moins d’étonnement, mais c’est un concept difficile à exprimer à de nouveaux développeurs. À un niveau pragmatique, il est difficile de déboguer les exceptions générées dans les constructeurs. Si l'objet ne parvient pas à s'initialiser, il ne sera pas possible d'inspecter l'état ou de se connecter depuis l'extérieur de ce constructeur.

Si vous ressentez le besoin de faire ce type de modèle de code, utilisez plutôt des méthodes statiques sur la classe.

Un constructeur existe pour fournir la logique d'initialisation lorsqu'un objet est instancié à partir d'une définition de classe. Vous construisez cette instance d'objet car il s'agit d'un conteneur qui encapsule un ensemble de propriétés et de fonctionnalités que vous souhaitez appeler à partir du reste de la logique de votre application.

Si vous n'avez pas l'intention d'utiliser l'objet que vous "construisez", alors à quoi sert-il d'instancier l'objet? Avoir une boucle while (true) dans un constructeur signifie effectivement que vous ne voulez jamais qu'il se termine ...

C # est un langage orienté objet très riche avec de nombreuses constructions et paradigmes différents que vous pouvez explorer, connaître vos outils et quand les utiliser, en bout de ligne:

En C #, n'exécutez pas de logique étendue ou infinie dans les constructeurs car ... il y a de meilleures alternatives

Chris Schaller
la source
1
sa ne semble pas offrir quoi que ce soit de substantiel sur les points faits et expliqués dans les 9 réponses précédentes
gnat
1
Hé, je devais essayer :) Je pensais qu'il était important de souligner que même s'il n'y a pas de règle contre cela, vous ne devriez pas en raison du fait qu'il existe des moyens plus simples. La plupart des autres réponses se concentrent sur les raisons techniques pour lesquelles c'est mauvais, mais c'est difficile à vendre à un nouveau dev qui dit "mais ça compile, donc je peux le laisser comme ça"
Chris Schaller
0

Il n'y a rien de mauvais en soi. Cependant, l’important est la question de savoir pourquoi vous avez choisi d’utiliser cette construction. Qu'avez-vous obtenu en faisant cela?

Tout d'abord, je ne parlerai pas de l'utilisation while(true)d'un constructeur. Il n'y a absolument rien de mal à utiliser cette syntaxe dans un constructeur. Cependant, le concept de "démarrage d'une boucle de jeu dans le constructeur" est susceptible de poser problème.

Il est très courant dans ces situations que le constructeur construise simplement l'objet et ait une fonction qui appelle la boucle infinie. Cela donne plus de flexibilité à l'utilisateur de votre code. Un exemple du type de problèmes pouvant apparaître est que vous limitez l'utilisation de votre objet. Si quelqu'un veut avoir un objet membre qui est "l'objet de jeu" dans sa classe, il doit être prêt à exécuter la boucle de jeu complète dans son constructeur car il doit construire l'objet. Cela pourrait conduire à un codage torturé pour contourner ce problème. Dans le cas général, vous ne pouvez pas pré-allouer votre objet de jeu, puis l'exécuter plus tard. Pour certains programmes, ce n'est pas un problème, mais il y a des classes de programmes où vous voulez pouvoir tout allouer à l'avance, puis appeler la boucle de jeu.

Une des règles de base de la programmation consiste à utiliser l'outil le plus simple pour le travail. Ce n'est pas une règle absolue, mais c'est une règle très utile. Si un appel de fonction aurait suffi, pourquoi se préoccuper de construire un objet de classe? Si je vois un outil compliqué plus puissant utilisé, je suppose que le développeur avait une raison à cela, et je vais commencer à explorer les trucs étranges que vous pourriez faire.

Il y a des cas où cela pourrait être raisonnable. Il peut y avoir des cas où il est vraiment intuitif d’avoir une boucle de jeu dans le constructeur. Dans ces cas, vous courez avec! Par exemple, votre boucle de jeu peut être intégrée à un programme beaucoup plus vaste qui construit des classes pour incorporer certaines données. Si vous devez exécuter une boucle de jeu pour générer ces données, il peut être très raisonnable de l'exécuter dans un constructeur. Traitez cela simplement comme un cas spécial: ce serait valable parce que le programme global le rend intuitif pour le développeur suivant qui comprend ce que vous avez fait et pourquoi.

Cort Ammon
la source
Si la construction de votre objet nécessite une boucle de jeu, placez-la au moins dans une méthode factory. GameTestResults testResults = runGameTests(tests, configuration);plutôt que GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751
@immibis Oui, c'est vrai, sauf dans les cas où cela est déraisonnable. Si vous travaillez avec un système basé sur la réflexion qui instancie votre objet pour vous, vous n'avez peut-être pas la possibilité de créer une méthode fabrique.
Cort Ammon
0

Mon impression est que certains concepts se sont mélangés et ont abouti à une question moins bien formulée.

Le constructeur d'une classe peut démarrer un nouveau thread qui ne se terminera jamais (bien que je préfère utiliser while(_KeepRunning)plus while(true)car je peux définir le membre boolean sur false quelque part).

Vous pouvez extraire cette méthode de création et de démarrage du fil dans une fonction qui lui est propre, afin de séparer la construction de l'objet et le début de son travail. Je préfère cette méthode car je contrôle mieux l'accès aux ressources.

Vous pouvez également consulter le "Modèle d'objet actif". En fin de compte, je suppose que la question visait à savoir quand commencer le fil d'un "objet actif", et ma préférence va au découplage de la construction et au démarrage pour un meilleur contrôle.

Bernhard Hiller
la source
0

Votre objet me rappelle le début des tâches . L'objet de la tâche a un constructeur, mais il il y a aussi un tas de méthodes statiques d'usine comme: Task.Run, Task.Startet Task.Factory.StartNew.

Celles-ci sont très similaires à ce que vous essayez de faire, et il est probable que ce soit la «convention». Le principal problème que les gens semblent avoir est que cette utilisation d'un constructeur les surprend. @ JörgWMittag dit qu'il rompt un contrat fondamental , ce qui signifie que Jörg est très surpris. Je suis d'accord et n'ai plus rien à ajouter à cela.

Cependant, je souhaite suggérer que l'OP utilise des méthodes d'usine statiques. La surprise disparaît et il n'y a pas de contrat fondamental en jeu ici. Les gens sont habitués aux méthodes d'usine statiques qui font des choses spéciales, et on peut les nommer en conséquence.

Vous pouvez fournir des constructeurs qui permettent un contrôle précis (comme le suggère @Brandin var g = new Game {...}; g.MainLoop();, qui s'adapte aux utilisateurs qui ne veulent pas démarrer le jeu immédiatement, et qui veulent peut-être le faire passer en premier. Et vous pouvez écrire quelque chose comme var runningGame = Game.StartNew();démarrer c'est tout de suite facile.

Nathan Cooper
la source
Cela signifie que Jörg est fondamentalement surpris, c'est-à-dire qu'une surprise comme celle-là est une douleur dans le fond.
Steve Jessop
0

Pourquoi une boucle while (true) dans un constructeur est-elle réellement mauvaise?

C'est pas mal du tout.

Pourquoi (ou est-ce du tout?) Considéré comme une mauvaise conception permettant à un constructeur de classe de commencer une boucle sans fin (c.-à-d. Une boucle de jeu)?

Pourquoi voudriez-vous jamais faire cela? Sérieusement. Cette classe a une seule fonction: le constructeur. Si tout ce que vous voulez, c'est une seule fonction, c'est pourquoi nous avons des fonctions, pas des constructeurs.

Vous avez évoqué vous-même certaines des raisons évidentes, mais vous avez laissé de côté la plus importante:

Il existe des options meilleures et plus simples pour réaliser la même chose.

S'il y a 2 choix et que l'un est meilleur, peu importe combien il vaut mieux, vous avez choisi le meilleur. Incidemment, c'est pourquoi nous n'utilisons presque jamais GoTo.

Peter
la source
-1

Le but d'un constructeur est de bien construire votre objet. Avant que le constructeur ait terminé, il est en principe dangereux d'utiliser les méthodes de cet objet. Bien sûr, nous pouvons appeler des méthodes de l’objet dans le constructeur, mais chaque fois que nous le faisons, nous devons nous assurer que cela est valable pour l’objet dans son état actuel de demi-traitement.

En mettant indirectement toute la logique dans le constructeur, vous ajoutez plus de charge de ce type sur vous-même. Cela suggère que vous n'avez pas assez bien conçu la différence entre initialisation et utilisation.

Hagen von Eitzen
la source
1
cela ne semble pas offrir quoi que ce soit de substantiel sur les points soulevés et expliqués dans les 8 réponses précédentes
gnat