Warum ist eine while (true) -Schleife in einem Konstruktor eigentlich schlecht?

47

Obwohl es sich um eine allgemeine Frage handelt, ist mein Gültigkeitsbereich eher C #, da mir bekannt ist, dass Sprachen wie C ++ unterschiedliche Semantiken in Bezug auf Konstruktorausführung, Speicherverwaltung, undefiniertes Verhalten usw. haben.

Jemand hat mir eine interessante Frage gestellt, die für mich nicht einfach zu beantworten war.

Warum (oder wird es überhaupt als schlechtes Design angesehen?) Sollte ein Konstruktor einer Klasse eine nie endende Schleife (dh eine Spieleschleife) beginnen lassen?

Es gibt einige Konzepte, die dadurch gebrochen werden:

  • Wie das Prinzip des geringsten Erstaunens erwartet der Benutzer nicht, dass sich der Konstruktor so verhält.
  • Unit-Tests sind schwieriger, da Sie diese Klasse nicht erstellen oder injizieren können, da sie die Schleife nie verlässt.
  • Das Ende der Schleife (Spielende) ist dann konzeptionell die Zeit, in der der Konstruktor endet, was ebenfalls ungerade ist.
  • Technisch gesehen hat eine solche Klasse keine öffentlichen Mitglieder außer dem Konstruktor, was das Verständnis erschwert (insbesondere für Sprachen, in denen keine Implementierung verfügbar ist).

Und dann gibt es technische Probleme:

  • Der Konstruktor ist eigentlich nie fertig. Was passiert also mit GC hier? Ist dieses Objekt bereits in Gen 0?
  • Das Ableiten von einer solchen Klasse ist unmöglich oder zumindest sehr kompliziert, da der Basiskonstruktor niemals zurückkehrt

Gibt es etwas offensichtlicher Schlechtes oder Falsches an einem solchen Ansatz?

Samuel
quelle
61
Warum ist es gut? Wenn Sie einfach Ihre Hauptschleife auf eine Methode verschieben (sehr einfaches Refactoring), kann der Benutzer nicht überraschenden Code wie folgt schreiben:var g = new Game {...}; g.MainLoop();
Brandin
61
Ihre Frage enthält bereits 6 Gründe, warum Sie sie nicht verwenden sollten, und ich würde gerne einen einzigen zu ihren Gunsten sehen. Sie könnten sich auch fragen, warum es eine schlechte Idee ist, eine while(true)Schleife in einen Eigenschaftssetter einzufügen new Game().RunNow = true:?
Groo
38
Extreme Analogie: Warum sagen wir, was Hitler getan hat, war falsch? Abgesehen von rassistischer Diskriminierung, dem Beginn des Zweiten Weltkriegs, der Ermordung von Millionen von Menschen, Gewalt [etc etc aus über 50 anderen Gründen] hat er nichts falsch gemacht. Klar, wenn Sie eine willkürliche Liste von Gründen entfernen, warum etwas nicht stimmt, können Sie auch zu dem Schluss kommen, dass das Ding für buchstäblich alles gut ist.
Bakuriu
4
In Bezug auf die Garbage Collection (GC) (Ihr technisches Problem) gibt es nichts Besonderes. Die Instanz ist vorhanden, bevor der Konstruktorcode tatsächlich ausgeführt wird. In diesem Fall ist die Instanz immer noch erreichbar, sodass sie von GC nicht beansprucht werden kann. Wenn GC aktiviert wird, überlebt diese Instanz und bei jedem Aktivieren von GC wird das Objekt gemäß der üblichen Regel zur nächsten Generation hochgestuft. Ob ein GC stattfindet oder nicht, hängt davon ab, ob (genug) neue Objekte erstellt werden oder nicht.
Jeppe Stig Nielsen
8
Ich würde noch einen Schritt weiter gehen und sagen, dass while (true) schlecht ist, unabhängig davon, wo es verwendet wird. Dies impliziert, dass es keine klare und saubere Möglichkeit gibt, die Schleife zu stoppen. Sollte die Schleife in Ihrem Beispiel nicht anhalten, wenn das Spiel beendet wird, oder den Fokus verlieren?
Jon Anderson

