Warum sollte ich separate Initialisierungs- und Bereinigungsmethoden verwenden, anstatt Logik in den Konstruktor und Destruktor für Motorkomponenten einzufügen?

9

Ich arbeite an meiner eigenen Spiel-Engine und entwerfe derzeit meine Manager. Ich habe gelesen, dass für die Speicherverwaltung die Verwendung von Init()und CleanUp()Funktionen besser sind als die Verwendung von Konstruktoren und Destruktoren.

Ich habe nach C ++ - Codebeispielen gesucht, um zu sehen, wie diese Funktionen funktionieren und wie ich sie in meine Engine implementieren kann. Wie funktioniert Init()und CleanUp()wie kann ich sie in meine Engine implementieren?

Friso
quelle
Informationen zu C ++ finden Sie unter stackoverflow.com/questions/3786853/…. Hauptgründe für die Verwendung von Init () sind 1) Verhindern von Ausnahmen und Abstürzen im Konstruktor mit Hilfsfunktionen 2) Verwenden virtueller Methoden aus der abgeleiteten Klasse 3) Umgehen kreisförmiger Abhängigkeiten 4) als private Methode, um Codeduplizierungen zu vermeiden
brita_

Antworten:

12

Eigentlich ist es ziemlich einfach:

Anstatt einen Konstruktor zu haben, der Ihr Setup ausführt,

// c-family pseudo-code
public class Thing {
    public Thing (a, b, c, d) { this.x = a; this.y = b; /* ... */ }
}

... lassen Sie Ihren Konstruktor wenig oder gar nichts tun und schreiben Sie eine Methode namens .initoder .initialize, die das tut, was Ihr Konstruktor normalerweise tun würde.

public class Thing {
    public Thing () {}
    public void initialize (a, b, c, d) {
        this.x = a; /*...*/
    }
}

Also jetzt anstatt einfach so zu gehen wie:

Thing thing = new Thing(1, 2, 3, 4);

Du kannst gehen:

Thing thing = new Thing();

thing.doSomething();
thing.bind_events(evt_1, evt_2);
thing.initialize(1, 2, 3, 4);

Der Vorteil besteht darin, dass Sie die Abhängigkeitsinjektion / -inversion der Steuerung jetzt einfacher in Ihren Systemen verwenden können.

Anstatt zu sagen

public class Soldier {
    private Weapon weapon;

    public Soldier (name, x, y) {
        this.weapon = new Weapon();
    }
}

Sie können den Soldaten bauen, geben Sie ihm ein Ausrüsten Verfahren, in dem Sie die Hand ihm eine Waffe, und dann der ganze Rest der Konstruktor - Funktionen aufrufen.

Anstatt also Feinde zu unterordnen, bei denen ein Soldat eine Pistole und ein anderer ein Gewehr und ein anderer eine Schrotflinte hat, und das ist der einzige Unterschied, können Sie einfach sagen:

Soldier soldier1 = new Soldier(),
        soldier2 = new Soldier(),
        soldier3 = new Soldier();

soldier1.equip(new Pistol());
soldier2.equip(new Rifle());
soldier3.equip(new Shotgun());

soldier1.initialize("Bob",  32,  48);
soldier2.initialize("Doug", 57, 200);
soldier3.initialize("Mike", 92,  30);

Gleiches gilt für die Zerstörung. Wenn Sie spezielle Anforderungen haben (Entfernen von Ereignis-Listenern, Entfernen von Instanzen aus Arrays / Strukturen, mit denen Sie arbeiten usw.), rufen Sie diese manuell auf, damit Sie genau wissen, wann und wo in dem Programm, das gerade ausgeführt wurde.

BEARBEITEN


Wie Kryotan weiter unten ausgeführt hat, beantwortet dies das "Wie" des ursprünglichen Beitrags , macht aber kein wirklich gutes "Warum".

Wie Sie wahrscheinlich in der obigen Antwort sehen können, gibt es möglicherweise keinen großen Unterschied zwischen:

var myObj = new Object();
myObj.setPrecondition(1);
myObj.setOtherPrecondition(2);
myObj.init();

und schreiben

var myObj = new Object(1,2);

während nur eine größere Konstruktorfunktion haben.
Es gibt ein Argument für Objekte mit 15 oder 20 Vorbedingungen, die es einem Konstruktor sehr, sehr schwer machen würden, damit zu arbeiten, und es würde das Sehen und Erinnern erleichtern, indem diese Dinge in die Schnittstelle gezogen werden , damit Sie sehen können, wie die Instanziierung funktioniert, eine Ebene höher.

