Cómo estructurar un ciclo que se repite hasta el éxito y maneja los fracasos

8

Soy un programador autodidacta. Empecé a programar hace aproximadamente 1,5 años. Ahora he comenzado a tener clases de programación en la escuela. Hemos tenido clases de programación durante 1/2 año y tendremos otro 1/2 en este momento.

En las clases estamos aprendiendo a programar en C ++ (que es un lenguaje que ya sabía que usaba bastante bien antes de comenzar).

No he tenido ninguna dificultad durante esta clase, pero hay un problema recurrente para el que no he podido encontrar una solución clara.

El problema es así (en pseudocódigo):

 do something
 if something failed:
     handle the error
     try something (line 1) again
 else:
     we are done!

Aquí hay un ejemplo en C ++

El código solicita al usuario que ingrese un número y lo hace hasta que la entrada sea válida. Se utiliza cin.fail()para verificar si la entrada no es válida. Cuándo cin.fail()es trueque tengo que llamar cin.clear()y cin.ignore()poder continuar recibiendo información de la transmisión.

Soy consciente de que este código no verifica EOF. No se espera que los programas que hemos escrito hagan eso.

Así es como escribí el código en una de mis tareas en la escuela:

for (;;) {
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        continue;
    }
    break;
}

Mi maestra dijo que no debería estar usando breaky continueasí. Me sugirió que debería usar un regular whileo un do ... whilebucle en su lugar.

Me parece que usar breaky continuees la forma más sencilla de representar este tipo de bucle. De hecho, lo pensé durante bastante tiempo, pero realmente no se me ocurrió una solución más clara.

Creo que él quería que yo hiciera algo como esto:

do {
    cout << ": ";
    cin >> input;
    bool fail = cin.fail();
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (fail);

Para mí, esta versión parece mucho más compleja ya que ahora también tenemos variables llamadas failpara realizar un seguimiento y la verificación de fallas de entrada se realiza dos veces en lugar de solo una.

También pensé que podía escribir el código de esta manera (abusando de la evaluación de cortocircuito):

do {
    cout << ": ";
    cin >> input;
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (cin.fail() && (cin.clear(), cin.ignore(512, '\n', true);

Esta versión funciona exactamente como las otras. No utiliza breako continuey la cin.fail()prueba solo se realiza una vez. Sin embargo, no me parece correcto abusar de la "regla de evaluación de cortocircuito" como esta. Tampoco creo que a mi maestra le guste.

Este problema no solo se aplica solo a la cin.fail()comprobación. He usado breaky me continuegusta esto para muchos otros casos que implican repetir un conjunto de código hasta que se cumpla una condición en la que también se debe hacer algo si la condición no se cumple (como llamar cin.clear()y cin.ignore(...)del cin.fail()ejemplo).

He seguido usando breaky continuedurante todo el curso y ahora mi maestro ha dejado de quejarse al respecto.

¿Cuáles son tus opiniones sobre esto?

¿Crees que mi maestro tiene razón?

¿Conoces una mejor manera de representar este tipo de problema?

wefwefa3
fuente
66
No estás haciendo un bucle para siempre, entonces, ¿por qué estás usando algo que implica que estás haciendo un bucle para siempre? Un bucle para siempre no siempre es una mala elección (aunque generalmente lo es) en este caso obviamente es una mala elección porque comunica la intención incorrecta del código. El segundo método está más cerca de la intención del código, por lo que es mejor. El código en el segundo fragmento podría hacerse un poco más ordenado y más fácil de leer con una mínima reorganización.
Dunk
3
@Dunk Eso parece un poco dogmático. En realidad, es muy raro tener que hacer un bucle para siempre, aparte del nivel superior de los programas interactivos. Veo while (true)y al instante asumir que hay una break, returno throwen algún lugar allí. La introducción de una variable adicional que debe mantenerse un registro de sólo para esquivar una breako continuees contraproducente. Yo diría que el problema con el código inicial es que el ciclo nunca termina una iteración normalmente, y que necesita saber que hay un continueadentro ifpara saber breakque no se tomará.
Doval
1
¿Es demasiado pronto para que aprenda el manejo de excepciones?
Mawg dice que reinstale a Mónica el
2
@Dunk Es posible que sepa de inmediato lo que se pretendía , pero tendrá que hacer una pausa y pensar más para verificar que sea correcto , lo que es peor. dataIsValides mutable, por lo que para saber que el ciclo termina correctamente, necesito verificar que esté configurado cuando los datos son válidos, y también que no esté desarmado en ningún momento posterior. En un bucle de la forma do { ... if (!expr) { break; } ... } while (true);, sé que si se exprevalúa como false, el bucle se romperá sin tener que mirar el resto del cuerpo del bucle.
Doval
1
@Doval: Tampoco sé cómo leer el código, pero ciertamente no leo todas las líneas cuando estoy tratando de averiguar qué está haciendo o dónde podría estar el error que estoy buscando. Si vi el ciclo while, es inmediatamente evidente (a partir de mi ejemplo) que el ciclo está obteniendo datos válidos. No me importa cómo lo esté haciendo, puedo omitir todo lo demás dentro del ciclo a menos que suceda que por alguna razón obtengo datos no válidos. Luego miraría el código dentro del bucle. Su idea requiere que mire todo el código del bucle para descubrir que no me interesa.
Dunk

Respuestas:

15

Escribiría la declaración if ligeramente diferente, por lo que se toma cuando la entrada es exitosa.

for (;;) {
    cout << ": ";
    if (cin >> input)
        break;
    cin.clear();
    cin.ignore(512, '\n');
}

Es más corto también.

Lo que sugiere una forma más corta que podría ser del agrado de tu maestro:

cout << ": ";
while (!(cin >> input)) {
    cin.clear();
    cin.ignore(512, '\n');
    cout << ": ";
}
Sjoerd
fuente
Buena idea revertir los condicionales como ese, así que no necesito un breakal final. Eso solo hace que el código sea mucho más fácil de leer. Yo tampoco sabía que eso !(cin >> input)era válido. ¡Gracias por hacérmelo saber!
wefwefa3
8
El problema con la segunda solución es que duplica la línea cout << ": ";, por lo que viola el principio DRY. Para el código del mundo real, esto sería imposible, ya que se vuelve propenso a errores (cuando tenga que cambiar esa parte más adelante, tendrá que asegurarse de no olvidar cambiar dos líneas de manera similar ahora). Sin embargo, te di un +1 para tu primera versión, que en mi humilde opinión es un ejemplo de cómo usarlo breakcorrectamente.
Doc Brown
2
Esto se siente como si te picaras los pelos, teniendo en cuenta que la línea duplicada es obviamente una interfaz de usuario normal. Podrías eliminar el primero por cout << ": "completo y nada se rompería.
jdevlin
2
@codingthewheel: tiene razón, este ejemplo es demasiado trivial para demostrar los riesgos potenciales al ignorar el precio DRY. Pero imagine una situación así en el código del mundo real, donde la introducción de errores accidentalmente puede tener consecuencias reales, entonces podría pensar diferente al respecto. Es por eso que después de tres décadas de programación, adquirí el hábito de nunca resolver un problema de salida de bucle duplicando la primera parte simplemente "porque un whilecomando se ve mejor".
Doc Brown
1
@DocBrown La duplicación de cout es principalmente un artefacto del ejemplo. En la práctica, las dos declaraciones cout serán diferentes ya que la segunda contendrá un mensaje de error. Por ejemplo, "Ingrese <foo>:" y "¡Error! Vuelva a ingresar <foo>:" .
Sjoerd
15

Lo que debes esforzarte es evitar los bucles sin procesar .

Mueva la lógica compleja a una función auxiliar y de repente las cosas están mucho más claras:

bool getValidUserInput(string & input)
{
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        return false;
    }
    return true;
}

int main() {
    string input;
    while (!getValidUserInput(input)) { 
        // We wait for a valid user input...
    }
}
glampert
fuente
1
Esto es más o menos lo que iba a sugerir. Refactoríelo para separar la obtención de la entrada de la decisión de obtener la entrada.
Rob K
44
Estoy de acuerdo en que esta es una buena solución. Se convierte en la alternativa muy superior cuando getValidUserInputse llama muchas veces y no solo una. Te doy +1 por eso, pero aceptaré la respuesta de Sjoerd porque encuentro que responde mejor a mi pregunta.
wefwefa3
@ user3787875: exactamente lo que pensé cuando leí las dos respuestas: buena elección.
Doc Brown
Este es el siguiente paso lógico después de las reescrituras en mi respuesta.
Sjoerd
9

No es tanto que for(;;)sea ​​malo. Simplemente no es tan claro como patrones como:

while (cin.fail()) {
    ...
}

O como dijo Sjoerd:

while (!(cin >> input)) {
    ...
}

Consideremos a la audiencia principal de estas cosas como sus compañeros programadores, incluida la versión futura de usted mismo que ya no recuerda por qué detuvo el descanso al final, o saltó el descanso con la continuación. Este patrón de tu ejemplo:

for (;;) {
    ...
    if (blah) {
        continue;
    }
    break;
}

... requiere un par de segundos de pensamiento extra para simular en comparación con otros patrones. No es una regla difícil y rápida, pero saltar la declaración de ruptura con la continuación se siente desordenado o inteligente sin ser útil, y poner la declaración de ruptura al final del ciclo, incluso si funciona, es inusual. Normalmente ambos breaky continuese usan para evacuar prematuramente el ciclo o esa iteración del ciclo, por lo que verlos al final se siente un poco extraño, incluso si no lo es.

Aparte de eso, ¡cosas buenas!

jdevlin
fuente
Su respuesta se ve bien, pero solo a primera vista. De hecho, oculta el problema inherente que podemos ver en @Sjoerd s answer: using a mientras que el bucle de esta manera puede conducir a una duplicación de código innecesaria para el ejemplo dado. Y ese es un problema que, en mi humilde opinión, es al menos tan importante de abordar como la legibilidad general del bucle.
Doc Brown
Relájate, doc. OP solicitó algunos consejos básicos / comentarios sobre su estructura de bucle, le di la interpretación estándar. Puede argumentar a favor de otras interpretaciones: el enfoque estándar no siempre es el mejor o el más riguroso matemáticamente, pero especialmente dado que su propio instructor se inclinó hacia el uso del while, creo que el bit "solo a primera vista" no está justificado .
jdevlin
No me malinterpreten, esto no tiene nada que ver con ser "matemáticamente riguroso": se trata de escribir código evolutivo , código en el que agregar nuevas funciones o cambiar las funciones existentes tiene el menor riesgo posible de introducir errores. En mi trabajo, eso no es realmente un problema académico.
Doc Brown
Me gustaría agregar que estoy de acuerdo principalmente con su respuesta. Solo quería señalar que su simplificación al principio podría ocultar un problema frecuente. Cuando tengo la oportunidad de usar "while" sin duplicación de código, no dudaré en usarlo.
Doc Brown
1
De acuerdo, y a veces una o dos líneas innecesarias antes del ciclo es el precio que pagamos por esa whilesintaxis declarativa de carga frontal . No me gusta hacer algo antes del bucle que también se hace dentro del bucle, por otra parte, tampoco me gusta un bucle que se une en nudos tratando de refactorizar el prefijo. Por lo tanto, un poco de un acto de equilibrio de cualquier manera.
jdevlin
3

Mi instructor de lógica en la escuela siempre decía, y lo golpeaba en mi cerebro, que solo debería haber una entrada y una salida a los bucles. Si no, comienza a obtener el código de espagueti y no sabe a dónde va el código durante la depuración.

En la vida real, creo que la legibilidad del código es realmente importante, de modo que si alguien más necesita corregir o depurar un problema con su código, es fácil para ellos hacerlo.

Además, si se trata de un problema de rendimiento porque está ejecutando este aspecto millones de veces, el ciclo for podría ser más rápido. Las pruebas responderían a esa pregunta.

No conozco C ++ pero entendí completamente los dos primeros ejemplos de código. Supongo que continuar va al final del ciclo for y luego lo repite nuevamente. El ejemplo while lo entendí completamente, pero para mí fue más fácil de entender que su ejemplo for (;;). El tercero tendría que hacer un trabajo de reflexión para resolverlo.

Entonces, para mí, que fue más fácil de leer (sin saber c ++) y lo que dijo mi instructor de lógica, usaría el bucle while.

Jaydel Gluckie
fuente
44
Entonces, ¿su instructor está en contra de usar excepciones? ¿Y regreso temprano? ¿Y cosas similares? Empleados adecuadamente, todos hacen que el código sea más legible ...
Deduplicador
1
La frase clave en su oración elipsis es "empleada adecuadamente". Mi amarga experiencia me parece indicar que maldita sea cerca de NADIE en esta loca locura sabe cómo emplear esas cosas "correctamente", para casi cualquier definición razonable de "correctamente" (o "legible", para el caso).
John R. Strohm
2
Recuerde que estamos hablando de una clase donde los estudiantes son principiantes. Ciertamente, es una buena idea entrenarlos para evitar estas cosas que causan problemas comunes mientras son principiantes (antes de que entiendan las implicaciones y puedan razonar sobre lo que sería una excepción a la regla).
user1118321
2
El dogma de "una entrada: una vez que sale" es, en mi humilde opinión, un punto de vista anticuado, probablemente "inventado" por Edward Dijkstra en un momento en que la gente jugaba con GOTO en idiomas como Basic, Pascal y COBOL. Estoy totalmente de acuerdo con lo que Deduplicator escribió anteriormente. Además, los primeros dos ejemplos del OP contienen solo una entrada y una salida, sin embargo, la legibilidad es mediocre.
Doc Brown
44
De acuerdo con @DocBrown. "Una entrada, una salida" en las bases de códigos de producción modernas tiende a ser una receta para el desmoronamiento del código y fomenta el anidamiento profundo de las estructuras de control.
jdevlin
-2

para mí, la traducción C más directa del pseudocódigo es

do
    {
    success = something();
    if (success == FALSE)
        {
        handle_the_error();
        }
    } while (success == FALSE)
\\ moving on ...

No entiendo por qué esta traducción obvia es un problema.

tal vez esto:

while (!something())
    {
    handle_the_error();
    }

eso se ve más simple.

robert bristow-johnson
fuente