Antworten:

250

Was ist der Zweck eines Konstruktors? Es wird ein neu erstelltes Objekt zurückgegeben. Was macht eine Endlosschleife? Es kehrt niemals zurück. Wie kann der Konstruktor ein neu erstelltes Objekt zurückgeben, wenn es überhaupt nicht zurückgegeben wird? Das geht nicht.

Ergo bricht eine Endlosschleife den grundlegenden Vertrag eines Konstrukteurs: etwas zu konstruieren.

Jörg W. Mittag
quelle
11
Vielleicht wollten sie dabei (wahr) mit einer Pause in die Mitte legen? Riskant, aber echt.
John Dvorak
2
Ich stimme zu, das ist zumindest richtig. aus akademischer Sicht. Gleiches gilt für jede Funktion, die etwas zurückgibt.
Doc Brown
61
Stellen Sie sich den armen Kerl vor, der eine Unterklasse dieser Klasse schafft ...
Newtopian
5
@ JanDvorak: Nun, das ist eine andere Frage. Das OP spezifizierte explizit "nie endend" und erwähnte eine Ereignisschleife.
Jörg W Mittag
4
@JörgWMittag na dann ja, steck das nicht in einen Konstruktor.
John Dvorak
45

Gibt es etwas offensichtlicher Schlechtes oder Falsches an einem solchen Ansatz?

Ja natürlich. Es ist unnötig, unerwartet, nutzlos, nicht relevant. Es verstößt gegen moderne Konzepte der Klassengestaltung (Zusammenhalt, Kopplung). Es bricht den Methodenvertrag (ein Konstruktor hat einen definierten Job und ist keine zufällige Methode). Es ist sicherlich nicht gut zu warten, zukünftige Programmierer werden viel Zeit damit verbringen, zu verstehen, was vor sich geht, und Gründe zu erraten, warum es so gemacht wurde.

Nichts davon ist ein "Bug" in dem Sinne, dass Ihr Code nicht funktioniert. Langfristig wird es jedoch wahrscheinlich enorme Nebenkosten verursachen (im Verhältnis zu den anfänglichen Kosten für das Schreiben des Codes), da die Wartung des Codes erschwert wird (dh das Hinzufügen von Tests, das Wiederverwenden, das Debuggen, das Erweitern usw. Sind schwierig) .).

Viele / modernste Verbesserungen der Softwareentwicklungsmethoden werden speziell durchgeführt, um den eigentlichen Prozess des Schreibens / Testens / Debuggens / Wartens von Software zu vereinfachen. All dies wird durch solche Dinge umgangen, bei denen Code zufällig platziert wird, weil er "funktioniert".

Leider werden Sie regelmäßig Programmierer treffen, die dies alles gar nicht kennen. Es funktioniert, das ist es.