Die optionale Konfiguration von Objekten ist eine natürliche Erweiterung davon. Optional können Sie Werte auf der Schnittstelle festlegen, bevor das Objekt ausgeführt wird.
JS hat einige großartige Abkürzungen für diese Idee, die in stärker typisierten c-ähnlichen Sprachen einfach fehl am Platz zu sein scheinen.

Das heißt, wenn Sie es mit einer so langen Argumentliste in Ihrem Konstruktor zu tun haben, besteht die Möglichkeit, dass Ihr Objekt zu groß ist und so viel tut, wie es ist. Auch dies ist eine persönliche Präferenzsache, und es gibt weit und breit Ausnahmen. Wenn Sie jedoch 20 Dinge an ein Objekt übergeben, stehen die Chancen gut, dass Sie einen Weg finden, dieses Objekt weniger zu machen, indem Sie kleinere Objekte erstellen .

Ein relevanterer und allgemein anwendbarer Grund wäre, dass die Initialisierung eines Objekts auf asynchronen Daten beruht, über die Sie derzeit nicht verfügen.

Sie wissen, dass Sie das Objekt benötigen, also werden Sie es trotzdem erstellen, aber damit es ordnungsgemäß funktioniert, benötigt es Daten vom Server oder von einer anderen Datei, die es jetzt laden muss.

Auch hier ist es für das Konzept nicht wirklich wichtig, ob Sie die benötigten Daten an einen gigantischen Init übergeben oder eine Schnittstelle aufbauen, sondern für die Schnittstelle Ihres Objekts und das Design Ihres Systems ...

In Bezug auf das Erstellen des Objekts können Sie jedoch Folgendes tun:

var obj_w_async_dependencies = new Object();
async_loader.load(obj_w_async_dependencies.async_data, obj_w_async_dependencies);

async_loader Möglicherweise wird ein Dateiname oder ein Ressourcenname oder was auch immer übergeben. Laden Sie diese Ressource - möglicherweise werden Audiodateien oder Bilddaten geladen, oder es werden gespeicherte Zeichenstatistiken geladen ...

... und dann würde es diese Daten zurückspeisen obj_w_async_dependencies.init(result);.

Diese Art von Dynamik findet man häufig in Web-Apps.
Nicht unbedingt in der Konstruktion eines Objekts, für Anwendungen auf höherer Ebene: Beispielsweise können Galerien sofort geladen und initialisiert werden und dann Fotos anzeigen, während sie einströmen - das ist eigentlich keine asynchrone Initialisierung, aber dort, wo sie häufiger zu sehen ist in JavaScript-Bibliotheken.

Ein Modul hängt möglicherweise von einem anderen ab, sodass die Initialisierung dieses Moduls möglicherweise verschoben wird, bis das Laden der abhängigen Elemente abgeschlossen ist.

Betrachten Sie in Bezug auf spielspezifische Instanzen eine tatsächliche GameKlasse.

Warum können wir nicht .startoder .runim Konstruktor aufrufen ?
Ressourcen müssen geladen werden - der Rest von allem wurde so ziemlich definiert und ist in Ordnung, aber wenn wir versuchen, das Spiel ohne Datenbankverbindung oder ohne Texturen oder Modelle oder Sounds oder Levels auszuführen, wird es nicht so sein ein besonders interessantes Spiel ...

... na und, dann ist der Unterschied zwischen dem, was wir von einem typischen sehen Game, außer dass wir seiner "Go-Ahead" -Methode einen Namen geben, der interessanter ist als .init(oder umgekehrt, die Initialisierung noch weiter auseinander zu brechen, um das Laden zu trennen). Einrichten der geladenen Dinge und Ausführen des Programms, wenn alles eingerichtet wurde).

