¿Por qué un bucle while (verdadero) en un constructor es realmente malo?

47

Aunque es una pregunta general, mi alcance es más bien C #, ya que soy consciente de que lenguajes como C ++ tienen una semántica diferente con respecto a la ejecución del constructor, la gestión de la memoria, el comportamiento indefinido, etc.

Alguien me hizo una pregunta interesante que para mí no fue respondida fácilmente.

¿Por qué (o se considera que es) un mal diseño permitir que un constructor de una clase inicie un ciclo sin fin (es decir, un ciclo de juego)?

Hay algunos conceptos que se rompen por esto:

  • Al igual que el principio de menor asombro, el usuario no espera que el constructor se comporte así.
  • Las pruebas unitarias son más difíciles ya que no puede crear esta clase o inyectarla, ya que nunca sale del bucle.
  • El final del ciclo (final del juego) es, conceptualmente, el momento en que termina el constructor, que también es extraño.
  • Técnicamente, tal clase no tiene miembros públicos, excepto el constructor, lo que hace que sea más difícil de entender (especialmente para los lenguajes donde no hay implementación disponible)

Y luego están los problemas técnicos:

  • El constructor en realidad nunca termina, entonces, ¿qué pasa con GC aquí? ¿Este objeto ya está en Gen 0?
  • Derivar de tal clase es imposible o al menos muy complicado debido al hecho de que el constructor base nunca regresa

¿Hay algo más obviamente malo o tortuoso con este enfoque?

Samuel
fuente
61
¿Por qué es bueno? Si simplemente mueve su bucle principal a un método (refactorización muy simple), el usuario puede escribir un código sorprendente como este:var g = new Game {...}; g.MainLoop();
Brandin
61
Su pregunta ya establece 6 razones para no usarla, y me gustaría ver una sola a su favor. También puede preguntar por qué es una mala idea poner un while(true)bucle en un montador de propiedad: new Game().RunNow = true?
Groo
38
Analogía extrema: ¿por qué decimos que lo que Hitler hizo estuvo mal? Excepto por la discriminación racial, el comienzo de la Segunda Guerra Mundial, el asesinato de millones de personas, la violencia [etc., etc. por más de 50 razones] no hizo nada malo. Claro, si elimina una lista arbitraria de la razón de por qué algo está mal, también puede concluir que esa cosa es buena, literalmente para cualquier cosa.
Bakuriu
44
Con respecto a la recolección de basura (GC) (su problema técnico), no tendría nada de especial. La instancia existe antes de que se ejecute realmente el código del constructor. En este caso, la instancia todavía es accesible, por lo que GC no puede reclamarla. Si se establece GC, esta instancia sobrevivirá, y cada vez que se establezca GC, el objeto será promovido a la próxima generación de acuerdo con la regla habitual. Si se lleva a cabo o no GC, depende de si se están creando (suficientes) nuevos objetos o no.
Jeppe Stig Nielsen
8
Yo iría un paso más allá y diría que while (verdadero) es malo, independientemente de dónde se use. Implica que no hay una forma clara y limpia de detener el ciclo. En su ejemplo, ¿no debería detenerse el ciclo cuando se sale del juego o pierde el foco?
Jon Anderson

Respuestas:

250

¿Cuál es el propósito de un constructor? Devuelve un objeto recién construido. ¿Qué hace un bucle infinito? Nunca vuelve. ¿Cómo puede el constructor devolver un objeto recién construido si no devuelve nada? No puede

Ergo, un bucle infinito rompe el contrato fundamental de un constructor: construir algo.

Jörg W Mittag
fuente
11
¿Quizás quisieron poner un tiempo (verdadero) con un descanso en el medio? Arriesgado, pero legítimo.
John Dvorak
2
Estoy de acuerdo, esto es correcto, al menos. desde un punto de vista académico. Lo mismo vale para cada función que devuelve algo.
Doc Brown
61
Imagina al pobre diablo que crea una subclase de dicha clase ...
Newtopian
55
@ JanDvorak: Bueno, esa es una pregunta diferente. El OP especificó explícitamente "interminable" y mencionó un bucle de eventos.
Jörg W Mittag
44
@ JörgWMittag bueno, entonces, sí, no pongas eso en un constructor.
John Dvorak
45

¿Hay algo más obviamente malo o tortuoso con este enfoque?

Sí, por supuesto. Es innecesario, inesperado, inútil, poco elegante. Viola los conceptos modernos de diseño de clase (cohesión, acoplamiento). Rompe el contrato del método (un constructor tiene un trabajo definido y no es solo un método aleatorio). Ciertamente no es fácil de mantener, los futuros programadores pasarán mucho tiempo tratando de comprender lo que está sucediendo y tratando de adivinar las razones por las que se hizo de esa manera.