Um mit einer Analogie abzuschließen (eine andere Programmiersprache, hier ist das Problem zu berechnen 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;

Was ist los mit dem ersten Ansatz? Es gibt 4 nach einigermaßen kurzer Zeit zurück; es ist richtig. Die Einwände (zusammen mit allen üblichen Gründen, die ein Entwickler geben kann, um sie zu widerlegen) sind:

  • Schwieriger zu lesen (aber hey, ein guter Programmierer kann es so leicht lesen, und wir haben es immer so gemacht; wenn Sie wollen, kann ich es in eine Methode umgestalten!)
  • Langsamer (dies ist jedoch nicht zeitkritisch, sodass wir dies ignorieren können. Außerdem ist es am besten, nur bei Bedarf zu optimieren!)
  • Verwendet viel mehr Ressourcen (ein neuer Prozess, mehr RAM usw. - aber dito, unser Server ist mehr als schnell genug)
  • Führt Abhängigkeiten von ein bash(aber es läuft sowieso nie unter Windows, Mac oder Android)
  • Und all die anderen oben genannten Gründe.
AnoE
quelle
5
"GC könnte sich während der Ausführung eines Konstruktors in einem außergewöhnlichen Sperrzustand befinden" - warum? Der Allokator reserviert zuerst und startet dann den Konstruktor als gewöhnliche Methode. Es ist nichts Besonderes dabei, oder? Und der Allokator muss ohnehin neue Allokationen (und diese könnten OOM-Situationen auslösen) im Konstruktor zulassen.
John Dvorak
1
@JanDvorak, Ich wollte nur darauf hinweisen, dass es könnte einige besondere Verhalten „irgendwo“ sein , während in einem Konstruktor, aber sie richtig sind, das Beispiel , das ich gepflückt war unrealistisch. Entfernt.
AnoE
Ich mag Ihre Erklärung sehr und würde gerne beide Antworten als Antworten markieren (mindestens eine positive Bewertung haben). Die Analogie ist schön und Ihre Überlegungen und Erklärungen zu den Nebenkosten sind es auch, aber ich betrachte sie als zusätzliche Anforderungen an den Code (genau wie meine Argumente). Gegenwärtiger Stand der Technik ist das Testen von Code, daher ist die Testbarkeit usw. eine de-facto-Anforderung. Aber @ JörgWMittags Aussage fundamental contract of a constructor: to construct somethingist genau richtig .
Samuel
Die Analogie ist nicht so gut. Wenn ich das sehe, würde ich erwarten, irgendwo einen Kommentar zu sehen, warum Sie Bash verwenden, um die Mathematik zu machen, und je nach Ihrem Grund kann es völlig in Ordnung sein. Dasselbe würde für das OP nicht funktionieren, da Sie im Grunde überall, wo Sie den Konstruktor "// Hinweis: Beim Erstellen dieses Objekts wird eine Ereignisschleife gestartet, die niemals zurückkehrt", Entschuldigungen in Kommentare einfügen müssten.
Brandin
4
"Leider trifft man regelmäßig Programmierer, die das alles gar nicht kennen. Es funktioniert, das ist es." So traurig, aber so wahr. Ich denke, ich kann für uns alle sprechen, wenn ich sage, dass die einzige wesentliche Belastung in unserem Berufsleben diese Menschen sind.
Leichtigkeit Rennen mit Monica
8

Sie geben in Ihrer Frage genügend Gründe an, um diesen Ansatz auszuschließen, aber die eigentliche Frage lautet hier: "Gibt es etwas offensichtlicher Schlechtes oder Falsches an einem solchen Ansatz?"

Mein erster Gedanke hier war, dass dies sinnlos ist. Wenn Ihr Konstruktor nie beendet wird, kann kein anderer Teil des Programms einen Verweis auf das konstruierte Objekt erhalten. Welche Logik steckt also dahinter, es in einem Konstruktor anstelle einer regulären Methode zu platzieren. Dann kam mir der Gedanke, dass der einzige Unterschied darin bestehen würde, dass Sie in Ihrer Schleife Referenzen erlauben könnten, thisdie Schleife teilweise zu verlassen. Dies ist zwar nicht streng auf diese Situation beschränkt, aber es ist garantiert, dass thisdiese Referenzen immer auf ein Objekt verweisen, das nicht vollständig konstruiert ist , wenn Sie es zulassen, zu flüchten.

Ich weiß nicht, ob die Semantik um diese Art von Situation in C # gut definiert ist, aber ich würde behaupten, dass es keine Rolle spielt, da es nicht etwas ist, mit dem sich die meisten Entwickler beschäftigen möchten.

JimmyJames
quelle
Ich würde davon ausgehen, dass der Konstruktor ausgeführt wird, nachdem das Objekt erstellt wurde, sodass in einer vernünftigen Sprache undicht ist. Dies sollte ein perfekt definiertes Verhalten sein.
Mroman
@mroman: Es könnte in Ordnung sein, einen Fehler thisim Konstruktor zu machen - aber nur, wenn Sie sich im Konstruktor der am besten abgeleiteten Klasse befinden. Wenn Sie zwei Klassen haben Aund Bmit B : A, wenn der Konstruktor von ALeak würde this, wären die Mitglieder von Bimmer noch nicht initialisiert (und virtuelle Methodenaufrufe oder Casts, die ausgeführt werden sollen, Bkönnen basierend darauf Chaos anrichten).
Hoffmale
1
Ich denke in C ++ kann das verrückt sein, ja. In anderen Sprachen erhalten Mitglieder einen Standardwert, bevor ihnen ihre tatsächlichen Werte zugewiesen werden. Während es also dumm ist, solchen Code tatsächlich zu schreiben, ist das Verhalten immer noch gut definiert. Wenn Sie Methoden aufrufen, die diese Member verwenden, stoßen Sie möglicherweise auf NullPointerExceptions oder auf falsche Berechnungen (da die Zahlen standardmäßig 0 sind) oder ähnliches, aber es ist technisch deterministisch, was passiert.
mroman
@mroman Kannst du eine definitive Referenz für die CLR finden, die zeigt, dass dies der Fall ist?
JimmyJames
Nun, Sie können Mitgliederwerte zuweisen, bevor der Konstruktor ausgeführt wird, sodass das Objekt in einem gültigen Zustand vorliegt, in dem den Mitgliedern Standardwerte oder Werte zugewiesen wurden, bevor der Konstruktor überhaupt ausgeführt wurde. Sie brauchen den Konstruktor nicht, um Mitglieder zu initialisieren. Lassen Sie mich sehen, ob ich das irgendwo in den Dokumenten finde.
Mroman
1

+1 Für das geringste Erstaunen, aber dies ist ein schwieriges Konzept, um neue Entwickler zu artikulieren. Auf einer pragmatischen Ebene ist es schwierig, in Konstruktoren ausgelöste Ausnahmen zu debuggen. Wenn das Objekt nicht initialisiert werden kann, können Sie den Status dieses Konstruktors nicht überprüfen oder von außerhalb des Konstruktors protokollieren.

Wenn Sie diese Art von Codemuster benötigen, verwenden Sie stattdessen statische Methoden für die Klasse.

Es gibt einen Konstruktor, der die Initialisierungslogik bereitstellt, wenn ein Objekt aus einer Klassendefinition instanziiert wird. Sie erstellen diese Objektinstanz als Container, der eine Reihe von Eigenschaften und Funktionen enthält, die Sie vom Rest Ihrer Anwendungslogik aus aufrufen möchten.

Wenn Sie nicht beabsichtigen, das Objekt zu verwenden, das Sie "konstruieren", wozu dann das Objekt überhaupt instanziieren? Wenn Sie eine while (true) -Schleife in einem Konstruktor haben, bedeutet dies, dass Sie niemals beabsichtigen, diese abzuschließen ...

C # ist eine sehr umfangreiche objektorientierte Sprache mit vielen verschiedenen Konstrukten und Paradigmen, mit denen Sie sich vertraut machen, Ihre Werkzeuge kennen und wann Sie sie verwenden können.

Führen Sie in C # keine erweiterte oder nie endende Logik innerhalb von Konstruktoren aus, da ... es bessere Alternativen gibt

Chris Schaller
quelle
1
sein scheint nicht zu bieten alles wesentliche über gemacht Punkte und erläutert vor 9 Antworten
gnat
1
Hey, ich musste es versuchen :) Ich dachte, es war wichtig hervorzuheben, dass es zwar keine Regel gibt, die dies verhindern, aber nicht aufgrund der Tatsache, dass es einfachere Wege gibt. Die meisten anderen Antworten konzentrieren sich auf die technischen Gründe, warum es schlecht ist, aber es ist schwer, es an einen neuen Entwickler zu verkaufen, der sagt, "aber es wird kompiliert, also kann ich es einfach so belassen"
Chris Schaller
0

