web-dev-qa-db-fra.com

Pourquoi une boucle while (true) dans un constructeur est-elle vraiment mauvaise?

Bien qu'une question générale, ma portée est plutôt C # car je suis conscient que les langages comme C++ ont une sémantique différente concernant l'exécution du constructeur, la gestion de la mémoire, le comportement indéfini, etc.

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

Pourquoi (ou est-ce du tout?) Considéré comme une mauvaise conception pour permettre au constructeur d'une classe de démarrer une boucle sans fin (c'est-à-dire une boucle de jeu)?

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

  • comme le principe du moindre étonnement, l'utilisateur ne s'attend pas à ce que le constructeur se comporte comme ça.
  • Les tests unitaires sont plus difficiles car vous ne pouvez pas créer cette classe ou l'injecter car elle ne quitte jamais la boucle.
  • La fin de la boucle (fin du jeu) est alors conceptuellement le moment où le constructeur se termine, ce qui est également étrange.
  • Techniquement, une telle classe n'a pas de membres publics sauf le constructeur, ce qui la rend plus difficile à comprendre (en particulier pour les langues où aucune implémentation n'est disponible)

Et puis il y a des problèmes techniques:

  • Le constructeur ne termine 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é du fait que le constructeur de base ne retourne jamais

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

47
Samuel

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

Ergo, une boucle infinie rompt le contrat fondamental d'un constructeur: construire quelque chose.

251
Jörg W Mittag

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

Oui bien sûr. C'est inutile, inattendu, inutile, non élégant. 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 seulement une méthode aléatoire). Ce n'est certainement pas bien maintenable, les futurs programmeurs passeront beaucoup de temps à essayer de comprendre ce qui se passe et à essayer de deviner les raisons pour lesquelles cela a été fait de cette façon.

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

De nombreuses améliorations/les plus modernes dans les méthodes de développement de logiciels sont faites spécifiquement pour faciliter le processus réel d'écriture/test/débogage/maintenance du logiciel. Tout cela est contourné par des trucs comme celui-ci, où le code est placé au hasard parce qu'il "fonctionne".

Malheureusement, vous rencontrerez régulièrement des programmeurs qui ignorent tout cela. Ça marche, c'est tout.