Nada de esto es un "error" en el sentido de que su código no funciona. Pero probablemente incurrirá en enormes costos secundarios (en relación con el costo de escribir el código inicialmente) a largo plazo al hacer que el código sea más difícil de mantener (es decir, pruebas difíciles de agregar, difíciles de reutilizar, difíciles de depurar, difíciles de extender, etc. .).

Muchas de las mejoras más modernas en los métodos de desarrollo de software se realizan específicamente para facilitar el proceso real de escritura / prueba / depuración / mantenimiento del software. Todo esto es burlado por cosas como esta, donde el código se coloca al azar porque "funciona".

Desafortunadamente, regularmente se encontrará con programadores que ignoran por completo todo esto. Funciona, eso es todo.

Para finalizar con una analogía (otro lenguaje de programación, aquí el problema en cuestión es calcular 2+2):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

¿Qué hay de malo con el primer enfoque? Devuelve 4 después de un período de tiempo razonablemente corto; es correcto. Las objeciones (junto con todas las razones habituales que un desarrollador puede dar para refutarlas) son:

  • Es más difícil de leer (pero bueno, un buen programador puede leerlo tan fácilmente, y siempre lo hemos hecho así; si lo desea, ¡puedo refactorizarlo en un método!)
  • Más lento (pero esto no está en un lugar crítico de tiempo, por lo que podemos ignorar eso, y además, ¡es mejor optimizar solo cuando sea necesario!)
  • Utiliza muchos más recursos (un nuevo proceso, más RAM, etc.), pero nuestro servidor es más que rápido
  • Introduce dependencias en bash(pero de todos modos nunca se ejecutará en Windows, Mac o Android)
  • Y todas las otras razones mencionadas anteriormente.
AnoE
fuente
55
"GC podría estar en un estado de bloqueo excepcional mientras se ejecuta un constructor", ¿por qué? El asignador asigna primero, luego lanza el constructor como un método ordinario. No hay nada realmente especial al respecto, ¿verdad? Y el asignador tiene que permitir nuevas asignaciones (y esto podría desencadenar situaciones OOM) dentro del constructor de todos modos.
John Dvorak
1
@ JanDvorak, solo quería señalar que podría haber algún comportamiento especial "en algún lugar" mientras estaba en un constructor, pero tienes razón, el ejemplo que elegí no era realista. Remoto.
AnoE
Realmente me gusta su explicación y me encantaría marcar ambas respuestas como respuestas (tener al menos un voto a favor). La analogía es agradable y su rastro de pensamiento y explicación de los costos secundarios también lo es, pero los considero como requisitos auxiliares para el código (igual que mis argumentos). El estado actual de la técnica es probar el código, por lo tanto, la capacidad de prueba, etc. es un requisito de facto. Pero la declaración de @ JörgWMittag es acertada fundamental contract of a constructor: to construct something.
Samuel
La analogía no es tan buena. Si veo esto, esperaría ver un comentario en algún lugar sobre por qué está usando Bash para hacer los cálculos, y dependiendo de su razón, puede estar totalmente bien. Lo mismo no funcionaría para el OP, porque básicamente tendría que poner disculpas en los comentarios en todos los lugares donde utilizó el constructor "// Nota: la construcción de este objeto inicia un ciclo de eventos que nunca regresa".
Brandin
44
"Desafortunadamente, te encontrarás con programadores que ignoran completamente todo esto. Funciona, eso es todo". Tan triste pero tan cierto. Creo que puedo hablar por todos nosotros cuando digo que la única carga sustancial en nuestra vida profesional es, bueno, esas personas.
Lightness compite con Monica el
8

Usted da suficientes razones en su pregunta para descartar este enfoque, pero la pregunta real aquí es "¿Hay algo más obviamente malo o tortuoso con ese enfoque?"

Mi primer pensamiento aquí fue que esto no tiene sentido. Si su constructor nunca termina, ninguna otra parte del programa puede obtener una referencia al objeto construido, entonces, ¿cuál es la lógica detrás de colocarlo en un constructor en lugar de un método regular? Entonces se me ocurrió que la única diferencia sería que en su bucle podría permitir referencias a un thisescape parcialmente construido del bucle. Si bien esto no se limita estrictamente a esta situación, se garantiza que si permite thisescapar, esas referencias siempre apuntarán a un objeto que no está completamente construido.

No sé si la semántica en torno a este tipo de situación está bien definida en C #, pero diría que no importa porque no es algo en lo que la mayoría de los desarrolladores quieran profundizar.

