¿Son las prácticas de programación incorrectas 'break' y 'continue'?

178

Mi jefe sigue mencionando indiferentemente que los programadores malos usan break y continue en bucles.

Los uso todo el tiempo porque tienen sentido; Déjame mostrarte la inspiración:

function verify(object) {
    if (object->value < 0) return false;
    if (object->value > object->max_value) return false;
    if (object->name == "") return false;
    ...
}

El punto aquí es que primero la función comprueba que las condiciones son correctas, luego ejecuta la funcionalidad real. IMO lo mismo se aplica con los bucles:

while (primary_condition) {
    if (loop_count > 1000) break;
    if (time_exect > 3600) break;
    if (this->data == "undefined") continue;
    if (this->skip == true) continue;
    ...
}

Creo que esto hace que sea más fácil de leer & depurar; pero tampoco veo un inconveniente.

    
pregunta Mikhail 16.08.2017 - 19:16
fuente

20 respuestas

227

Cuando se usan al comienzo de un bloque, como las primeras comprobaciones se realizan, actúan como condiciones previas, por lo que es bueno.

Cuando se usan en medio del bloque, con algún código alrededor, actúan como trampas ocultas, por lo que es malo.

    
respondido por el Klaim 15.03.2011 - 16:08
fuente
83

Puede leer el artículo de Donald Knuth de 1974 Programación estructurada con go a Declaraciones , en la que analiza varios usos del go to que son estructuralmente deseables. Incluyen el equivalente de las declaraciones break y continue (muchos de los usos de go to en ellos se han desarrollado en construcciones más limitadas). ¿Es su jefe el tipo para llamar a Knuth un mal programador?

(Los ejemplos dados me interesan. Por lo general, a las personas a las que les gusta una entrada y una salida de cualquier parte del código no les gusta a break y continue , y ese tipo de persona también frunce el ceño en varias declaraciones de return . )

    
respondido por el David Thornley 15.03.2011 - 16:12
fuente
40

No creo que sean malos. La idea de que son malos viene de los días de la programación estructurada. Está relacionado con la noción de que una función debe tener un solo punto de entrada y un solo punto de salida, i. mi. solo un return por función

Esto tiene algún sentido si su función es larga y si tiene varios bucles anidados. Sin embargo, sus funciones deben ser cortas, y debe envolver los bucles y sus cuerpos en funciones cortas propias. Generalmente, obligar a una función a tener un solo punto de salida puede resultar en una lógica muy complicada.

Si su función es muy corta, si tiene un solo bucle o, en el peor de los casos, dos bucles anidados, y si el cuerpo del bucle es muy corto, entonces está muy claro lo que hace un break o un continue . También está claro lo que hacen varias declaraciones return .

Estos problemas se tratan en "Clean Code" por Robert C. Martin y en "Refactoring" por Martin Fowler.

    
respondido por el Dima 15.03.2011 - 16:11
fuente
32

Los malos programadores hablan en términos absolutos (al igual que Sith). Los buenos programadores utilizan la solución más clara posible ( todas las demás cosas son iguales ).

Usar break y continue frecuentemente hace que el código sea difícil de seguir. Pero si reemplazarlos hace que el código sea aún más difícil de seguir, entonces es un mal cambio.

El ejemplo que dio es definitivamente una situación en la que los descansos y continuos deben reemplazarse por algo más elegante.

    
respondido por el Matthew Read 06.10.2011 - 00:34
fuente
22

La mayoría de la gente piensa que es una mala idea porque el comportamiento no es fácilmente predecible. Si está leyendo el código y ve que while(x < 1000){} asume que se ejecutará hasta x > = 1000 ... Pero si hay interrupciones en el medio, eso no es cierto, por lo que puede ' Realmente confío en tus bucles ...

Es la misma razón por la que a la gente no le gusta GOTO: seguro, se puede usar bien, pero también puede llevar a un código espagueti espantoso, donde el código salta al azar de una sección a otra.

Para mí, si fuera a hacer un bucle que se rompió en más de una condición, haría while(x){} y luego alternaría X a falso cuando necesitaba salir. El resultado final sería el mismo, y cualquier persona que lea el código sabrá que debe mirar más de cerca las cosas que cambiaron el valor de X.

    
respondido por el Satanicpuppy 15.03.2011 - 16:07
fuente
14

Sí, puede [re] escribir programas sin sentencias de ruptura (o retornos desde la mitad de los bucles, que hacen lo mismo). Pero es posible que tenga que introducir variables adicionales y / o duplicación de código, las cuales generalmente hacen que el programa sea más difícil de entender. Pascal (el lenguaje de programación) era muy malo especialmente para los programadores principiantes por esa razón. Tu jefe básicamente quiere que programes en las estructuras de control de Pascal. Si Linus Torvalds estuviera en tus zapatos, ¡probablemente le mostraría el dedo medio a tu jefe!