Daran ist nichts Schlechtes. Wichtig ist jedoch die Frage, warum Sie sich für dieses Konstrukt entschieden haben. Was haben Sie damit erreicht?

Zunächst werde ich nicht über die Verwendung while(true)in einem Konstruktor sprechen . Es ist absolut nichts Falsches daran, diese Syntax in einem Konstruktor zu verwenden. Das Konzept des "Startens einer Spieleschleife im Konstruktor" kann jedoch einige Probleme verursachen.

In diesen Situationen ist es sehr verbreitet, dass der Konstruktor einfach das Objekt erstellt und eine Funktion hat, die die Endlosschleife aufruft. Dies gibt dem Benutzer Ihres Codes mehr Flexibilität. Als Beispiel für mögliche Probleme können Sie die Verwendung Ihres Objekts einschränken. Wenn jemand ein Mitgliedsobjekt haben möchte, das "das Spielobjekt" in seiner Klasse ist, muss er bereit sein, die gesamte Spielschleife in seinem Konstruktor auszuführen, da er das Objekt erstellen muss. Dies könnte zu gefolterter Codierung führen, um dies zu umgehen. Im Allgemeinen können Sie Ihr Spielobjekt nicht vorab zuweisen und später ausführen. Bei einigen Programmen ist dies kein Problem, aber es gibt Programmklassen, bei denen Sie in der Lage sein möchten, alles im Voraus zuzuweisen und dann die Spieleschleife aufzurufen.