JimmyJames
fuente
Supongo que el constructor se ejecuta después de que se creó el objeto, por lo tanto, en una filtración de lenguaje sensata, este debería ser un comportamiento perfectamente bien definido.
mroman
@mroman: podría estar bien filtrarse thisen el constructor, pero solo si está en el constructor de la clase más derivada. Si tiene dos clases, Ay Bcon B : A, si el constructor de Ase filtrara this, los miembros de Btodavía no se inicializarían (y las llamadas o conversiones de métodos virtuales Bpueden causar estragos en función de eso).
hoffmale
1
Supongo que en C ++ eso puede ser una locura, sí. En otros idiomas, los miembros reciben un valor predeterminado antes de que se les asignen sus valores reales, por lo que si bien es estúpido escribir ese código, el comportamiento aún está bien definido. Si llama a métodos que usan estos miembros, es posible que encuentre NullPointerExceptions o cálculos incorrectos (porque los números predeterminan a 0) o cosas por el estilo, pero es técnicamente determinista lo que sucede.
mroman
@mroman ¿Puedes encontrar una referencia definitiva para el CLR que muestre que ese es el caso?
JimmyJames
Bueno, puede asignar valores de miembros antes de que se ejecute el constructor para que el objeto exista en un estado válido con los miembros asignados valores predeterminados o valores asignados a ellos incluso antes de que se ejecute el constructor. No necesita el constructor para inicializar miembros. Déjame ver si puedo encontrar eso en algún lugar de los documentos.
mroman
1

+1 Para el menor asombro, pero este es un concepto difícil de articular para los nuevos desarrolladores. A nivel pragmático, es difícil depurar las excepciones generadas dentro de los constructores, si el objeto no se inicializa, no existirá para que pueda inspeccionar el estado o iniciar sesión desde fuera de ese constructor.

Si siente la necesidad de hacer este tipo de patrón de código, utilice métodos estáticos en la clase.

Existe un constructor para proporcionar la lógica de inicialización cuando un objeto se instancia desde una definición de clase. Construye esta instancia de objeto porque es un contenedor que encapsula un conjunto de propiedades y funcionalidades que desea llamar desde el resto de la lógica de su aplicación.

Si no tiene la intención de utilizar el objeto que está "construyendo", ¿cuál es el punto de instanciar el objeto en primer lugar? Tener un ciclo while (verdadero) en un constructor efectivamente significa que nunca tiene la intención de que se complete ...

C # es un lenguaje orientado a objetos muy rico con muchas construcciones y paradigmas diferentes para que explore, conozca sus herramientas y cuándo usarlas.

En C # no ejecute lógica extendida o interminable dentro de los constructores porque ... hay mejores alternativas

Chris Schaller
fuente
1
el suyo no parece ofrecer nada sustancial sobre los puntos hechos y explicados en 9 respuestas anteriores
mosquito
1
Oye, tuve que intentarlo :) Pensé que era importante resaltar que si bien no hay una regla en contra de hacer esto, no deberías por el hecho de que hay formas más simples. La mayoría de las otras respuestas se centran en el tecnicismo de por qué es malo, pero eso es difícil de vender a un nuevo desarrollador que dice "pero se compila, así que puedo dejarlo así"
Chris Schaller
0

No hay nada inherentemente malo al respecto. Sin embargo, lo que será importante es la pregunta de por qué elegiste usar esta construcción. ¿Qué obtuviste haciendo esto?

En primer lugar, no voy a hablar sobre el uso while(true)en un constructor. No hay absolutamente nada de malo en usar esa sintaxis en un constructor. Sin embargo, el concepto de "iniciar un ciclo de juego en el constructor" puede causar algunos problemas.

Es muy común en estas situaciones que el constructor simplemente construya el objeto y tenga una función que llame al bucle infinito. Esto le da al usuario de su código más flexibilidad. Como ejemplo de los tipos de problemas que pueden aparecer es que restringe el uso de su objeto. Si alguien quiere tener un objeto miembro que sea "el objeto del juego" en su clase, debe estar listo para ejecutar todo el ciclo del juego en su constructor porque tiene que construir el objeto. Esto podría llevar a una codificación torturada para evitar esto. En el caso general, no puede preasignar su objeto de juego y luego ejecutarlo más tarde. Para algunos programas eso no es un problema, pero hay clases de programas en los que desea poder asignar todo por adelantado y luego llamar al bucle del juego.

Una de las reglas generales en la programación es usar la herramienta más simple para el trabajo. No es una regla difícil y rápida, pero es muy útil. Si una llamada a la función hubiera sido suficiente, ¿por qué molestarse en construir un objeto de clase? Si veo que se usa una herramienta complicada más poderosa, supongo que el desarrollador tenía una razón para ello, y comenzaré a explorar qué tipo de trucos extraños podrías estar haciendo.

