Perché dovrei usare l'inizializzazione separata e metodi di pulizia invece di mettere la logica nel costruttore e nel distruttore per i componenti del motore?


9

Sto lavorando sul mio motore di gioco e attualmente sto progettando i miei manager. Ho letto che per la gestione della memoria, usare Init()e le CleanUp()funzioni sono meglio che usare costruttori e distruttori.

Ho cercato esempi di codice C ++, per vedere come funzionano queste funzioni e come posso implementarle nel mio motore. Come funziona Init()e CleanUp()il lavoro, e come posso implementare nel mio motore?



Per C ++, consultare stackoverflow.com/questions/3786853/… I motivi principali per utilizzare Init () sono 1) Prevenire eccezioni e arresti anomali nel costruttore con funzioni di supporto 2) Essere in grado di utilizzare metodi virtuali dalla classe derivata 3) Dipendenze circolari di elusione 4) come metodo privato per evitare la duplicazione del codice
brita_

Risposte:


12

È piuttosto semplice, in realtà:

Invece di avere un costruttore che fa il tuo setup,

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

... chiedi al tuo costruttore di fare poco o niente e di scrivere un metodo chiamato .inito .initialize, che farebbe quello che farebbe normalmente il tuo costruttore.

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

Quindi ora invece di andare semplicemente come:

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

Puoi andare:

Thing thing = new Thing();

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

Il vantaggio è che ora puoi usare la dipendenza da iniezione / inversione di controllo più facilmente nei tuoi sistemi.

Invece di dire

public class Soldier {
    private Weapon weapon;

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

È possibile costruire il soldato, gli danno un metodo di equipaggiare, dove consegnare l'un'arma, e quindi chiamare tutto il resto delle funzioni di costruzione.

Quindi ora, invece di sottoclassare i nemici in cui un soldato ha una pistola e un altro ha un fucile e un altro ha un fucile, e questa è l'unica differenza, puoi solo dire:

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);

Lo stesso accordo con la distruzione. Se hai esigenze particolari (rimozione di listener di eventi, rimozione di istanze da array / qualsiasi struttura con cui stai lavorando, ecc.), Le chiameresti manualmente, in modo da sapere esattamente quando e dove nel programma si stava verificando.

MODIFICARE


Come ha sottolineato Kryotan, di seguito, questo risponde al "How" del post originale , ma in realtà non fa un buon lavoro di "Why".

Come probabilmente vedrai nella risposta sopra, potrebbe non esserci molta differenza tra:

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

e scrivere

var myObj = new Object(1,2);

pur avendo solo una funzione di costruzione più grande.
C'è un argomento da fare per gli oggetti che hanno 15 o 20 pre-condizioni, il che renderebbe molto, molto difficile lavorare con un costruttore, e renderebbe le cose più facili da vedere e ricordare, estraendo quelle cose nell'interfaccia , in modo da poter vedere come funziona l'istanza, un livello superiore.

La configurazione opzionale degli oggetti è un'estensione naturale di ciò; facoltativamente impostare i valori sull'interfaccia, prima di far funzionare l'oggetto.
JS ha alcune scorciatoie fantastiche per questa idea, che sembrano fuori posto in linguaggi di tipo c più forti.

Detto questo, è probabile che, se hai a che fare con un elenco di argomenti così a lungo nel tuo costruttore, che il tuo oggetto è troppo grande e fa troppo, così come è. Ancora una volta, questa è una preferenza personale, e ci sono eccezioni in lungo e in largo, ma se stai passando 20 cose in un oggetto, è probabile che tu possa trovare un modo per fare in modo che quell'oggetto faccia meno, creando oggetti più piccoli .

Un motivo più pertinente e ampiamente applicabile sarebbe che l'inizializzazione di un oggetto si basa su dati asincroni, che attualmente non si possiedono.

Sai che hai bisogno dell'oggetto, quindi lo creerai comunque, ma per farlo funzionare correttamente, ha bisogno di dati dal server o da un altro file che ora deve caricare.

Ancora una volta, sia che tu stia trasferendo i dati necessari in un gigantesco init, o costruendo un'interfaccia non è davvero importante per il concetto, tanto quanto è importante per l'interfaccia del tuo oggetto e il design del tuo sistema ...

Ma in termini di costruzione dell'oggetto, potresti fare qualcosa del genere:

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

async_loader potrebbe essere passato un nome di file, o un nome di risorsa o altro, caricare quella risorsa - forse carica file audio o dati di immagine o forse carica le statistiche dei personaggi salvate ...

... e poi reinserirebbe quei dati obj_w_async_dependencies.init(result);.

