while (verdadero) y ruptura de bucle: ¿antipatrón?

32

Considere el siguiente código:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Suponga que este proceso implica un número finito pero dependiente de entrada de pasos; el bucle está diseñado para terminar por sí solo como resultado del algoritmo, y no está diseñado para ejecutarse indefinidamente (hasta que sea cancelado por un evento externo). Debido a que la prueba para ver si el ciclo debe finalizar se encuentra en el medio de un conjunto lógico de pasos, el ciclo while en sí mismo actualmente no verifica nada significativo; la verificación se realiza en el lugar "apropiado" dentro del algoritmo conceptual.

Me dijeron que este es un código incorrecto, porque es más propenso a errores debido a que la estructura de bucle no verifica la condición de finalización. Es más difícil descubrir cómo saldría del bucle, y podría invitar a los errores, ya que la condición de ruptura podría omitirse u omitirse accidentalmente en futuros cambios.

Ahora, el código podría estructurarse de la siguiente manera:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

Sin embargo, esto duplica una llamada a un método en código, violando DRY; si TransformInSomeWayluego se reemplazara con algún otro método, ambas llamadas tendrían que ser encontradas y cambiadas (y el hecho de que haya dos puede ser menos obvio en un código más complejo).

También puedes escribirlo como:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... pero ahora tiene una variable cuyo único propósito es cambiar la verificación de condición a la estructura de bucle, y también debe verificarse varias veces para proporcionar el mismo comportamiento que la lógica original.

Por mi parte, digo que dado el algoritmo que este código implementa en el mundo real, el código original es el más legible. Si lo estuviera haciendo usted mismo, esta es la forma en que lo pensaría, por lo que sería intuitivo para las personas familiarizadas con el algoritmo.

Entonces, ¿cuál es "mejor"? ¿Es mejor dar la responsabilidad de la verificación de condiciones al ciclo while estructurando la lógica alrededor del ciclo? ¿O es mejor estructurar la lógica de una manera "natural" como lo indican los requisitos o una descripción conceptual del algoritmo, aunque eso puede significar pasar por alto las capacidades integradas del bucle?

KeithS
fuente
12
Posiblemente, pero a pesar de las muestras de código, la pregunta que se hace es IMO más conceptual, lo que está más en línea con esta área de SE. Lo que es un patrón o antipatrón se discute aquí a menudo, y esa es la verdadera pregunta; ¿Está estructurando un bucle para que se ejecute infinitamente y luego salir de él es algo malo?
KeithS
3
La do ... untilconstrucción también es útil para este tipo de bucles ya que no tienen que estar "cebados".
Blrfl
2
El caso de bucle y medio todavía carece de una respuesta realmente buena.
Loren Pechtel
1
"Sin embargo, esto duplica una llamada a un método en código, violando DRY" No estoy de acuerdo con esa afirmación y parece un malentendido del principio.
Andy
1
Aparte de que prefiero el estilo C para (;;) que explícitamente significa "Tengo un bucle y no verifico la declaración del bucle", su primer enfoque es absolutamente correcto. Tienes un bucle Hay trabajo por hacer antes de que pueda decidir si desea salir. Luego sale si se cumplen algunas condiciones. Luego haces el trabajo real y comienzas de nuevo. Eso está absolutamente bien, es fácil de entender, lógico, no redundante y fácil de entender. La segunda variante es simplemente horrible, mientras que la tercera simplemente no tiene sentido; volviéndose horrible si el resto del bucle que entra en el if es muy largo.
gnasher729

Respuestas:

14

Quédate con tu primera solución. Los bucles pueden involucrarse mucho y uno o más descansos limpios pueden ser mucho más fáciles para usted, cualquier otra persona que vea su código, el optimizador y el rendimiento del programa que elabora declaraciones if y variables agregadas. Mi enfoque habitual es configurar un bucle con algo como "while (verdadero)" y obtener la mejor codificación que pueda. A menudo puedo y luego reemplazo las interrupciones con un control de bucle estándar; La mayoría de los bucles son bastante simples, después de todo. La estructura de bucle debe verificar la condición de finalización por las razones que menciona, pero no a costa de agregar nuevas variables enteras, agregar sentencias if y repetir código, todo lo cual es aún más propenso a errores.

RalphChapin
fuente
"sentencias if y código repetitivo, todos los cuales son aún más propensos a errores". - Sí, cuando trato con personas a las que llamo "futuros errores" de If
Pat
13

Es solo un problema significativo si el cuerpo del bucle no es trivial. Si el cuerpo es tan pequeño, entonces analizarlo de esta manera es trivial y no es un problema en absoluto.

DeadMG
fuente
7

Tengo problemas para encontrar las otras soluciones mejor que la que tiene un descanso. Bueno, prefiero la forma Ada que permite hacer

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

pero la solución de ruptura es la representación más limpia.

Mi punto de vista es que cuando falta una estructura de control, la solución más limpia es emularla con otras estructuras de control, sin utilizar el flujo de datos. (Y de manera similar, cuando tengo un problema de flujo de datos para preferir soluciones de flujo de datos puro al que involucra estructuras de control *).

Es más difícil descubrir cómo saldrías del bucle,

No se equivoque, la discusión no tendría lugar si la dificultad no fuera la misma: la condición es la misma y está presente en el mismo lugar.

Cual de

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

y

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

hacer mejor la estructura prevista? Voy a votar por el primero, no tienes que verificar que las condiciones coincidan. (Pero mira mi sangría).