Hay casos en que esto podría ser razonable. Puede haber casos en los que sea realmente intuitivo tener un bucle de juego en el constructor. En esos casos, ¡corres con eso! Por ejemplo, su ciclo de juego puede estar integrado en un programa mucho más grande que construye clases para incorporar algunos datos. Si tiene que ejecutar un bucle de juego para generar esos datos, puede ser muy razonable ejecutar el bucle de juego en un constructor. Simplemente trate esto como un caso especial: sería válido hacerlo porque el programa general hace que sea intuitivo que el próximo desarrollador entienda lo que hizo y por qué.

Cort Ammon
fuente
Si construir tu objeto requiere un ciclo de juego, al menos ponlo en un método de fábrica. GameTestResults testResults = runGameTests(tests, configuration);en lugar de GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751
@immibis Sí, eso es cierto, excepto en los casos en que eso no es razonable. Si está trabajando con un sistema basado en la reflexión que crea una instancia de su objeto por usted, es posible que no tenga la opción de hacer un método de fábrica.
Cort Ammon
0

Mi impresión es que algunos conceptos se mezclaron y dieron lugar a una pregunta no tan bien redactada.

El constructor de una clase puede iniciar un nuevo hilo que "nunca" terminará (aunque prefiero usar while(_KeepRunning)over while(true)porque puedo configurar el miembro booleano como falso en alguna parte).

Podría extraer ese método de crear e iniciar el subproceso en una función propia, para separar la construcción de objetos y el inicio real de su trabajo; prefiero esta forma porque obtengo un mejor control sobre el acceso a los recursos.

También puede echar un vistazo al "Patrón de objeto activo". Al final, supongo que la pregunta estaba dirigida a cuándo comenzar el hilo de un "Objeto Activo", y mi preferencia es un desacoplamiento de la construcción y comenzar para un mejor control.

Bernhard Hiller
fuente
0

Su objeto me recuerda a iniciar Tareas . El objeto de tarea tiene un constructor, pero también hay un montón de métodos de fábrica estáticos como: Task.Run, Task.Starty Task.Factory.StartNew.

Estos son muy similares a lo que está tratando de hacer, y es probable que esta sea la 'convención'. El problema principal que la gente parece tener es que este uso de un constructor los sorprende. @ JörgWMittag dice que rompe un contrato fundamental , lo que supongo que significa que Jörg está muy sorprendido. Estoy de acuerdo y no tengo nada más que agregar a eso.

Sin embargo, quiero sugerir que el OP intente con métodos de fábrica estáticos. La sorpresa se desvanece y no hay un contrato fundamental en juego aquí. Las personas están acostumbradas a los métodos estáticos de fábrica que hacen cosas especiales, y se les puede nombrar en consecuencia.

Puede proporcionar constructores que permitan un control detallado (como sugiere @Brandin, algo así var g = new Game {...}; g.MainLoop();, que se adapta a los usuarios que no desean comenzar el juego de inmediato, y tal vez quieran pasarlo primero. Y puede escribir algo como var runningGame = Game.StartNew();comenzar Es inmediatamente fácil.

Nathan Cooper
fuente
Significa que Jörg está fundamentalmente sorprendido, lo que quiere decir que una sorpresa como esa es un dolor en el fondo.
Steve Jessop
0

¿Por qué un bucle while (verdadero) en un constructor es realmente malo?

Eso no está nada mal.

¿Por qué (o se considera que es) un mal diseño permitir que un constructor de una clase inicie un ciclo sin fin (es decir, un ciclo de juego)?

¿Por qué querrías hacer esto? Seriamente. Esa clase tiene una sola función: el constructor. Si todo lo que quieres es una sola función, es por eso que tenemos funciones, no constructores.

Usted mencionó algunas de las razones obvias en su contra, pero omitió la más importante:

Hay opciones mejores y más simples para lograr lo mismo.

Si hay 2 opciones y una es mejor, no importa cuánto sea mejor, usted elige la mejor. Por cierto, es por eso que casi nunca usamos GoTo.

Peter
fuente
-1

El propósito de un constructor es, bueno, construir su objeto. Antes de que el constructor haya completado, en principio es inseguro usar cualquier método de ese objeto. Por supuesto, podemos llamar métodos del objeto dentro del constructor, pero luego cada vez que lo hagamos, tenemos que asegurarnos de que hacerlo sea válido para el objeto en su estado actual de medio cocción.

Al poner indirectamente toda la lógica en el constructor, agrega más carga de este tipo sobre usted. Esto sugiere que no diseñó la diferencia entre inicialización y uso lo suficientemente bien.

Hagen von Eitzen
fuente
1
Esto no parece ofrecer nada sustancial sobre los puntos hechos y explicados en 8 respuestas anteriores
mosquito