Eine der Faustregeln beim Programmieren ist, das einfachste Werkzeug für den Job zu verwenden. Es ist keine feste Regel, aber sehr nützlich. Wenn ein Funktionsaufruf ausreichend gewesen wäre, warum sollte man sich die Mühe machen, ein Klassenobjekt zu konstruieren? Wenn ich sehe, dass ein leistungsfähigeres, komplizierteres Tool verwendet wird, hat der Entwickler vermutlich einen Grund dafür und ich werde untersuchen, welche seltsamen Tricks Sie möglicherweise machen.

Es gibt Fälle, in denen dies sinnvoll sein kann. Es kann Fälle geben, in denen es für Sie wirklich intuitiv ist, eine Spieleschleife im Konstruktor zu haben. In diesen Fällen rennst du mit! Beispielsweise kann Ihre Spieleschleife in ein viel größeres Programm eingebettet sein, das Klassen erstellt, um einige Daten zu verkörpern. Wenn Sie eine Spieleschleife ausführen müssen, um diese Daten zu generieren, kann es sehr sinnvoll sein, die Spieleschleife in einem Konstruktor auszuführen. Betrachten Sie dies einfach als einen Sonderfall: Dies ist zulässig, da das übergreifende Programm es dem nächsten Entwickler intuitiv macht, zu verstehen, was Sie getan haben und warum.

Cort Ammon
quelle
Wenn für die Erstellung Ihres Objekts eine Spieleschleife erforderlich ist, setzen Sie sie stattdessen in eine Factory-Methode. GameTestResults testResults = runGameTests(tests, configuration);eher als GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751
@immibis Ja, das stimmt, mit Ausnahme der Fälle, in denen dies nicht zumutbar ist. Wenn Sie mit einem reflexionsbasierten System arbeiten, das Ihr Objekt für Sie instanziiert, können Sie möglicherweise keine Factory-Methode erstellen.
Cort Ammon
0

Mein Eindruck ist, dass einige Konzepte verwechselt wurden und zu einer nicht so gut formulierten Frage führten.

Der Konstruktor einer Klasse kann einen neuen Thread startet das wird „nie“ Ende (obwohl ich es vorziehen , zu verwenden , while(_KeepRunning)über , while(true)weil ich das boolean Mitglied auf false irgendwo einstellen kann).

Sie können diese Methode zum Erstellen und Starten des Threads in eine eigene Funktion extrahieren, um die Objektkonstruktion und den tatsächlichen Arbeitsbeginn voneinander zu trennen. Ich bevorzuge diese Methode, da ich eine bessere Kontrolle über den Zugriff auf Ressourcen habe.