Norguard
quelle
2
" Sie würden sie dann manuell aufrufen, damit Sie genau wissen, wann und wo in dem Programm, das gerade ausgeführt wurde. " Das einzige Mal in C ++, in dem ein Destruktor implizit aufgerufen wird, ist für ein Stapelobjekt (oder global). Heap-zugewiesene Objekte erfordern eine explizite Zerstörung. Es ist also immer klar, wann die Zuordnung des Objekts aufgehoben wird.
Nicol Bolas
6
Es ist nicht genau richtig zu sagen, dass Sie diese separate Methode benötigen, um die Injektion verschiedener Waffentypen zu ermöglichen, oder dass dies der einzige Weg ist, um die Verbreitung von Unterklassen zu vermeiden. Sie können die Waffeninstanzen über den Konstruktor übergeben! Es ist also eine -1 von mir, da dies kein überzeugender Anwendungsfall ist.
Kylotan
1
-1 Auch von mir aus den gleichen Gründen wie Kylotan. Sie machen kein sehr überzeugendes Argument, all dies hätte mit Konstruktoren geschehen können.
Paul Manta
Ja, dies könnte mit Konstruktoren und Destruktoren erreicht werden. Er fragte nach Anwendungsfällen einer Technik und warum und wie, anstatt wie sie funktionieren oder warum sie funktionieren. Ein komponentenbasiertes System mit Setter- / Bindungsmethoden im Vergleich zu vom Konstruktor übergebenen Parametern für DI hängt wirklich davon ab, wie Sie Ihre Schnittstelle erstellen möchten. Wenn Ihr Objekt jedoch 20 IOC-Komponenten benötigt, möchten Sie ALLE in Ihren Konstruktor einfügen? Können Sie? Natürlich kannst du. Sollten Sie? Vielleicht, vielleicht nicht. Wenn Sie sich nicht , dann brauchen Sie ein .init, vielleicht auch nicht, aber wahrscheinlich. Ergo, gültiger Fall.
Norguard
1
@Kylotan Ich habe den Titel der Frage bearbeitet, um zu fragen, warum. Das OP fragte nur "wie". Ich habe die Frage um das "Warum" erweitert, da das "Wie" für jeden, der etwas über Programmierung weiß, trivial ist ("Verschieben Sie einfach die Logik, die Sie in den CTOR haben würden, in eine separate Funktion und rufen Sie sie auf") und das "Warum". ist interessanter / allgemeiner.
Tetrad
17

Was auch immer Sie lesen, dass Init und CleanUp besser sind, sollte Ihnen auch sagen, warum. Artikel, die ihre Behauptungen nicht rechtfertigen, sind nicht lesenswert.

Durch separate Initialisierungs- und Herunterfahrfunktionen können Systeme einfacher eingerichtet und zerstört werden, da Sie auswählen können, in welcher Reihenfolge sie aufgerufen werden sollen, während Konstruktoren genau dann aufgerufen werden, wenn das Objekt erstellt wird, und Destruktoren aufgerufen werden, wenn das Objekt zerstört wird. Wenn Sie komplexe Abhängigkeiten zwischen zwei Objekten haben, müssen diese häufig vorhanden sein, bevor sie sich selbst einrichten. Dies ist jedoch häufig ein Zeichen für ein schlechtes Design an anderer Stelle.

Einige Sprachen haben keine Destruktoren, auf die Sie sich verlassen können, da Referenzzählung und Speicherbereinigung es schwieriger machen zu wissen, wann das Objekt zerstört wird. In diesen Sprachen benötigen Sie fast immer eine Methode zum Herunterfahren / Bereinigen, und einige möchten die Init-Methode für die Symmetrie hinzufügen.

Kylotan
quelle
Vielen Dank, aber ich suche hauptsächlich nach Beispielen, da der Artikel sie nicht hatte. Ich entschuldige mich, wenn meine Frage dazu unklar war, aber ich habe sie jetzt bearbeitet.
Friso
3

Ich denke, der beste Grund ist: Pooling zuzulassen.
Wenn Sie Init und CleanUp haben, können Sie, wenn ein Objekt getötet wird, einfach CleanUp aufrufen und das Objekt auf einen Objektstapel des gleichen Typs schieben: einen 'Pool'.
Wenn Sie dann ein neues Objekt benötigen, können Sie ein Objekt aus dem Pool entfernen ODER wenn der Pool leer ist - zu schlecht - müssen Sie ein neues erstellen. Dann rufen Sie Init für dieses Objekt auf.
Eine gute Strategie besteht darin, den Pool vorab zu füllen, bevor das Spiel mit einer „guten“ Anzahl von Objekten beginnt, sodass Sie während des Spiels niemals ein gepooltes Objekt erstellen müssen.
Wenn Sie andererseits 'new' verwenden und einfach aufhören, auf ein Objekt zu verweisen, wenn es für Sie nicht von Nutzen ist, erstellen Sie Müll, der irgendwann wieder gesammelt werden muss. Diese Erinnerung ist besonders schlecht für Single-Threaded-Sprachen wie Javascript, bei denen der Garbage Collector den gesamten Code stoppt, wenn er auswertet, dass er den Speicher der nicht mehr verwendeten Objekte abrufen muss. Das Spiel hängt einige Millisekunden und das Spielerlebnis wird beeinträchtigt.
- Sie haben bereits verstanden -: Wenn Sie alle Ihre Objekte zusammenfassen, erfolgt keine Erinnerung, daher keine zufällige Verlangsamung mehr.

