¿Por qué debería usar métodos de inicialización y limpieza separados en lugar de poner lógica en el constructor y destructor para los componentes del motor?

9

Estoy trabajando en mi propio motor de juego, y actualmente estoy diseñando mis gerentes. He leído que para la administración de memoria, el uso Init()y las CleanUp()funciones son mejores que el uso de constructores y destructores.

He estado buscando ejemplos de código C ++, para ver cómo funcionan esas funciones y cómo puedo implementarlas en mi motor. ¿Cómo funciona Init()y CleanUp()trabajo, y cómo puedo implementarlas en mi motor?

Friso
fuente
Para C ++, consulte stackoverflow.com/questions/3786853/… Las razones principales para usar Init () son 1) Prevenir excepciones y bloqueos en el constructor con funciones auxiliares 2) Ser capaz de usar métodos virtuales de la clase derivada 3) Evitar dependencias circulares 4) como método privado para evitar la duplicación de código
brita_

Respuestas:

12

Es bastante simple, en realidad:

En lugar de tener un Constructor que hace tu configuración,

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

... haga que su constructor haga poco o nada, y escriba un método llamado .inito .initialize, que haría lo que su constructor haría normalmente.

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

Entonces, en lugar de simplemente decir:

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

Se puede ir:

Thing thing = new Thing();

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

El beneficio es que ahora puede usar la inyección de dependencia / inversión de control más fácilmente en sus sistemas.

En lugar de decir