Sie können sich auch das "Active Object Pattern" ansehen. Letztendlich war die Frage wohl darauf gerichtet, wann der Thread eines "aktiven Objekts" gestartet werden soll, und ich bevorzuge eine Abkopplung von Konstruktion und Start, um eine bessere Kontrolle zu erreichen.

Bernhard Hiller
quelle
0

Ihr Objekt erinnert mich an das Starten von Aufgaben . Das Task - Objekt hat einen Konstruktor, aber es gibt auch eine Reihe von statischen Factory - Methoden wie: Task.Run, Task.Startund Task.Factory.StartNew.

Diese sind dem, was Sie versuchen, sehr ähnlich, und es ist wahrscheinlich, dass dies die "Konvention" ist. Das Hauptproblem, das Menschen zu haben scheinen, ist, dass diese Verwendung eines Konstruktors sie überrascht. @ JörgWMittag sagt es bricht ein grundsätzlicher Vertrag , was Jörg wohl sehr überrascht. Ich stimme zu und habe dem nichts hinzuzufügen.

Ich möchte jedoch vorschlagen, dass das OP statische Factory-Methoden verwendet. Die Überraschung verschwindet und es steht hier kein grundlegender Vertrag auf dem Spiel. Die Leute sind es gewohnt, mit statischen Factory-Methoden besondere Dinge zu tun, und sie können entsprechend benannt werden.

Sie können Konstrukteure schaffen , die feinkörnige Steuerung erlauben (wie @Brandin schon sagt, so etwas wie var g = new Game {...}; g.MainLoop();, dass die Nutzer unterbringen , die das Spiel nicht tun wollen , sofort starten, und vielleicht wollen es passieren um die erste. Und man kann so etwas schreiben var runningGame = Game.StartNew();zu machen beginnen es ist sofort einfach.

Nathan Cooper
quelle
Das heißt, Jörg ist von Grund auf überrascht, was bedeutet, dass eine solche Überraschung ein Grundschmerz ist.
Steve Jessop
0

Warum ist eine while (true) -Schleife in einem Konstruktor eigentlich schlecht?

Das ist überhaupt nicht schlecht.

Warum (oder wird es überhaupt als schlechtes Design angesehen?) Sollte ein Konstruktor einer Klasse eine nie endende Schleife (dh eine Spieleschleife) beginnen lassen?

Warum würdest du das jemals tun wollen? Ernsthaft. Diese Klasse hat eine einzige Funktion: den Konstruktor. Wenn alles, was Sie wollen, eine einzelne Funktion ist, haben wir deshalb Funktionen, keine Konstruktoren.

Sie haben selbst einige offensichtliche Gründe dafür genannt, aber den größten haben Sie ausgelassen:

Es gibt bessere und einfachere Möglichkeiten, um dasselbe zu erreichen.

Wenn es zwei Möglichkeiten gibt und eine besser ist, spielt es keine Rolle, um wie viel es besser ist, Sie haben die bessere gewählt. Im übrigen ist das , warum wir nicht so gut wie nie GoTo verwenden.

Peter
quelle
-1

Der Zweck eines Konstruktors ist es, Ihr Objekt zu konstruieren. Bevor der Konstruktor abgeschlossen ist, ist es grundsätzlich unsicher, Methoden dieses Objekts zu verwenden. Natürlich können wir Methoden des Objekts innerhalb des Konstruktors aufrufen, aber dann müssen wir jedes Mal sicherstellen, dass dies für das Objekt in seinem aktuellen halbgebackenen Zustand gültig ist.

Indem Sie indirekt die gesamte Logik in den Konstruktor einbauen, tragen Sie mehr Last dieser Art auf sich. Dies deutet darauf hin, dass Sie den Unterschied zwischen Initialisierung und Verwendung nicht gut genug entworfen haben.

Hagen von Eitzen
quelle
1
Dies scheint nichts Wesentliches zu bieten über Punkte, die in den vorherigen 8 Antworten gemacht und erklärt wurden
Mücke