Pour terminer avec une analogie (autre langage de programmation, le problème est ici 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 (ainsi que 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 refactoriser en une méthode!)
  • Plus lent (mais ce n'est pas dans un endroit critique pour le timing, nous pouvons donc ignorer cela, et en plus, il est préférable de ne l'optimiser que si nécessaire!)
  • Utilise beaucoup plus de ressources (un nouveau processus, plus RAM etc. - mais dito, notre serveur est plus que suffisamment rapide)
  • Introduit des dépendances sur bash (mais il ne fonctionnera jamais sur Windows, Mac ou Android de toute façon)
  • Et toutes les autres raisons mentionnées ci-dessus.
45
AnoE

Vous donnez suffisamment de raisons dans votre question pour exclure cette approche, mais la vraie question ici est "Y a-t-il quelque chose de plus manifestement mauvais ou sournois avec une telle approche?

Ma première chose ici, c'est que c'est inutile. Si votre constructeur ne termine jamais, aucune autre partie du programme ne peut obtenir une référence à l'objet built alors quelle est la logique derrière le mettre dans un constructeur au lieu d'une méthode régulière. Il m'est alors apparu que la seule différence serait que dans votre boucle, vous pouviez autoriser les références à this partiellement construites à s'échapper de la boucle. Bien que cela ne soit pas strictement limité à cette situation, il est garanti que si vous autorisez this à s'é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 parce que ce n'est pas quelque chose que la plupart des développeurs voudraient explorer.

8
JimmyJames

+1 Pour le moins d'étonnement, mais c'est un concept difficile à articuler pour les nouveaux développeurs. À un niveau pragmatique, il est difficile de déboguer les exceptions déclenchées au sein des constructeurs, si l'objet ne s'initialise pas, il n'existera pas pour que vous puissiez inspecter l'état ou vous 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 elle est un conteneur qui encapsule un ensemble de propriétés et de fonctionnalités que vous souhaitez appeler à partir du reste de votre logique d'application.

Si vous n'avez pas l'intention d'utiliser l'objet que vous "construisez", alors à quoi bon instancier l'objet en premier lieu? Avoir une boucle while (true) dans un constructeur signifie que vous n'avez jamais l'intention de le terminer ...

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, résultat:

En C #, n'exécutez pas de logique étendue ou sans fin dans les constructeurs car ... il existe de meilleures alternatives

1
Chris Schaller

Il n'y a rien de intrinsèquement mauvais à ce sujet. Cependant, ce qui sera important, c'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 vais pas parler de l'utilisation de while(true) dans un constructeur. Il n'y a absolument rien de mal à utiliser cette syntaxe dans un constructeur. Cependant, le concept de "démarrer une boucle de jeu dans le constructeur" est celui qui peut causer certains problèmes.

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 à l'utilisateur de votre code plus de flexibilité. Comme exemple des types de problèmes qui peuvent apparaître, 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 entière 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 existe des classes de programmes où vous voulez pouvoir tout allouer à l'avance, puis appeler la boucle de jeu.

L'une des règles de base de la programmation est d'utiliser l'outil le plus simple pour le travail. Ce n'est pas une règle stricte et rapide, mais elle est très utile. Si un appel de fonction aurait suffi, pourquoi s'embêter à construire un objet classe? Si je vois un outil compliqué plus puissant utilisé, je suppose que le développeur avait une raison à cela, et je commencerai à explorer quelles sortes de trucs étranges vous pourriez faire.

Il y a des cas où cela peut ê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 dans un programme beaucoup plus grand 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 d'exécuter la boucle de jeu dans un constructeur. Il suffit de traiter cela comme un cas spécial: ce serait valable car le programme global rend intuitif pour le prochain développeur de comprendre ce que vous avez fait et pourquoi.

0
Cort Ammon

Pourquoi une boucle while (true) dans un constructeur est-elle vraiment mauvaise?

Ce n'est pas mal du tout.

Pourquoi (ou est-ce du tout?) Considéré comme une mauvaise conception pour permettre au constructeur d'une classe de démarrer une boucle sans fin (c'est-à-dire une boucle de jeu)?

Pourquoi voudriez-vous jamais faire ça? 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 mentionné vous-même certaines des raisons évidentes contre cela, mais vous avez omis la plus grande:

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 c'est mieux, vous avez choisi le meilleur. Soit dit en passant, c'est pourquoi nous ne pas n'utilisez presque jamais GoTo.

0
Peter

J'ai l'impression que certains concepts se sont mélangés et ont donné lieu à 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) plutôt que while(true) car je peux définir le membre booléen sur false quelque part).

Vous pouvez extraire cette méthode de création et de démarrage du thread dans une fonction qui lui est propre, afin de séparer la construction de l'objet et le début réel 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 démarrer le thread d'un "objet actif", et ma préférence est un découplage de la construction et un démarrage pour un meilleur contrôle.

0
Bernhard Hiller

Votre objet me rappelle de commencer Tâches . L'objet de tâche a un constructeur, mais il existe également un tas de méthodes d'usine statiques comme: Task.Run, Task.Start Et 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, c'est que cette utilisation d'un constructeur les surprend. @ JörgWMittag dit qu'il rompt un contrat fondamental , ce qui, je suppose, signifie que Jörg est très surpris. Je suis d'accord et je n'ai rien à ajouter à cela.

Cependant, je veux suggérer que l'OP essaie des méthodes d'usine statiques. La surprise s'évanouit et il n'y a pas contrat fondamental en jeu ici. Les gens sont habitués aux méthodes d'usine statiques faisant des choses spéciales, et ils peuvent être nommés en conséquence.

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

0
Nathan Cooper