Hay un resultado informático llamado la jerarquía de las estructuras de control de Kosaraju, que se remonta a 1973 y que se menciona en el famoso artículo de Knuth sobre gotos de 1974. (Este documento de Knuth ya fue recomendado anteriormente por David Thornley, por cierto.) Lo que S. Rao Kosaraju demostró en 1973 es que no es posible reescribir todos los programas que tienen cortes de profundidad de varios niveles n en programas con una profundidad de corte menor que n sin introducir variables extra. Pero digamos que es solo un resultado puramente teórico. (¡¿Solo agrega algunas variables extra ?! Seguramente puedes hacer eso para complacer a tu jefe ...)

Lo que es mucho más importante desde la perspectiva de la ingeniería de software es un artículo más reciente de 1995 de Eric S. Roberts titulado Salidas de bucle y programación estructurada: reapertura del debate ( enlace ). Roberts resume varios estudios empíricos realizados por otros antes que él. Por ejemplo, cuando se pidió a un grupo de estudiantes del tipo CS101 que escribieran código para una función que implementa una búsqueda secuencial en una matriz, el autor del estudio dijo lo siguiente sobre aquellos estudiantes que usaron un descanso / retorno / goto para salir del el bucle de búsqueda secuencial cuando se encontró el elemento:

  

Todavía tengo que encontrar a una sola persona que intentó un programa usando [este estilo] que produjo una solución incorrecta.

Roberts también dice que:

  

A los estudiantes que intentaron resolver el problema sin utilizar un retorno explícito del bucle for no les fue tan bien: solo siete de los 42 estudiantes que intentaron esta estrategia lograron generar las soluciones correctas. Esa cifra representa una tasa de éxito inferior al 20%.

Sí, puede que tenga más experiencia que los estudiantes de CS101, pero sin usar la instrucción break (o el equivalente de retorno / goto desde el medio de los bucles), eventualmente escribirá un código que, aunque nominalmente está bien estructurado, es lo suficientemente vicioso de las variables lógicas adicionales y la duplicación de código, alguien, probablemente tú mismo, colocará errores lógicos al intentar seguir el estilo de codificación de tu jefe.

También voy a decir aquí que el documento de Roberts es mucho más accesible para el programador promedio, por lo que es una mejor primera lectura que la de Knuth. También es más corto y cubre un tema más estrecho. Probablemente incluso puedas recomendarlo a tu jefe, incluso si él es el tipo de administración en lugar del tipo CS.

    
respondido por el user3588161 15.07.2014 - 01:37
fuente
9

No considero utilizar ninguna de estas malas prácticas, pero usarlas demasiado dentro del mismo bucle debería justificar un replanteamiento de la lógica que se usa en el bucle. Utilízalos con moderación.

    
respondido por el Bernard 15.03.2011 - 16:00
fuente
7

El ejemplo que dio no necesita interrupciones ni continúa:

while (primary-condition AND
       loop-count <= 1000 AND
       time-exec <= 3600) {
   when (data != "undefined" AND
           NOT skip)
      do-something-useful;
   }

Mi "problema" con las 4 líneas en su ejemplo es que todas están en el mismo nivel pero hacen cosas diferentes: algunas se rompen, algunas continúan ... Debe leer cada línea.

En mi enfoque anidado, cuanto más profundo vayas, más "útil" se vuelve el código.

Pero, si en el fondo encontraras una razón para detener el bucle (que no sea la condición primaria), una ruptura o retorno tendría su uso. Prefiero el uso de una marca adicional que se va a probar en la condición de nivel superior. El quiebre / retorno es más directo; indica mejor la intención que establecer otra variable.

    
respondido por el Peter Frings 16.03.2011 - 13:35
fuente
6

La "maldad" depende de cómo los uses. Normalmente uso cortes en construcciones de bucle SOLAMENTE cuando me ahorrará ciclos que no se pueden guardar a través de una refactorización de un algoritmo. Por ejemplo, pasar por una colección en busca de un elemento con un valor en una propiedad específica establecida en verdadero. Si todo lo que necesita saber es que uno de los elementos tenía esta propiedad establecida en verdadero, una vez que logre ese resultado, una interrupción es buena para terminar el bucle de manera apropiada.

Si usar una pausa no hará que el código sea más fácil de leer, más corto para ejecutar o guardar los ciclos en el procesamiento de manera significativa, entonces es mejor no usarlos. Tiendo a codificar el "denominador común más bajo" cuando sea posible para asegurarme de que cualquier persona que me sigue pueda mirar mi código fácilmente y averiguar qué está pasando (no siempre tengo éxito en esto). Los descansos reducen eso porque introducen puntos de entrada / salida impares. Utilizados incorrectamente, pueden comportarse de manera muy parecida a una declaración "goto" fuera de lugar.

    
respondido por el Joel Etherton 15.03.2011 - 16:08
fuente
4