Es ist auch viel schneller, init für ein Objekt aus dem Pool aufzurufen, als Speicher zuzuweisen + ein neues Objekt zu initiieren.
Die Geschwindigkeitsverbesserung ist jedoch weniger wichtig, da die Objekterstellung häufig kein Leistungsengpass darstellt ... Mit wenigen Ausnahmen wie hektischen Spielen, Partikel-Engines oder physikalischen Engines, die intensiv 2D / 3D-Vektoren für ihre Berechnungen verwenden. Hier werden sowohl die Geschwindigkeit als auch die Speicherbereinigung durch die Verwendung eines Pools erheblich verbessert.

Rq: Möglicherweise benötigen Sie keine CleanUp-Methode für Ihre gepoolten Objekte, wenn Init () alles zurücksetzt.

Bearbeiten: Die Beantwortung dieses Beitrags hat mich motiviert, einen kleinen Artikel über das Pooling in Javascript fertigzustellen .
Sie finden es hier, wenn Sie interessiert sind:
http://gamealchemist.wordpress.com/

GameAlchemist
quelle
1
-1: Sie müssen dies nicht nur tun, um einen Pool von Objekten zu haben. Sie können dies tun, indem Sie die Zuordnung von der Konstruktion durch Platzierung neu und die Freigabe von der Löschung durch einen expliziten Destruktoraufruf trennen. Dies ist also kein gültiger Grund, Konstruktoren / Destruktoren von einer Initialisierungsmethode zu trennen.
Nicol Bolas
Die neue Platzierung ist C ++ -spezifisch und auch ein bisschen esoterisch.
Kylotan
+1 Möglicherweise ist dies in c + auch anders möglich. Aber nicht in anderen Sprachen ... und dies ist wahrscheinlich nur der Grund, warum ich die Init-Methode für Spielobjekte verwenden würde.
Kikaimaru
1
@Nicol Bolas: Ich denke du reagierst überreagiert. Die Tatsache, dass es andere Möglichkeiten zum Pooling gibt (Sie erwähnen eine komplexe, für C ++ spezifische), macht die Tatsache nicht ungültig, dass die Verwendung eines separaten Init eine nette und einfache Möglichkeit ist, das Pooling in vielen Sprachen zu implementieren. Meine Vorlieben für GameDev beziehen sich auf allgemeinere Antworten.
GameAlchemist
@ VincentPiel: Wie ist die Verwendung der Platzierung neu und so "komplex" in C ++? Wenn Sie in einer GC-Sprache arbeiten, stehen die Chancen gut, dass Objekte GC-basierte Objekte enthalten. Müssen sie also auch jeden von ihnen befragen? Das Erstellen eines neuen Objekts umfasst daher das Abrufen einer Reihe neuer Objekte aus Pools.
Nicol Bolas
0

Ihre Frage ist umgekehrt ... Historisch gesehen ist die sachdienlichere Frage:

Warum ist + Bau intialisation verschmelzt , also warum nicht wir tun separat diese Schritte? Sicher geht das gegen SoC ?

Für C ++ besteht die Absicht von RAII darin, die Ressourcenerfassung und -freigabe direkt an die Objektlebensdauer zu binden, in der Hoffnung, dass dies die Ressourcenfreigabe gewährleistet. Macht es? Teilweise. Es wird zu 100% im Kontext von stapelbasierten / automatischen Variablen erfüllt, wobei das Verlassen des zugehörigen Bereichs automatisch Destruktoren aufruft / diese Variablen freigibt (daher das Qualifikationsmerkmal automatic). Bei Heap-Variablen bricht dieses sehr nützliche Muster jedoch leider zusammen, da Sie immer noch gezwungen sind, explizit aufzurufen delete, um den Destruktor auszuführen. Wenn Sie dies vergessen, werden Sie immer noch von dem gebissen, was RAII zu lösen versucht. Im Kontext von Heap-zugewiesenen Variablen bietet C ++ einen begrenzten Nutzen gegenüber C ( deletevs.free()) während die Konstruktion mit der Initialisierung in Verbindung gebracht wird, was sich negativ auf Folgendes auswirkt:

Das Erstellen eines Objektsystems für Spiele / Simulationen in C wird dringend empfohlen, da es durch ein tieferes Verständnis der Annahmen, die C ++ und spätere klassische OO-Sprachen treffen, viel Licht auf die Einschränkungen von RAII und anderen solchen OO-zentrierten Mustern werfen wird (Denken Sie daran, dass C ++ als in C integriertes OO-System begann.)

Techniker
quelle