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?
c#
architecture
Samuel
quelle
quelle
var g = new Game {...}; g.MainLoop();
while(true)
Schleife in einen Eigenschaftssetter einzufügennew Game().RunNow = true
:?Antworten:
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.
quelle
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
):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:
bash
(aber es läuft sowieso nie unter Windows, Mac oder Android)quelle
fundamental contract of a constructor: to construct something
ist genau richtig .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,
this
die Schleife teilweise zu verlassen. Dies ist zwar nicht streng auf diese Situation beschränkt, aber es ist garantiert, dassthis
diese 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.
quelle
this
im Konstruktor zu machen - aber nur, wenn Sie sich im Konstruktor der am besten abgeleiteten Klasse befinden. Wenn Sie zwei Klassen habenA
undB
mitB : A
, wenn der Konstruktor vonA
Leak würdethis
, wären die Mitglieder vonB
immer noch nicht initialisiert (und virtuelle Methodenaufrufe oder Casts, die ausgeführt werden sollen,B
können basierend darauf Chaos anrichten).+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.
quelle
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.
quelle
GameTestResults testResults = runGameTests(tests, configuration);
eher alsGameTestResults testResults = new GameTestResults(tests, configuration);
.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.
quelle
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.Start
undTask.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 schreibenvar runningGame = Game.StartNew();
zu machen beginnen es ist sofort einfach.quelle
Das ist überhaupt nicht schlecht.
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
nichtso gut wie nie GoTo verwenden.quelle
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.
quelle