Absolutamente no ... Sí, el uso de goto es malo porque deteriora la estructura de su programa y también es muy difícil entender el flujo de control.

Pero el uso de declaraciones como break y continue son absolutamente necesarios en estos días y no se consideran en absoluto como una mala práctica de programación.

Y tampoco es tan difícil de entender el flujo de control en uso de break y continue . En construcciones como switch , la declaración break es absolutamente necesaria.

    
respondido por el Radheshyam Nayak 27.11.2013 - 15:09
fuente
3

La noción esencial proviene de poder analizar semánticamente su programa. Si tiene una sola entrada y una única salida, la matemática necesaria para denotar posibles estados es considerablemente más fácil que si tuviera que administrar rutas de bifurcación.

En parte, esta dificultad se refleja en la capacidad de razonar conceptualmente acerca de su código.

Francamente, tu segundo código no es obvio. ¿Qué está haciendo? ¿Continúa "continuar" o "sigue" el bucle? No tengo idea. Al menos tu primer ejemplo es claro.

    
respondido por el Paul Nathan 15.03.2011 - 16:56
fuente
3

Reemplazaría tu segundo fragmento de código con

while (primary_condition && (loop_count <= 1000 && time_exect <= 3600)) {
    if (this->data != "undefined" && this->skip != true) {
        ..
    }
}

no por razones de terseness: realmente creo que es más fácil de leer y que alguien entienda lo que está pasando. En términos generales, las condiciones para sus bucles deben estar contenidas únicamente dentro de aquellas condiciones de bucle que no se encuentran en todo el cuerpo. Sin embargo, hay algunas situaciones en las que break y continue pueden ayudar a la legibilidad. break moreso que continue podría agregar: D

    
respondido por el El Ronnoco 13.06.2017 - 23:09
fuente
2

No estoy de acuerdo con tu jefe. Hay lugares adecuados para usar break y continue . De hecho, la razón por la que se introdujeron las excepciones y el manejo de excepciones en los lenguajes de programación modernos es que no se pueden resolver todos los problemas con solo structured techniques .

En una nota al margen No quiero iniciar una discusión religiosa aquí, pero podría reestructurar su código para que sea aún más fácil de leer así:

while (primary_condition) {
    if (loop_count > 1000) || (time_exect > 3600) {
        break;
    } else if ( ( this->data != "undefined") && ( !this->skip ) ) {
       ... // where the real work of the loop happens
    }
}

En otra nota al margen

Personalmente no me gusta el uso de ( flag == true ) en condicionales porque si la variable ya es booleana, entonces está introduciendo una comparación adicional que debe suceder cuando el valor del booleano tenga la respuesta que desea, a menos que, por supuesto, están seguros de que su compilador optimizará esa comparación adicional de distancia.

    
respondido por el Zeke Hansell 13.06.2017 - 23:08
fuente
1

Estoy de acuerdo con tu jefe. Son malos porque producen métodos con alta complejidad ciclomática. Tales métodos son difíciles de leer y difíciles de probar. Afortunadamente hay una solución fácil. Extraiga el cuerpo del bucle en un método separado, donde "continuar" se convierte en "retorno". "Volver" es mejor porque después de "regresar" se acabó, no hay preocupaciones sobre el estado local.

Para "break" extraiga el bucle en un método separado, reemplazando "break" por "return".

Si los métodos extraídos requieren una gran cantidad de argumentos, eso es una indicación para extraer una clase, ya sea que los recoja en un objeto de contexto.

    
respondido por el kevin cline 16.08.2017 - 19:28
fuente
0

Creo que solo es un problema cuando está anidado profundamente dentro de múltiples bucles. Es difícil saber en qué bucle estás rompiendo. También puede ser difícil seguir una continuación, pero creo que el verdadero dolor proviene de los descansos: la lógica puede ser difícil de seguir.

    
respondido por el davidhaskins 15.03.2011 - 15:54
fuente
0

Mientras no se usen como goto disfrazado como en el siguiente ejemplo:

do
{
      if (foo)
      {
             /*** code ***/
             break;
      }

      if (bar)
      {
             /*** code ***/
             break;
      }
} while (0);

Estoy bien con ellos. (Ejemplo visto en código de producción, meh)

    
respondido por el SuperBloup 13.06.2017 - 23:09
fuente
0

No me gusta ninguno de estos estilos. Esto es lo que preferiría:

function verify(object)
{
    if not (object->value < 0) 
       and not(object->value > object->max_value)
       and not(object->name == "") 
       {
         do somethign important
       }
    else return false; //probably not necessary since this function doesn't even seem to be defined to return anything...?
}

Realmente no me gusta usar return para abortar una función. Se siente como un abuso de return .

El uso de break tampoco es siempre fácil de leer.

Mejor aún podría ser:

notdone := primarycondition    
while (notDone)
{
    if (loop_count > 1000) or (time_exect > 3600)
    {
       notDone := false; 
    }
    else
    { 
        skipCurrentIteration := (this->data == "undefined") or (this->skip == true) 

        if not skipCurrentIteration
        {
           do something
        } 
        ...
    }
}

se anidan menos y las condiciones complejas se refaccionan en variables (en un programa real tendrías que tener mejores nombres, obviamente ...)

(Todo el código anterior es pseudo-código)

    
respondido por el FrustratedWithFormsDesigner 13.06.2017 - 23:10
fuente
0

No. Es una forma de resolver un problema, y hay otras formas de resolverlo.

Muchos lenguajes principales actuales (Java, .NET (C # + VB), PHP, escriben los suyos propios) usan "break" y "continue" para omitir bucles. Ambos son oraciones "goto (s) estructuradas".

Sin ellos:

String myKey = "mars";

int i = 0; bool found = false;
while ((i < MyList.Count) && (not found)) {
  found = (MyList[i].key == myKey);
  i++;   
}
if (found)
  ShowMessage("Key is " + i.toString());
else
  ShowMessage("Not found.");

Con ellos:

String myKey = "mars";

for (i = 0; i < MyList.Count; i++) {
  if (MyList[i].key == myKey)
    break;
}
ShowMessage("Key is " + i.toString());

Tenga en cuenta que el código de "interrupción" y "continuar" es más corto, y generalmente convierte "mientras que las oraciones en oraciones" para "o" para cada uno ".

Ambos casos son una cuestión de estilo de codificación. Prefiero no usarlos , porque el estilo detallado me permite tener más control del código.

En realidad, trabajo en algunos proyectos, donde era obligatorio usar esas oraciones.

Algunos desarrolladores pueden pensar que no son necesariamente, pero hipotéticos, si tuviéramos que eliminarlos, tendremos que eliminar "while" y "do while" ("repetir hasta", ustedes pascal) también; -)

Conclusión, incluso si prefiero no usarlos, creo que es una opción, no una mala práctica de programación.

    
respondido por el umlcat 13.06.2017 - 23:10
fuente
0

No estoy en contra de continue y break en principio, pero creo que son construcciones de muy bajo nivel que muy a menudo se pueden reemplazar por algo incluso mejor.

Estoy usando C # como ejemplo aquí, considere el caso de querer iterar sobre una colección, pero solo queremos los elementos que cumplen algún predicado, y no queremos hacer más de un máximo de 100 iteraciones.

for (var i = 0; i < collection.Count; i++)
{
    if (!Predicate(item)) continue;
    if (i >= 100) break; // at first I used a > here which is a bug. another good thing about the more declarative style!

    DoStuff(item);
}

Esto se ve razonablemente limpio. No es muy difícil de entender. Sin embargo, creo que podría ganar mucho siendo más declarativo. Compáralo con lo siguiente:

foreach (var item in collection.Where(Predicate).Take(100))
    DoStuff(item);

Quizás las llamadas Where y Take no deberían estar en este método. Tal vez este filtrado debería realizarse ANTES de que la colección se pase a este método. De todos modos, al alejarse de las cosas de bajo nivel y centrarse más en la lógica de negocios real, se vuelve más claro en lo que realmente nos interesa. Es más fácil separar nuestro código en módulos cohesivos que se adhieran más a las buenas prácticas de diseño y así en.

Las cosas de bajo nivel seguirán existiendo en algunas partes del código, pero queremos ocultar esto tanto como sea posible, porque requiere energía mental que podríamos estar utilizando para razonar sobre los problemas comerciales.

    
respondido por el kai 13.06.2017 - 23:11
fuente
-1

Code Complete tiene una buena sección sobre el uso de goto y múltiples devoluciones de la rutina o el ciclo.

En general no es una mala práctica. break o continue dicen exactamente qué sucede a continuación. Y estoy de acuerdo con esto.

Steve McConnell (autor de Code Complete) usa casi los mismos ejemplos que usted para mostrar las ventajas de usar varias declaraciones goto .

Sin embargo, el uso excesivo de break o continue podría llevar a un software complejo e imposible de mantener.

    
respondido por el Grzegorz Gierlik 16.03.2011 - 16:34
fuente

Lea otras preguntas en las etiquetas