public class Soldier {
    private Weapon weapon;

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

Se puede construir el soldado, le dan un método Equip, donde se entregue él un arma, y luego llamar a todo el resto de las funciones constructoras.

Así que ahora, en lugar de subclasificar enemigos donde un soldado tiene una pistola y otro tiene un rifle y otro tiene una escopeta, y esa es la única diferencia, solo puede decir:

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

El mismo trato con la destrucción. Si tiene necesidades especiales (eliminar oyentes de eventos, eliminar instancias de matrices / cualquier estructura con la que esté trabajando, etc.), debería llamarlas manualmente, para que sepa exactamente cuándo y dónde en el programa que estaba sucediendo.

EDITAR


Como Kryotan ha señalado, a continuación, esto responde al "Cómo" de la publicación original , pero en realidad no hace un buen trabajo de "Por qué".

Como probablemente pueda ver en la respuesta anterior, puede que no haya mucha diferencia entre:

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

y escribiendo

var myObj = new Object(1,2);

mientras que solo tiene una función constructora más grande.
Hay un argumento para los objetos que tienen 15 o 20 condiciones previas, lo que haría que un constructor sea muy, muy difícil de trabajar, y haría que las cosas fueran más fáciles de ver y recordar, sacando esas cosas a la interfaz , para que pueda ver cómo funciona la instanciación, un nivel más alto.

La configuración opcional de objetos es una extensión natural de esto; opcionalmente, establecer valores en la interfaz, antes de ejecutar el objeto.
JS tiene algunos atajos geniales para esta idea, que parecen fuera de lugar en lenguajes tipo C de tipo más fuerte.

Dicho esto, lo más probable es que, si está lidiando con una lista de argumentos tan larga en su constructor, que su objeto es demasiado grande y hace demasiado, como es. Una vez más, esto es algo de preferencia personal, y hay excepciones a lo largo y ancho, pero si está pasando 20 cosas en un objeto, es muy probable que encuentre una manera de hacer que ese objeto haga menos, haciendo objetos más pequeños. .

Una razón más pertinente, y que es ampliamente aplicable, sería que la inicialización de un objeto se basa en datos asincrónicos, que actualmente no tiene.

Sabe que necesita el objeto, por lo que lo va a crear de todos modos, pero para que funcione correctamente, necesita datos del servidor o de otro archivo que ahora necesita cargar.

Una vez más, ya sea que esté pasando los datos necesarios a un gigantesco init, o construyendo una interfaz, no es realmente importante para el concepto, tanto como es importante para la interfaz de su objeto y el diseño de su sistema ...

Pero en términos de construir el objeto, podrías hacer algo como esto:

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

async_loader podría pasar un nombre de archivo, o un nombre de recurso o lo que sea, cargar ese recurso; tal vez carga archivos de sonido o datos de imagen, o tal vez carga estadísticas de caracteres guardadas ...

... y luego volvería a alimentar esos datos obj_w_async_dependencies.init(result);.

Este tipo de dinámica se encuentra con frecuencia en las aplicaciones web.
No necesariamente en la construcción de un objeto, para aplicaciones de nivel superior: por ejemplo, las galerías pueden cargarse e inicializarse de inmediato, y luego mostrar fotos a medida que se transmiten, eso no es realmente una inicialización asincrónica, pero donde se ve con más frecuencia sería en bibliotecas de JavaScript.

Un módulo puede depender de otro, por lo que la inicialización de ese módulo puede diferirse hasta que se complete la carga de los dependientes.

En términos de instancias específicas de este juego, considere una Gameclase real .

¿Por qué no podemos llamar .starto .runen el constructor?
Deben cargarse recursos: el resto de todo se ha definido y está listo, pero si intentamos ejecutar el juego sin una conexión de base de datos, o sin texturas, modelos, sonidos o niveles, no será un juego particularmente interesante ...

... entonces, ¿cuál es la diferencia entre lo que vemos de un típico Game, excepto que le damos a su método "seguir adelante" un nombre que es más interesante que .init(o por el contrario, separa la inicialización aún más, para separar la carga, configurar las cosas que se han cargado y ejecutar el programa cuando todo se ha configurado).

Norguard
fuente
2
" entonces los llamaría manualmente, para saber exactamente cuándo y dónde en el programa que estaba sucediendo " . El único momento en C ++ donde se llamará implícitamente a un destructor es para un objeto de pila (o global). Los objetos asignados al montón requieren una destrucción explícita. Por lo tanto, siempre está claro cuándo se desasigna el objeto.
Nicol Bolas
66
No es exactamente exacto decir que necesita este método separado para permitir la inyección de diferentes tipos de armas, o que esta es la única forma de evitar la proliferación de subclases. ¡Puedes pasar las instancias de armas a través del constructor! Entonces es un -1 de mi parte, ya que este no es un caso de uso convincente.
Kylotan
1
-1 De mí también, por las mismas razones que Kylotan. No haces un argumento muy convincente, todo esto podría haberse hecho con constructores.
Paul Manta
Sí, se podría lograr con constructores y destructores. Pidió casos de uso de una técnica y por qué y cómo, en lugar de cómo funcionan o por qué lo hacen. Tener un sistema basado en componentes donde tiene métodos de establecimiento / enlace, en comparación con los parámetros pasados ​​por el constructor para DI realmente se reduce a cómo desea construir su interfaz. Pero si su objeto requiere 20 componentes IOC, ¿desea poner TODOS en su constructor? ¿Puedes? Por supuesto que puede. Deberías? Tal vez tal vez no. Si elige no hacerlo, entonces necesita un .init, tal vez no, pero probablemente. Ergo, caso válido.
Norguard
1
@Kylotan Realmente edité el título de la pregunta para preguntar por qué. El OP solo preguntó "cómo". Extendí la pregunta para incluir el "por qué" como el "cómo" es trivial para cualquiera que sepa algo sobre programación ("Simplemente mueva la lógica que tendría en el ctor a una función separada y llámelo") y el "por qué" Es más interesante / general.
Tetrad
17

Cualquier cosa que leas que diga Init y CleanUp es mejor, también debería haberte dicho por qué. No vale la pena leer los artículos que no justifican sus afirmaciones.

Tener funciones de inicialización y apagado separadas puede facilitar la configuración y destrucción de sistemas porque puede elegir en qué orden llamarlos, mientras que los constructores se llaman exactamente cuando se crea el objeto y los destructores cuando se destruye el objeto. Cuando tiene dependencias complejas entre 2 objetos, a menudo necesita que ambos existan antes de que se configuren, pero a menudo esto es un signo de un diseño deficiente en otros lugares.

Algunos idiomas no tienen destructores en los que pueda confiar, ya que el conteo de referencias y la recolección de basura hacen que sea más difícil saber cuándo se destruirá el objeto. En estos idiomas casi siempre necesita un método de apagado / limpieza, y a algunos les gusta agregar el método init para la simetría.

Kylotan
fuente
Gracias, pero principalmente estoy buscando ejemplos, ya que el artículo no los tenía. Pido disculpas si mi pregunta no estaba clara al respecto, pero la he editado ahora.
Friso
3

Creo que la mejor razón es: permitir la agrupación.
si tiene Init y CleanUp, puede, cuando se mata un objeto, simplemente llame a CleanUp y empuje el objeto a una pila de objetos del mismo tipo: un 'grupo'.
Luego, cada vez que necesite un nuevo objeto, puede hacer estallar un objeto del grupo O, si el grupo está vacío -demasiado malo-, debe crear uno nuevo. Entonces llamas a Init en este objeto.
Una buena estrategia es llenar previamente el grupo antes de que el juego comience con un "buen" número de objetos, para que nunca tenga que crear ningún objeto agrupado durante el juego.
Si, por otro lado, usa 'nuevo', y simplemente deja de hacer referencia a un objeto cuando no le sirve, crea basura que debe ser recogida en algún momento. Este recuerdo es especialmente malo para lenguajes de un solo hilo como Javascript, donde el recolector de basura detiene todo el código cuando evalúa que necesita recordar la memoria de los objetos que ya no están en uso. El juego se cuelga durante unos pocos milisegundos, y la experiencia de juego se echa a perder.
- Ya entendiste: si agrupas todos tus objetos, no se produce ningún recuerdo y, por lo tanto, no se ralentiza al azar.

También es mucho más rápido llamar a init en un objeto que viene del grupo que asignar memoria + init a un nuevo objeto.
Pero la mejora de la velocidad tiene menos importancia, ya que con frecuencia la creación de objetos no es un cuello de botella en el rendimiento ... Con algunas excepciones, como juegos frenéticos, motores de partículas o motores físicos que usan intensivamente vectores 2D / 3D para sus cálculos. Aquí, tanto la velocidad como la creación de basura se mejoran enormemente mediante el uso de un grupo.

Rq: es posible que no necesite tener un método de limpieza para sus objetos agrupados si Init () restablece todo.

Editar: responder a esta publicación me motivó a finalizar un pequeño artículo que hice sobre la agrupación en Javascript .
Puede encontrarlo aquí si está interesado:
http://gamealchemist.wordpress.com/

GameAlchemist
fuente
1
-1: No necesitas hacer esto solo para tener un grupo de objetos. Puede hacerlo separando la asignación de la construcción a través de la colocación nueva y la desasignación de la eliminación mediante una llamada explícita al destructor. Por lo tanto, esta no es una razón válida para separar constructores / destructores de algún método inicializador.
Nicol Bolas
la ubicación nueva es específica de C ++ y también un poco esotérica.
Kylotan
+1 puede ser posible hacer esto de otra manera en c +. Pero no en otros idiomas ... y esta es probablemente la única razón por la que usaría el método Init en los objetos de juego.
Kikaimaru
1
@Nicol Bolas: creo que estás exagerando. El hecho de que existan otras formas de agrupación (usted menciona una compleja, específica de C ++) no invalida el hecho de que usar un Init por separado es una forma agradable y sencilla de implementar la agrupación en muchos idiomas. mis preferencias van, en GameDev, a respuestas más genéricas.
GameAlchemist
@VincentPiel: ¿Cómo es el uso de la colocación nueva y tan "compleja" en C ++? Además, si está trabajando en un lenguaje GC, es muy probable que los objetos contengan objetos basados ​​en GC. Entonces, ¿también tendrán que sondear a cada uno de ellos? Por lo tanto, crear un nuevo objeto implicará obtener un montón de nuevos objetos de los grupos.
Nicol Bolas
0

Su pregunta se invierte ... Históricamente hablando, la pregunta más pertinente es:

¿Por qué se combinan construcción + inicialización , es decir, por qué no hacemos estos pasos por separado? ¿Seguramente esto va en contra de SoC ?

Para C ++, la intención de RAII es que la adquisición y liberación de recursos se vincule directamente con la vida útil del objeto, con la esperanza de que esto asegure la liberación de recursos. ¿Lo hace? Parcialmente. Se cumple al 100% en el contexto de variables automáticas o basadas en pila, donde dejar el ámbito asociado llama automáticamente a los destructores / libera estas variables (de ahí el calificador automatic). Sin embargo, para las variables de montón, este patrón muy útil se rompe tristemente, ya que aún se ve obligado a llamar explícitamente deletepara ejecutar el destructor, y si olvida hacerlo, lo morderá lo que RAII intenta resolver; en el contexto de las variables asignadas en el montón, entonces, C ++ proporciona un beneficio limitado sobre C ( deletevsfree()) mientras se combina la construcción con la inicialización, lo que impacta negativamente en términos de lo siguiente:

Se recomienda encarecidamente construir un sistema de objetos para juegos / simulaciones en C, ya que arrojará mucha luz sobre las limitaciones de RAII y otros patrones centrados en OO a través de una comprensión más profunda de los supuestos que C ++ y los lenguajes clásicos de OO posteriores (recuerde que C ++ comenzó como un sistema OO integrado en C).

Ingeniero
fuente