Questo tipo di dinamica si trova frequentemente nelle app Web.
Non necessariamente nella costruzione di un oggetto, per applicazioni di livello superiore: ad esempio, le gallerie potrebbero caricarsi e inizializzarsi immediatamente e quindi visualizzare le foto durante lo streaming - non è in realtà un'inizializzazione asincrona, ma dove sarebbe visto più frequentemente sarebbe nelle librerie JavaScript.

Un modulo potrebbe dipendere da un altro, quindi l'inizializzazione di quel modulo potrebbe essere rinviata fino al completamento del caricamento delle persone a carico.

In termini di istanze specifiche del gioco, considera una Gameclasse reale .

Perché non possiamo chiamare .starto .runnel costruttore?
Le risorse devono essere caricate: il resto di tutto è stato praticamente definito e va bene, ma se proviamo a eseguire il gioco senza una connessione al database, o senza trame o modelli o suoni o livelli, non sarà un gioco particolarmente interessante ...

... quindi qual è, allora, la differenza tra ciò che vediamo di un tipico Game, tranne per il fatto che diamo al suo metodo "vai avanti" un nome che è più interessante di .init(o al contrario, spezziamo ulteriormente l'inizializzazione, per separare il caricamento, impostare le cose che sono state caricate ed eseguire il programma quando tutto è stato impostato).


2
" li chiameresti manualmente, in modo da sapere esattamente quando e dove nel programma stava accadendo. " L'unica volta in C ++ in cui un distruttore verrebbe chiamato implicitamente è per un oggetto stack (o globale). Gli oggetti allocati in heap richiedono la distruzione esplicita. Quindi è sempre chiaro quando l'oggetto viene deallocato.
Nicol Bolas,

6
Non è esattamente esatto affermare che è necessario questo metodo separato per consentire l'iniezione di diversi tipi di armi o che questo è l'unico modo per evitare la proliferazione di sottoclassi. Puoi passare le istanze dell'arma tramite il costruttore! Quindi è un -1 da parte mia in quanto questo non è un caso d'uso convincente.
Kylotan,

1
-1 Anche da me, per le stesse ragioni di Kylotan. Non fai un argomento molto convincente, tutto ciò avrebbe potuto essere fatto con i costruttori.
Paul Manta,

Sì, potrebbe essere realizzato con costruttori e distruttori. Ha chiesto casi d'uso di una tecnica e perché e come, piuttosto che come funzionano o perché lo fanno. Avere un sistema basato su componenti in cui si hanno metodi setter / binding, rispetto ai parametri passati dal costruttore per DI, dipende davvero da come si vuole costruire la propria interfaccia. Ma se il tuo oggetto richiede 20 componenti IOC, vuoi metterli TUTTI nel tuo costruttore? Puoi? Certo che puoi. Dovresti? Forse sì forse no. Se scegli di non farlo, allora hai bisogno di un .init, forse no, ma probabilmente. Ergo, caso valido.
Norguard,

1
@Kylotan Ho effettivamente modificato il titolo della domanda per chiedermi perché. L'OP ha solo chiesto "come". Ho esteso la domanda per includere il "perché" come "come" è banale per chiunque sappia qualcosa sulla programmazione ("Basta spostare la logica che avresti nel ctor in una funzione separata e chiamarla") e il "perché" è più interessante / generale.
Tetrad,

17

Qualunque cosa tu abbia letto, che Init e CleanUp siano migliori, avrebbe dovuto dirti anche perché. Non vale la pena leggere articoli che non giustificano le loro affermazioni.

Avere funzioni di inizializzazione e spegnimento separate può semplificare la configurazione e la distruzione dei sistemi perché è possibile scegliere in quale ordine chiamarli, mentre i costruttori vengono chiamati esattamente quando viene creato l'oggetto e i distruttori chiamati quando l'oggetto viene distrutto. Quando si hanno dipendenze complesse tra 2 oggetti, spesso è necessario che entrambi esistano prima che si configurino, ma spesso questo è un segno di cattiva progettazione altrove.

Alcune lingue non hanno distruttori su cui poter fare affidamento, poiché il conteggio dei riferimenti e la raccolta dei rifiuti rendono più difficile sapere quando l'oggetto verrà distrutto. In queste lingue è quasi sempre necessario un metodo di arresto / pulizia e ad alcuni piace aggiungere il metodo init per la simmetria.


Grazie, ma cerco principalmente esempi, dato che l'articolo non li aveva. Mi scuso se la mia domanda non è chiara al riguardo, ma l'ho modificata ora.
Friso,

3

Penso che il motivo migliore sia: consentire il pooling.
se hai Init e CleanUp, puoi, quando un oggetto viene ucciso, semplicemente chiamare CleanUp e spingere l'oggetto su una pila di oggetti dello stesso tipo: un 'pool'.
Quindi, ogni volta che è necessario un nuovo oggetto, è possibile estrarre un oggetto dal pool OPPURE se il pool è vuoto, peccato che sia necessario crearne uno nuovo. Quindi si chiama Init su questo oggetto.
Una buona strategia è quella di pre-riempire il pool prima che il gioco inizi con un "buon" numero di oggetti, quindi non devi mai creare alcun oggetto pool durante il gioco.
Se, d'altra parte, usi "nuovo" e smetti di fare riferimento a un oggetto quando non ti è utile, crei immondizia che deve essere raccolta in un determinato momento. Questo ricordo è particolarmente negativo per i linguaggi a thread singolo come Javascript, in cui il Garbage Collector interrompe tutto il codice quando lo valuta deve ricordare la memoria degli oggetti non più in uso. Il gioco si blocca durante alcuni millisecondi e l'esperienza di gioco è viziata.
- Hai già capito -: se unisci tutti i tuoi oggetti, non si verifica alcun ricordo, quindi non c'è più un rallentamento casuale.

È anche molto più veloce chiamare init su un oggetto proveniente dal pool piuttosto che allocare memoria + init un nuovo oggetto.
Ma il miglioramento della velocità ha meno importanza, dal momento che molto spesso la creazione di oggetti non è un collo di bottiglia delle prestazioni ... Con poche eccezioni, come giochi frenetici, motori di particelle o motore fisico che usano intensamente vettori 2D / 3d per i loro calcoli. Qui sia la velocità che la creazione di immondizia sono notevolmente migliorate utilizzando un pool.

Rq: potrebbe non essere necessario disporre di un metodo CleanUp per gli oggetti raggruppati se Init () reimposta tutto.

Modifica: la risposta a questo post mi ha motivato a finalizzare un piccolo articolo che ho realizzato sul pooling in Javascript .
Puoi trovarlo qui se sei interessato:
http://gamealchemist.wordpress.com/


1
-1: Non è necessario farlo solo per avere un pool di oggetti. Puoi farlo semplicemente separando l'allocazione dalla costruzione tramite posizionamento nuovo e deallocazione dalla cancellazione mediante una chiamata esplicita del distruttore. Quindi questo non è un motivo valido per separare costruttori / distruttori da un metodo di inizializzazione.
Nicol Bolas,

il posizionamento nuovo è specifico per C ++ e anche un po 'esoterico.
Kylotan,

+1 potrebbe essere possibile farlo in altro modo in c +. Ma non in altre lingue ... e questo è probabilmente solo il motivo per cui avrei usato il metodo Init su gameobjects.
Kikaimaru,

1
@Nicol Bolas: penso che tu stia reagendo in modo esagerato. Il fatto che ci siano altri modi per fare il pool (ne menzionate uno complesso, specifico per C ++) non invalida il fatto che l'uso di un Init separato sia un modo semplice e piacevole per implementare il pool in molte lingue. le mie preferenze vanno, su GameDev, a risposte più generiche.
GameAlchemist,

@VincentPiel: In che modo l'utilizzo del posizionamento è nuovo e così "complesso" in C ++? Inoltre, se stai lavorando in un linguaggio GC, le probabilità sono buone che gli oggetti contengano oggetti basati su GC. Quindi dovranno anche sondare ciascuno di loro? Pertanto, la creazione di un nuovo oggetto implica il recupero di un sacco di nuovi oggetti dai pool.
Nicol Bolas,

0

La tua domanda è invertita ... Storicamente parlando, la domanda più pertinente è:

Perché è compresa la costruzione + intializzazione , ovvero perché non facciamo questi passaggi separatamente? Sicuramente questo va contro il SoC ?

Per C ++, l'intenzione di RAII è che l'acquisizione e il rilascio delle risorse siano collegati direttamente alla durata degli oggetti, nella speranza che ciò assicuri il rilascio delle risorse. Vero? In parte. È soddisfatto al 100% nel contesto di variabili basate su stack / automatiche, dove lasciare l'ambito associato chiama automaticamente distruttori / rilascia queste variabili (da cui il qualificatore automatic). Tuttavia, per le variabili heap, questo modello molto utile si rompe tristemente, poiché sei ancora costretto a chiamare esplicitamente deleteper eseguire il distruttore, e se dimentichi di farlo sarai comunque morso da ciò che RAII tenta di risolvere; nel contesto delle variabili allocate in heap, quindi, C ++ offre un vantaggio limitato rispetto a C ( deletevsfree()) durante la fusione della costruzione con l'inizializzazione, che ha un impatto negativo in termini di quanto segue:

Costruire un sistema a oggetti per giochi / simulazioni in C è fortemente raccomandato in quanto farà molta luce sui limiti della RAII e di altri simili schemi OO attraverso una comprensione più profonda delle ipotesi che il C ++ e successivamente i linguaggi OO classici fanno (ricorda che C ++ è iniziato come un sistema OO incorporato in C).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.