Un programador
fuente
1
Oh, mira, un lenguaje que no admite directamente bucles mediados de salida! :)
mjfgates
Me gusta su punto de vista sobre la emulación de estructuras de control que faltan con estructuras de control. Esa es probablemente una buena respuesta a las preguntas relacionadas con GOTO también. Uno debe usar las estructuras de control de un lenguaje cada vez que se ajustan a los requisitos semánticos, pero si un algoritmo requiere algo más, uno debe usar la estructura de control requerida por el algoritmo.
supercat
3

while (verdadero) y break es un idioma común. Es la forma canónica de implementar bucles de salida media en lenguajes tipo C. Agregar una variable para almacenar la condición de salida y un if () alrededor de la segunda mitad del ciclo es una alternativa razonable. Algunas tiendas pueden estandarizarse oficialmente en una u otra, pero ninguna es superior; son equivalentes

Un buen programador que trabaje en estos idiomas debería ser capaz de saber lo que está sucediendo de un vistazo si ve cualquiera de ellos. Podría ser una buena pregunta para la entrevista; entregarle a alguien dos bucles, uno usando cada idioma, y ​​preguntarles cuál es la diferencia (respuesta correcta: "nada", con quizás una adición de "pero habitualmente uso ESO").

mjfgates
fuente
2

Sus alternativas no son tan malas como puede pensar. Un buen código debería:

  1. Indique claramente con buenos nombres / comentarios de variables bajo qué condiciones se debe terminar el ciclo. (La condición de ruptura no debe ocultarse)

  2. Indique claramente cuál es el orden de las operaciones. Aquí es donde poner la tercera operación opcional ( DoSomethingElseTo(input)) dentro del if es una buena idea.

Dicho esto, es fácil dejar que los instintos perfeccionistas y pulidores de latón consuman mucho tiempo. Simplemente modificaría el if como se mencionó anteriormente; comenta el código y sigue adelante.

Palmadita
fuente
Creo que los instintos perfeccionistas y pulidores de latón ahorran tiempo a largo plazo. Esperemos que, con la práctica, desarrolle tales hábitos para que su código sea perfecto y su latón pulido sin consumir mucho tiempo. Pero a diferencia de la construcción física, el software puede durar para siempre y utilizarse en un número infinito de lugares. El ahorro del código que es incluso 0.00000001% más rápido, más confiable, utilizable y mantenible puede ser grande. (Por supuesto, la mayor parte de mi código tan pulido es en lenguajes de largo obsoletos o ganar dinero para alguien más en estos días, pero se hicieron ayuda a desarrollar buenos hábitos.)
RalphChapin
Bueno, no puedo llamarte mal :-) Esta es una pregunta de opinión y si surgieron buenas habilidades del pulido de latón: genial.
Pat
Creo que es una posibilidad de considerar, de todos modos. Ciertamente no quisiera ofrecer ninguna garantía. Me hace sentir mejor pensar que mi pulido de latón mejoró mis habilidades, por lo que puedo tener prejuicios sobre este punto.
RalphChapin 01 de
2

La gente dice que es una mala práctica usar while (true), y muchas veces tienen razón. Si tuwhile declaración contiene muchos códigos complicados y no está claro cuándo está rompiendo si, entonces queda un código difícil de mantener y un error simple podría causar un bucle infinito.

En su ejemplo, es correcto de usar while(true)porque su código es claro y fácil de entender. No tiene sentido crear código ineficiente y difícil de leer, simplemente porque algunas personas consideran que siempre es una mala práctica usarlo while(true).

Me enfrenté a un problema similar:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

El uso do ... while(true)evita isset($array][$key]innecesariamente ser llamado dos veces, el código es más corto, más limpio y más fácil de leer. No hay absolutamente ningún riesgo de que se introduzca un error de bucle infinito else break;al final.

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

Es bueno seguir las buenas prácticas y evitar las malas prácticas, pero siempre hay excepciones y es importante mantener una mente abierta y darse cuenta de esas excepciones.

Dan Bray
fuente
0

Si un flujo de control particular se expresa naturalmente como una declaración de interrupción, esencialmente nunca es una mejor opción utilizar otros mecanismos de control de flujo para emular una declaración de interrupción.

(tenga en cuenta que esto incluye desenrollar parcialmente el bucle y deslizar el cuerpo del bucle hacia abajo para que el bucle comience coincida con la prueba condicional)

Si puede reorganizar su ciclo para que el flujo de control se exprese naturalmente al tener una verificación condicional al comienzo del ciclo, genial. Incluso podría valer la pena pasar un tiempo pensando si eso es posible. Pero no es así, no intentes colocar una clavija cuadrada en un agujero redondo.


fuente
0

También podrías ir con esto:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

O menos confuso:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
Eclipse
fuente
1
Si bien a algunas personas les gusta, afirmo que la condición del bucle generalmente debe reservarse para pruebas condicionales, en lugar de declaraciones que hacen cosas.
55
Expresión de coma en una condición de bucle ... Eso es horrible. Eres muy listo Y solo funciona en este caso trivial donde TransformInSomeWay se puede expresar como una expresión.
gnasher729
0

Cuando la complejidad de la lógica se vuelve difícil de manejar, el uso de un bucle sin límites con interrupciones de medio bucle para el control de flujo realmente tiene más sentido. Tengo un proyecto con un código similar al siguiente. (Observe la excepción al final del ciclo).

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Para hacer esta lógica sin romper el bucle sin límites, terminaría con algo como lo siguiente:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Entonces la excepción ahora se lanza en un método auxiliar. También tiene bastante if ... elseanidamiento. No estoy satisfecho con este enfoque. Por lo tanto, creo que el primer patrón es el mejor.

intrepidis
fuente
De hecho, hice esta pregunta en SO y fui derribado en llamas ... stackoverflow.com/questions/47253486/…
intrepidis