Pourquoi devrais-je utiliser des méthodes d'initialisation et de nettoyage distinctes au lieu de mettre de la logique dans le constructeur et le destructeur pour les composants du moteur?

9

Je travaille sur mon propre moteur de jeu et je conçois actuellement mes managers. J'ai lu que pour la gestion de la mémoire, l'utilisation des fonctions Init()et CleanUp()est meilleure que celle des constructeurs et des destructeurs.

Je cherchais des exemples de code C ++, pour voir comment ces fonctions fonctionnent et comment je peux les implémenter dans mon moteur. Comment fonctionne Init()et CleanUp()fonctionne, et comment puis-je les implémenter dans mon moteur?

Friso
la source
Pour C ++, voir stackoverflow.com/questions/3786853/… Les principales raisons d'utiliser Init () sont 1) Empêcher les exceptions et les plantages dans le constructeur avec des fonctions d'assistance 2) Pouvoir utiliser des méthodes virtuelles de la classe dérivée 3) Contourner les dépendances circulaires 4) comme méthode privée pour éviter la duplication de code
brita_

Réponses:

12

C'est assez simple, en fait:

Au lieu d'avoir un constructeur qui fait votre configuration,

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

... demandez à votre constructeur de faire peu ou rien du tout, et écrivez une méthode appelée .initou .initialize, qui ferait ce que votre constructeur ferait normalement.

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

Alors maintenant, au lieu de simplement faire comme:

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

Tu peux y aller:

Thing thing = new Thing();

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

L'avantage est que vous pouvez désormais utiliser plus facilement l'injection de dépendance / l'inversion de contrôle dans vos systèmes.

Au lieu de dire

public class Soldier {
    private Weapon weapon;

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

Vous pouvez construire le soldat, lui donner une méthode équipez, où vous remettez lui une arme, et appeler alors tous le reste des fonctions constructeur.

Alors maintenant, au lieu de sous-classer les ennemis où un soldat a un pistolet et un autre a un fusil et un autre a un fusil de chasse, et c'est la seule différence, vous pouvez simplement 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);

Même chose pour la destruction. Si vous avez des besoins particuliers (suppression d'écouteurs d'événements, suppression d'instances de tableaux / quelles que soient les structures avec lesquelles vous travaillez, etc.), vous les appellerez manuellement, afin de savoir exactement quand et où dans le programme qui se passait.

ÉDITER


Comme Kryotan l'a souligné, ci-dessous, cela répond au "Comment" du post original , mais ne fait pas vraiment un bon travail de "Pourquoi".

Comme vous pouvez probablement le voir dans la réponse ci-dessus, il pourrait ne pas y avoir beaucoup de différence entre:

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

et l'écriture

var myObj = new Object(1,2);

tout en ayant une fonction constructeur plus grande.
Il y a un argument à faire pour les objets qui ont 15 ou 20 conditions préalables, ce qui rendrait un constructeur très, très difficile à travailler, et cela rendrait les choses plus faciles à voir et à retenir, en tirant ces choses dans l'interface , afin que vous puissiez voir comment l'instanciation fonctionne, un niveau plus haut.

La configuration optionnelle des objets en est une extension naturelle; définir éventuellement des valeurs sur l'interface, avant d'exécuter l'objet.
JS a de très bons raccourcis pour cette idée, qui semblent tout simplement hors de propos dans les langages de type c plus forts.

Cela dit, il y a des chances, si vous avez affaire à une liste d'arguments aussi longue dans votre constructeur, que votre objet est trop grand et fait trop, tel quel. Encore une fois, c'est une chose de préférence personnelle, et il y a des exceptions partout, mais si vous passez 20 choses dans un objet, il y a de bonnes chances que vous puissiez trouver un moyen de faire en sorte que cet objet fasse moins, en faisant des objets plus petits .

Une raison plus pertinente, et largement applicable, serait que l'initialisation d'un objet repose sur des données asynchrones, que vous n'avez pas actuellement.

Vous savez que vous avez besoin de l'objet, vous allez donc le créer de toute façon, mais pour qu'il fonctionne correctement, il a besoin des données du serveur ou d'un autre fichier qu'il doit maintenant charger.

Encore une fois, que vous passiez les données nécessaires dans un gigantesque init ou que vous construisiez une interface n'est pas vraiment important pour le concept, autant qu'il l'est pour l'interface de votre objet et la conception de votre système ...

Mais en termes de construction de l'objet, vous pourriez faire quelque chose comme ceci:

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

async_loader peut obtenir un nom de fichier, un nom de ressource ou autre, charger cette ressource - peut-être qu'il charge des fichiers audio ou des données d'image, ou peut-être qu'il charge des statistiques de caractères enregistrées ...

... et ensuite il alimenterait ces données obj_w_async_dependencies.init(result);.

Ce type de dynamique se retrouve fréquemment dans les applications Web.
Pas nécessairement dans la construction d'un objet, pour les applications de niveau supérieur: par exemple, les galeries peuvent se charger et s'initialiser tout de suite, puis afficher les photos au fur et à mesure - ce n'est pas vraiment une initialisation asynchrone, mais là où elle est vue plus fréquemment, ce serait dans les bibliothèques JavaScript.

Un module peut dépendre d'un autre, et donc l'initialisation de ce module peut être différée jusqu'à ce que le chargement des dépendants soit terminé.

En termes d'instances spécifiques au jeu, considérez une Gameclasse réelle .

Pourquoi ne pouvons-nous pas appeler .startou .rundans le constructeur?
Les ressources doivent être chargées - le reste de tout a été à peu près défini et il est bon d'y aller, mais si nous essayons d'exécuter le jeu sans connexion à la base de données, ou sans textures ou modèles ou sons ou niveaux, cela ne sera pas un jeu particulièrement intéressant ...

... alors quelle est la différence entre ce que nous voyons d'un typique Game, sauf que nous donnons à sa méthode "go ahead" un nom qui est plus intéressant que .init(ou inversement, casser l'initialisation encore plus loin, pour séparer le chargement, mise en place des choses qui ont été chargées, et exécution du programme lorsque tout a été mis en place).

Norguard
la source
2
" vous les appelleriez alors manuellement, de sorte que vous sachiez exactement quand et où dans le programme qui se produisait. " Le seul moment en C ++ où un destructeur serait implicitement appelé est pour un objet de pile (ou global). Les objets alloués en tas nécessitent une destruction explicite. Il est donc toujours clair lorsque l'objet est désalloué.
Nicol Bolas
6
Il n'est pas tout à fait exact de dire que vous avez besoin de cette méthode distincte pour permettre l'injection de différents types d'armes, ou que c'est le seul moyen d'éviter la prolifération des sous-classes. Vous pouvez passer les instances d'armes via le constructeur! C'est donc un -1 de ma part car ce n'est pas un cas d'utilisation convaincant.
Kylotan
1
-1 De moi aussi, pour à peu près les mêmes raisons que Kylotan. Vous ne faites pas un argument très convaincant, tout cela aurait pu être fait avec des constructeurs.
Paul Manta
Oui, cela pourrait être accompli avec des constructeurs et des destructeurs. Il a demandé des cas d'utilisation d'une technique et pourquoi et comment, plutôt que comment ils fonctionnent ou pourquoi ils le font. Avoir un système basé sur des composants où vous avez des méthodes de définition / liaison, par rapport aux paramètres passés par le constructeur pour DI, tout se résume vraiment à la façon dont vous voulez construire votre interface. Mais si votre objet nécessite 20 composants IOC, voulez-vous les mettre TOUS dans votre constructeur? Peut tu? Bien sûr vous pouvez. Devrais-tu? Peut-être peut-être pas. Si vous choisissez de ne pas le faire, en avez-vous besoin .init, peut-être pas, mais probablement. Ergo, cas valide.
Norguard
1
@Kylotan J'ai en fait édité le titre de la question pour demander pourquoi. L'OP a seulement demandé "comment". J'ai étendu la question pour inclure le "pourquoi" car le "comment" est trivial pour quiconque sait quoi que ce soit sur la programmation ("Il suffit de déplacer la logique que vous auriez dans le ctor dans une fonction distincte et de l'appeler") et le "pourquoi" est plus intéressant / général.
Tetrad
17

Tout ce que vous avez lu qui dit qu'Init et CleanUp est meilleur, aurait également dû vous dire pourquoi. Les articles qui ne justifient pas leurs affirmations ne valent pas la peine d'être lus.

Le fait d'avoir des fonctions d'initialisation et d'arrêt séparées peut faciliter la configuration et la destruction des systèmes, car vous pouvez choisir l'ordre dans lequel les appeler, tandis que les constructeurs sont appelés exactement lorsque l'objet est créé et les destructeurs appelés lorsque l'objet est détruit. Lorsque vous avez des dépendances complexes entre 2 objets, vous avez souvent besoin que les deux existent avant de s'installer - mais souvent c'est un signe de mauvaise conception ailleurs.

Certains langages n'ont pas de destructeurs sur lesquels vous pouvez compter, car le comptage des références et la collecte des ordures rendent plus difficile de savoir quand l'objet sera détruit. Dans ces langues, vous avez presque toujours besoin d'une méthode d'arrêt / nettoyage, et certains aiment ajouter la méthode init pour la symétrie.

Kylotan
la source
Merci, mais je recherche principalement des exemples, car l'article n'en avait pas. Je m'excuse si ma question n'était pas claire à ce sujet, mais je l'ai éditée maintenant.
Friso
3

Je pense que la meilleure raison est: de permettre la mise en commun.
si vous avez Init et CleanUp, vous pouvez, quand un objet est tué, appeler simplement CleanUp, et pousser l'objet sur une pile d'objets du même type: un 'pool'.
Ensuite, chaque fois que vous avez besoin d'un nouvel objet, vous pouvez extraire un objet du pool OU si le pool est vide - trop mauvais - vous devez en créer un nouveau. Ensuite, vous appelez Init sur cet objet.
Une bonne stratégie consiste à pré-remplir le pool avant le début du jeu avec un «bon» nombre d'objets, de sorte que vous n'avez jamais à créer d'objet groupé pendant le jeu.
Si, en revanche, vous utilisez «nouveau» et arrêtez simplement de référencer un objet lorsqu'il ne vous est d'aucune utilité, vous créez des ordures qui doivent être récupérées à un moment donné. Ce souvenir est particulièrement une mauvaise chose pour les langages à un seul thread comme Javascript, où le garbage collector arrête tout le code lorsqu'il évalue qu'il doit se souvenir de la mémoire des objets qui ne sont plus utilisés. Le jeu se bloque pendant quelques millisecondes et l'expérience de jeu est gâchée.
- Vous avez déjà compris -: si vous regroupez tous vos objets, aucun souvenir ne se produit, donc plus de ralentissement aléatoire.

Il est également beaucoup plus rapide d'appeler init sur un objet provenant du pool que d'allouer de la mémoire + init à un nouvel objet.
Mais l'amélioration de la vitesse a moins d'importance, car bien souvent la création d'objets n'est pas un goulot d'étranglement des performances ... À quelques exceptions près, comme les jeux frénétiques, les moteurs à particules ou le moteur physique utilisant intensivement des vecteurs 2D / 3D pour leurs calculs. Ici, la vitesse et la création de déchets sont grandement améliorées en utilisant un pool.

Rq: vous n'aurez peut-être pas besoin d'avoir une méthode CleanUp pour vos objets regroupés si Init () réinitialise tout.

Edit: répondre à ce post m'a motivé à finaliser un petit article que j'ai fait sur la mutualisation en Javascript .
Vous pouvez le trouver ici si vous êtes intéressé:
http://gamealchemist.wordpress.com/

GameAlchemist
la source
1
-1: Vous n'avez pas besoin de faire cela juste pour avoir un pool d'objets. Vous pouvez le faire en séparant simplement l'allocation de la construction via le placement nouveau et la désallocation de la suppression par un appel destructeur explicite. Ce n'est donc pas une raison valable pour séparer les constructeurs / destructeurs d'une méthode d'initialisation.
Nicol Bolas
placement new est spécifique à C ++ et un peu ésotérique également.
Kylotan
+1 il peut être possible de le faire d'une autre manière en c +. Mais pas dans d'autres langues ... et c'est probablement la seule raison pour laquelle j'utiliserais la méthode Init sur les objets de jeu.
Kikaimaru
1
@Nicol Bolas: je pense que vous réagissez de manière excessive. Le fait qu'il existe d'autres façons de faire du pooling (vous en mentionnez un complexe, spécifique à C ++) n'invalide pas le fait que l'utilisation d'un Init séparé est un moyen agréable et simple d'implémenter le pooling dans de nombreux langages. mes préférences vont, sur GameDev, à des réponses plus génériques.
GameAlchemist
@VincentPiel: Comment l'utilisation du placement est-elle nouvelle et si "complexe" en C ++? De plus, si vous travaillez dans un langage GC, il y a de fortes chances que les objets contiennent des objets basés sur GC. Alors, devront-ils également sonder chacun d'eux? Ainsi, la création d'un nouvel objet impliquera d'obtenir un tas de nouveaux objets à partir des pools.
Nicol Bolas
0

Votre question est inversée ... Historiquement parlant, la question la plus pertinente est:

Pourquoi est la construction + intialisation confondait , à savoir pourquoi ne pas nous ces étapes séparément? Cela va sûrement à l'encontre de SoC ?

Pour C ++, l'intention de RAII est que l'acquisition et la libération des ressources soient directement liées à la durée de vie de l'objet, dans l'espoir que cela garantira la libération des ressources. Le fait-il? Partiellement. Il est rempli à 100% dans le contexte des variables basées sur la pile / automatiques, où le fait de laisser la portée associée appelle automatiquement des destructeurs / libère ces variables (d'où le qualificatif automatic). Cependant, pour les variables de tas, ce modèle très utile tombe malheureusement en panne, car vous êtes toujours obligé d'appeler explicitement deletepour exécuter le destructeur, et si vous oubliez de le faire, vous serez toujours mordu par ce que RAII tente de résoudre; dans le contexte des variables allouées en tas, alors, C ++ offre un avantage limité sur C ( deletevsfree()) tout en confondant construction et initialisation, ce qui a un impact négatif sur les points suivants:

La construction d'un système d'objets pour les jeux / simulations en C est fortement recommandée car elle apportera beaucoup de lumière sur les limites de RAII et d'autres modèles centrés sur OO grâce à une compréhension plus approfondie des hypothèses que font C ++ et plus tard les langages OO classiques. (rappelez-vous que C ++ a commencé comme un système OO construit en C).

Ingénieur
la source