¿A qué tipo de errores conducen las declaraciones "goto"? ¿Hay algún ejemplo históricamente significativo?

103

Entiendo que salvo por romper bucles anidados en bucles; la gotodeclaración es evadida y vilipendiada como un estilo de programación propenso a errores, que nunca se utilizará.

XKCD Texto alternativo : "Neal Stephenson piensa que es lindo nombrar sus etiquetas 'dengo'".
Vea el cómic original en: http://xkcd.com/292/

Porque aprendí esto temprano; Realmente no tengo ninguna idea o experiencia sobre a qué tipo de errores gotorealmente conduce. Entonces, ¿de qué estamos hablando aquí?

  • ¿Inestabilidad?
  • ¿Código no mantenible o ilegible?
  • ¿Vulnerabilidades de seguridad?
  • ¿Algo más por completo?

¿A qué tipo de errores conducen realmente las declaraciones "goto"? ¿Hay algún ejemplo históricamente significativo?

Akiva
fuente
33
¿Has leído el breve artículo original de Dijkstra sobre este tema? (Esa es una pregunta Sí / No, y cualquier respuesta que no sea un instante inequívoco "sí" es un "no".)
John R. Strohm
2
Supongo que una salida de alarma es cuando ocurre algo catastrófico. de donde nunca volverás. Como falla de energía, dispositivos o archivos que desaparecen. Algunos tipos de "en error goto" ... Pero creo que la mayoría de los errores "casi catastróficos" pueden manejarse mediante construcciones "try - catch" hoy en día.
Lenne
13
Nadie ha mencionado el GOTO calculado todavía. Cuando la tierra era joven, BASIC tenía números de línea. Podría escribir lo siguiente: 100 N = A * 100; GOTO N. Intenta depurar eso :)
mcottle
77
@ JohnR.Strohm He leído el artículo de Dijkstra. Fue escrito en 1968 y sugiere que la necesidad de gotos podría reducirse en idiomas que incluyen características tan avanzadas como las declaraciones y funciones de cambio. Fue escrito en respuesta a idiomas en los que goto era el método principal de control de flujo, y podía usarse para saltar a cualquier punto del programa. Mientras que en los lenguajes que se desarrollaron después de que se escribió este documento, por ejemplo, C, goto solo puede saltar a lugares en el mismo marco de pila, y generalmente se usa solo cuando otras opciones no funcionan bien. (continuación ...)
Ray
10
(... continuación) Sus puntos eran válidos en ese momento, pero ya no son relevantes, independientemente de si goto es una buena idea o no. Si queremos argumentar eso de una forma u otra, deben hacerse argumentos relacionados con los lenguajes modernos. Por cierto, ¿ has leído la "Programación estructurada con declaraciones Goto" de Knuth?
Ray

Respuestas:

2

Aquí hay una manera de verlo que aún no he visto en las otras respuestas.

Se trata de alcance. Uno de los pilares principales de las buenas prácticas de programación es mantener su alcance ajustado. Necesita un alcance limitado porque carece de la capacidad mental para supervisar y comprender más que unos pocos pasos lógicos. Entonces crea pequeños bloques (cada uno de los cuales se convierte en "una cosa") y luego construye con ellos para crear bloques más grandes que se convierten en una sola cosa, y así sucesivamente. Esto mantiene las cosas manejables y comprensibles.

Un goto efectivamente infla el alcance de su lógica a todo el programa. Esto seguramente derrotará a su cerebro para cualquiera de los programas más pequeños que abarcan solo unas pocas líneas.

Por lo tanto, ya no podrá saber si cometió un error porque hay demasiado para controlar y controlar su pobre cabecita. Este es el verdadero problema, los errores son solo un resultado probable.

Martin Maat
fuente
¿Goto no local?
Deduplicador
thebrain.mcgill.ca/flash/capsules/experience_jaune03.html << La mente humana puede realizar un seguimiento de un promedio de 7 cosas a la vez. Su justificación juega con esta limitación, ya que ampliar el alcance agregará muchas más cosas que deben mantenerse al tanto. Por lo tanto, los tipos de errores que se introducirán son probablemente de omisión y olvido. También ofrece una estrategia de mitigación y buenas prácticas en general, de "mantener su alcance ajustado". Como tal, acepto esta respuesta. Buen trabajo Martin.
Akiva
1
¡¿Cuan genial es eso?! Entre más de 200 votos emitidos, ganando con solo 1!
Martin Maat
68

No es que gotosea ​​malo en sí mismo. (Después de todo, cada instrucción de salto en una computadora es un goto.) El problema es que existe un estilo humano de programación que precede a la programación estructurada, lo que podría llamarse programación de "diagrama de flujo".

En la programación de diagramas de flujo (que la gente de mi generación aprendió y se usó para el programa Apollo moon), hace un diagrama con bloques para ejecuciones de declaraciones y diamantes para decisiones, y puede conectarlos con líneas que van por todo el lugar. (El llamado "código de espagueti").

El problema con el código de espagueti es que usted, el programador, podría "saber" que era correcto, pero ¿cómo podría probarlo usted mismo o cualquier otra persona? De hecho, podría tener un mal comportamiento potencial, y su conocimiento de que siempre es correcto podría estar equivocado.

Junto vino la programación estructurada con bloques de inicio-fin, para, while, if-else, y así sucesivamente. Estos tenían la ventaja de que aún podía hacer cualquier cosa en ellos, pero si fuera cuidadoso, podría estar seguro de que su código era correcto.

Por supuesto, la gente aún puede escribir código de espagueti incluso sin él goto. El método común es escribir un while(...) switch( iState ){..., donde diferentes casos establecen iStatevalores diferentes. De hecho, en C o C ++ se podría escribir una macro para hacer eso y nombrarla GOTO, por lo que decir que no está usando gotoes una distinción sin una diferencia.

Como ejemplo de cómo la prueba de código puede evitar restricciones goto, hace mucho tiempo me topé con una estructura de control útil para cambiar dinámicamente las interfaces de usuario. Lo llamé ejecución diferencial . Es totalmente universal de Turing, pero su prueba de corrección depende de la programación estructurada pura - sin goto, return, continue, break, o excepciones.

ingrese la descripción de la imagen aquí

Mike Dunlavey
fuente
49
La corrección no se puede probar, excepto en el más trivial de los programas, incluso cuando se utiliza programación estructurada para escribirlos. Solo puede aumentar su nivel de confianza; la programación estructurada logra eso.
Robert Harvey
23
@MikeDunlavey: Relacionado: "Cuidado con los errores en el código anterior; solo he demostrado que es correcto, no lo he probado". staff.fnwi.uva.nl/p.vanemdeboas/knuthnote.pdf
Mooing Duck
26
@RobertHarvey No es del todo cierto que las pruebas de corrección solo sean prácticas para programas triviales. Sin embargo, se necesitan herramientas más especializadas que la programación estructurada.
Mike Haskel
18
@MSalters: es un proyecto de investigación. El objetivo de este proyecto de investigación es demostrar que podrían escribir un compilador verificado si tuvieran los recursos. Si observa su trabajo actual, verá que simplemente no están interesados ​​en admitir versiones posteriores de C, sino que están interesados ​​en expandir las propiedades de corrección que pueden probar. Y para eso, es completamente irrelevante si son compatibles con C90, C99, C11, Pascal, Fortran, Algol o Plankalkül.
Jörg W Mittag
8
@martineau: desconfío de esas "frases de bosón", en las que los programadores coinciden en que coinciden en que es malo o bueno, porque se sentarán si no lo hacen. Tiene que haber razones más básicas para las cosas.
Mike Dunlavey
63

¿Por qué es peligroso ir a Goto?

  • gotono causa inestabilidad por sí mismo. A pesar de unos 100.000 gotos, el kernel de Linux sigue siendo un modelo de estabilidad.
  • gotopor sí solo no debe causar vulnerabilidades de seguridad. Sin embargo, en algunos idiomas, mezclarlo con try/ catchbloques de administración de excepciones podría generar vulnerabilidades como se explica en esta recomendación del CERT . Los compiladores de C ++ convencionales señalan y evitan tales errores, pero desafortunadamente, los compiladores más antiguos o más exóticos no lo hacen.
  • gotocausa código ilegible e imposible de mantener. Esto también se llama código de espagueti , porque, como en un plato de espagueti, es muy difícil seguir el flujo de control cuando hay demasiados gotos.

Incluso si logras evitar el código de espagueti y si usas solo algunos gotos, todavía facilitan errores como la fuga de recursos:

  • El código que usa programación de estructura, con bloques anidados claros y bucles o interruptores, es fácil de seguir; Su flujo de control es muy predecible. Por lo tanto, es más fácil garantizar que se respeten los invariantes.
  • Con una gotodeclaración, rompes ese flujo directo y rompes las expectativas. Por ejemplo, es posible que no note que todavía tiene que liberar recursos.
  • Muchos gotoen diferentes lugares pueden enviarte a un solo objetivo de goto. Por lo tanto, no es obvio saber con certeza el estado en el que se encuentra al llegar a este lugar. El riesgo de hacer suposiciones erróneas / infundadas es, por lo tanto, bastante grande.

Información adicional y cotizaciones:

C proporciona la gotodeclaración y etiquetas infinitamente abusivas a las que ramificarse. Formalmente, gotonunca es necesario, y en la práctica casi siempre es fácil escribir código sin él. (...)
Sin embargo, sugeriremos algunas situaciones en las que los goto 's pueden encontrar un lugar. El uso más común es abandonar el procesamiento en algunas estructuras profundamente anidadas, como romper dos bucles a la vez. (...)
Aunque no somos dogmáticos sobre el asunto, parece que las declaraciones de goto deben usarse con moderación, si es que lo hacen .

  • James Gosling y Henry McGilton escribieron en su libro blanco sobre el entorno del lenguaje Java de 1995 :

    No más declaraciones Goto
    Java no tiene declaración Goto. Los estudios ilustraron que goto se usa (mal) con mayor frecuencia que simplemente "porque está ahí". La eliminación de goto condujo a una simplificación del lenguaje (...) Los estudios en aproximadamente 100,000 líneas de código C determinaron que aproximadamente el 90 por ciento de las declaraciones de goto se usaron exclusivamente para obtener el efecto de romper bucles anidados. Como se mencionó anteriormente, romper y continuar en varios niveles elimina la mayor parte de la necesidad de declaraciones goto.

  • Bjarne Stroustrup define goto en su glosario en estos términos atractivos:

    goto - el infame goto. Principalmente útil en código C ++ generado por máquina.

¿Cuándo se puede usar goto?

Como K&R, no soy dogmático sobre los gotos. Admito que hay situaciones en las que goto podría facilitar la vida.

Por lo general, en C, goto permite la salida de bucle multinivel, o el manejo de errores que requiere alcanzar un punto de salida apropiado que libera / desbloquea todos los recursos asignados hasta ahora (es decir, la asignación múltiple en secuencia significa múltiples etiquetas). Este artículo cuantifica los diferentes usos del goto en el kernel de Linux.

Personalmente prefiero evitarlo y en 10 años de C, usé un máximo de 10 gotos. Prefiero usar ifs anidados , que creo que son más legibles. Cuando esto llevaría a un anidamiento demasiado profundo, optaría por descomponer mi función en partes más pequeñas o usar un indicador booleano en cascada. Los compiladores de optimización actuales son lo suficientemente inteligentes como para generar casi el mismo código que el mismo código goto.

El uso de goto depende en gran medida del idioma:

  • En C ++, el uso adecuado de RAII hace que el compilador destruya automáticamente los objetos que quedan fuera del alcance, de modo que los recursos / bloqueo se limpiarán de todos modos, y ya no será necesario volver a usarlos.

  • En Java no hay necesidad de Goto (ver cita de autor de Java anterior y este excelente respuesta de desbordamiento de pila ): el recolector de basura que limpia el desorden, break, continue, y try/ catchmanejo de excepciones cubre todo el caso en el que gotopuedan ser de utilidad, pero en un mejor y más seguro conducta. La popularidad de Java demuestra que la declaración goto se puede evitar en un lenguaje moderno.

Zoom sobre la famosa vulnerabilidad SSL goto fail

Descargo de responsabilidad importante: en vista de la feroz discusión en los comentarios, quiero aclarar que no pretendo que la declaración goto sea la única causa de este error. No pretendo que sin ir a Goto no habría ningún error. Solo quiero mostrar que un goto puede estar involucrado en un error grave.

No sé cuántos errores graves están relacionados gotoen la historia de la programación: los detalles a menudo no se comunican. Sin embargo, hubo un famoso error SSL de Apple que debilitó la seguridad de iOS. La declaración que condujo a este error fue una gotodeclaración incorrecta .

Algunos argumentan que la causa raíz del error no fue la declaración goto en sí misma, sino una copia / pegar incorrecta, una sangría engañosa, falta de llaves alrededor del bloque condicional, o tal vez los hábitos de trabajo del desarrollador. Tampoco puedo confirmar ninguno de ellos: todos estos argumentos son hipótesis e interpretación probables. Nadie lo sabe realmente. ( mientras tanto, la hipótesis de una fusión que salió mal como alguien sugirió en los comentarios parece ser un muy buen candidato en vista de algunas otras inconsistencias de sangría en la misma función ).

El único hecho objetivo es que un duplicado gotocondujo a salir prematuramente de la función. Mirando el código, la única otra declaración única que podría haber causado el mismo efecto habría sido una devolución.

El error está en función SSLEncodeSignedServerKeyExchange()en este archivo :

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
        goto fail;
    if ((err =...) !=0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;  // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
    if (...)
        goto fail;

    ... // Do some cryptographic operations here

fail:
    ... // Free resources to process error

De hecho, las llaves alrededor del bloque condicional podrían haber evitado el error:
habría provocado un error de sintaxis en la compilación (y, por lo tanto, una corrección) o un goto inofensivo redundante. Por cierto, GCC 6 podría detectar estos errores gracias a su advertencia opcional para detectar sangría inconsistente.

Pero en primer lugar, todos estos problemas podrían haberse evitado con un código más estructurado. Entonces goto es al menos indirectamente una causa de este error. Hay al menos dos formas diferentes que podrían haberlo evitado:

Enfoque 1: si la cláusula ifes anidada

En lugar de probar muchas condiciones de error secuencialmente, y cada vez que se envía a una failetiqueta en caso de problema, uno podría haber optado por ejecutar las operaciones criptográficas en una ifdeclaración que lo haría solo si no hubiera una precondición incorrecta:

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
        (err = ...) == 0 ) &&
        (err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
        ...
        (err = ...) == 0 ) )
    {
         ... // Do some cryptographic operations here
    }
    ... // Free resources

Enfoque 2: use un acumulador de errores

Este enfoque se basa en el hecho de que casi todas las declaraciones aquí llaman a alguna función para establecer un errcódigo de error y ejecutan el resto del código solo si errfue 0 (es decir, la función se ejecutó sin error). Una buena alternativa segura y legible es:

bool ok = true;
ok =  ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok =  ok && (err = NextFunction(...)) == 0;
...
ok =  ok && (err = ...) == 0;
... // Free resources

Aquí, no hay un solo goto: no hay riesgo de saltar rápidamente al punto de salida de falla. Y visualmente sería fácil detectar una línea desalineada o olvidada ok &&.

Esta construcción es más compacta. Se basa en el hecho de que en C, la segunda parte de un lógico y ( &&) se evalúa solo si la primera parte es verdadera. De hecho, el ensamblador generado por un compilador optimizador es casi equivalente al código original con gotos: el optimizador detecta muy bien la cadena de condiciones y genera código, que en el primer valor de retorno no nulo salta al final ( prueba en línea ).

Incluso podría prever una comprobación de coherencia al final de la función que podría identificar durante la fase de prueba las discrepancias entre el indicador ok y el código de error.

assert( (ok==false && err!=0) || (ok==true && err==0) );

Errores tales como un ==0reemplazo inadvertido por un !=0error de conector lógico se detectarían fácilmente durante la fase de depuración.

Como dije: no pretendo que las construcciones alternativas hubieran evitado cualquier error. Solo quiero decir que podrían haber hecho que el error sea más difícil de producir.

Christophe
fuente
34
No, el error de fallo de goto de Apple fue causado por un programador inteligente que decidió no incluir las llaves después de una declaración if. :-) Entonces, cuando apareció el siguiente programador de mantenimiento (o el mismo, la misma diferencia) y agregó otra línea de código que solo debería haberse ejecutado cuando la instrucción if se evaluó como verdadera, la instrucción se ejecutó cada vez. Bam, el error "goto fail", que era un error de seguridad importante . Pero esencialmente no tenía nada que ver con el hecho de que se utilizó una declaración goto.
Craig el
47
El error "goto fail" de Apple fue causado directamente por la duplicación de una línea de código. El efecto particular de ese error fue causado por esa línea gotodentro de una ifdeclaración sin llaves. Pero si está sugiriendo que evitar gotohará que su código sea inmune a los efectos de líneas duplicadas al azar, tengo malas noticias para usted.
user253751
12
¿Está utilizando un comportamiento de operador booleano de acceso directo para ejecutar una declaración mediante un efecto secundario y afirma que es más claro que una ifdeclaración? Tiempos divertidos. :)
Craig
38
Si en lugar de "ir a fallar" hubiera habido una declaración "fallido = verdadero", exactamente lo mismo habría sucedido.
gnasher729
15
Ese error fue causado por un desarrollador descuidado que no prestó atención a lo que estaba haciendo y no leyó sus propios cambios de código. Nada más y nada menos. De acuerdo, quizás también incluyas algunos descuidos de prueba.
Carreras de ligereza en órbita
34

El famoso artículo de Dijkstra fue escrito en un momento en que algunos lenguajes de programación eran realmente capaces de crear subrutinas con múltiples puntos de entrada y salida. En otras palabras, podría saltar literalmente al centro de una función y saltar en cualquier lugar dentro de la función, sin llamar a esa función o regresar de la manera convencional. Eso sigue siendo cierto para el lenguaje ensamblador. Nadie argumenta que dicho enfoque es superior al método estructurado de escribir software que ahora utilizamos.

En la mayoría de los lenguajes de programación modernos, las funciones se definen muy específicamente con un punto de entrada y un punto de salida. El punto de entrada es donde especifica los parámetros de la función y lo llama, y ​​el punto de salida es donde devuelve el valor resultante y continúa la ejecución en la instrucción que sigue a la llamada a la función original.

Dentro de esa función, deberías poder hacer lo que quieras, dentro de lo razonable. Si poner un goto o dos en la función lo hace más claro o mejora su velocidad, ¿por qué no? El objetivo de una función es secuestrar un poco de funcionalidad claramente definida, para que ya no tenga que pensar en cómo funciona internamente. Una vez que está escrito, solo lo usa.

Y sí, puede tener múltiples declaraciones de retorno dentro de una función; siempre hay un lugar en una función adecuada desde el que regresa (el reverso de la función, básicamente). Eso no es lo mismo que saltar de una función con un goto antes de que tenga la oportunidad de regresar adecuadamente.

ingrese la descripción de la imagen aquí

Entonces no se trata realmente de usar goto's. Se trata de evitar su abuso. Todos están de acuerdo en que puedes hacer un programa terriblemente complicado usando gotos, pero también puedes hacer lo mismo abusando de las funciones (es mucho más fácil abusar de los gotos).

Por lo que vale, desde que me gradué de los programas BASIC de estilo de número de línea a la programación estructurada usando Pascal y lenguajes de llaves, nunca he tenido que usar un goto. La única vez que he tenido la tentación de usar uno es hacer una salida temprana de un bucle anidado (en idiomas que no admiten la salida temprana de varios niveles de los bucles), pero generalmente puedo encontrar otra forma más limpia.

Robert Harvey
fuente
2
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
maple_shaft
11

¿A qué tipo de errores conducen las declaraciones "goto"? ¿Hay algún ejemplo históricamente significativo?

Solía ​​usar gotodeclaraciones cuando escribía programas BASIC cuando era niño como una forma simple de obtener bucles for y while (Commodore 64 BASIC no tenía bucles while, y era demasiado inmaduro para aprender la sintaxis y el uso adecuados de los bucles for ) Mi código era frecuentemente trivial, pero cualquier error de bucle podría atribuirse inmediatamente a mi uso de goto.

Ahora uso principalmente Python, un lenguaje de programación de alto nivel que ha determinado que no necesita goto.

Cuando Edsger Dijkstra declaró "Goto considerado dañino" en 1968, no dio un puñado de ejemplos en los que los errores relacionados pudieran atribuirse al goto, sino que declaró que gotoera innecesario para los idiomas de nivel superior y que debería evitarse a favor de lo que hoy consideramos el flujo de control normal: bucles y condicionales. Sus palabras:

El uso desenfrenado de go to tiene una consecuencia inmediata de que se vuelve terriblemente difícil encontrar un conjunto significativo de coordenadas en las que describir el progreso del proceso.
[...]
La declaración go to tal como está es demasiado primitiva; es demasiado una invitación para hacer un desastre con el programa de uno.

Probablemente tenía montañas de ejemplos de errores cada vez que depuraba el código goto. Pero su documento era una declaración de posición generalizada respaldada por pruebas que gotoeran innecesarias para los idiomas de nivel superior. El error generalizado es que es posible que no tenga la capacidad de analizar estáticamente el código en cuestión.

El código es mucho más difícil de analizar estáticamente con gotodeclaraciones, especialmente si retrocede en su flujo de control (que solía hacer) o en alguna sección de código no relacionada. El código puede y fue escrito de esta manera. Se realizó para optimizar en gran medida los recursos informáticos muy escasos y, por lo tanto, las arquitecturas de las máquinas.

Un ejemplo apócrifo

Hubo un programa de blackjack que un responsable de mantenimiento consideró que estaba optimizado con elegancia pero que también era " imposible" para él debido a la naturaleza del código. Fue programado en código de máquina que depende en gran medida de los gotos, así que creo que esta historia es bastante relevante. Este es el mejor ejemplo canónico que se me ocurre.

Un contraejemplo

Sin embargo, la fuente C de CPython (la implementación de Python más común y de referencia) usa gotodeclaraciones con gran efecto. Se utilizan para evitar el flujo de control irrelevante dentro de las funciones para llegar al final de las funciones, haciendo que las funciones sean más eficientes sin perder legibilidad. Esto respeta uno de los ideales de la programación estructurada : tener un único punto de salida para las funciones.

Para el registro, considero que este contraejemplo es bastante fácil de analizar estáticamente.

Aaron Hall
fuente
55
La "Historia de Mel" que encontré fue en una arquitectura extraña donde (1) cada instrucción de lenguaje de máquina pasa a un goto después de su operación, y (2) era terriblemente ineficiente colocar una secuencia de instrucciones en ubicaciones de memoria consecutivas. Y el programa fue escrito en ensamblaje optimizado a mano, no en un lenguaje compilado.
@MilesRout Creo que podría ser correcto aquí, pero la página de wikipedia sobre programación estructurada dice: "La desviación más común, que se encuentra en muchos idiomas, es el uso de una declaración de retorno para la salida temprana de una subrutina. Esto da como resultado múltiples puntos de salida , en lugar del único punto de salida requerido por la programación estructurada ". - ¿Estás diciendo que esto es incorrecto? ¿Tienes una fuente para citar?
Aaron Hall
@AaronHall Ese no es el significado original.
Miles Rout
11

Cuando el wigwam era un arte arquitectónico actual, sus constructores sin duda podían darle consejos prácticos sobre la construcción de wigwams, cómo dejar escapar el humo, etc. Afortunadamente, los constructores de hoy probablemente pueden olvidar la mayoría de esos consejos.

Cuando la diligencia era un arte de transporte actual, sus conductores sin duda podrían darle consejos prácticos sobre caballos de diligencias, cómo defenderse de los bandoleros, etc. Afortunadamente, los automovilistas de hoy también pueden olvidar la mayoría de esos consejos.

Cuando las tarjetas perforadas eran arte de programación actual, sus practicantes también podrían brindarle consejos prácticos sobre la organización de las tarjetas, cómo numerar las declaraciones, etc. No estoy seguro de que ese consejo sea muy relevante hoy.

¿Eres lo suficientemente mayor como para saber la frase "numerar declaraciones" ? No necesita saberlo, pero si no lo hace, entonces no está familiarizado con el contexto histórico en el que la advertencia contra gotoera principalmente relevante.

La advertencia en contra gotono es muy relevante hoy. Con entrenamiento básico en bucles while / for y llamadas a funciones, ni siquiera pensará en emitir un gotomuy a menudo. Cuando lo pienses, probablemente tengas una razón, así que adelante.

Pero, ¿ gotono se puede abusar de la declaración?

Respuesta: claro, se puede abusar de él, pero su abuso es un problema bastante pequeño en la ingeniería de software en comparación con errores mucho, mucho más comunes, como el uso de una variable donde servirá una constante, o como la programación de cortar y pegar (de lo contrario conocido como negligencia para refactorizar). Dudo que estés en peligro. A menos que esté usando longjmpo transfiriendo el control a un código lejano, si piensa usar un gotoo simplemente desea probarlo por diversión, continúe. Estarás bien.

Puede notar la falta de historias de terror recientes en las que gotointerpreta al villano. La mayoría o todas estas historias parecen tener 30 o 40 años. Te paras en tierra firme si consideras esas historias como obsoletas.

thb
fuente
1
Veo que he atraído al menos un voto negativo. Bueno, eso es de esperarse. Más votos negativos pueden venir. Sin embargo, no voy a dar la respuesta convencional, cuando décadas de experiencia en programación me han enseñado que, en este caso, la respuesta convencional es incorrecta. Esta es mi opinión, en cualquier caso. He dado mis razones. El lector puede decidir.
THB
1
Tu publicación es buena y mala, no puedo rechazar comentarios.
Pieter B
7

Para agregar una cosa a las otras respuestas excelentes, con las gotodeclaraciones puede ser difícil decir exactamente cómo llegaste a un lugar determinado en el programa. Puede saber que se produjo una excepción en alguna línea específica, pero si hay gotocódigos en el código, no hay forma de saber qué sentencias ejecutadas para que esta excepción provoque un estado sin buscar en todo el programa. No hay pila de llamadas ni flujo visual. Es posible que haya una declaración a 1000 líneas de distancia que lo puso en un mal estado ejecutado gotoa en la línea que generó una excepción.

qfwfq
fuente
1
Visual Basic 6 y VBA tienen un manejo de errores doloroso, pero si lo usa On Error Goto errh, puede usar Resumepara volver a intentarlo, Resume Nextomitir la línea ofensiva y Erlsaber qué línea era (si usa números de línea).
Cees Timmerman
@CeesTimmerman He hecho muy poco con Visual Basic, no lo sabía. Agregaré un comentario al respecto.
qfwfq
2
@CeesTimmerman: la semántica VB6 de "reanudar" es horrible si se produce un error dentro de una subrutina que no tiene su propio bloque de error. Resumevolverá a ejecutar esa función desde el principio y Resume Nextomitirá todo en la función que aún no lo ha hecho.
supercat
2
@supercat Como dije, es doloroso. Si necesita conocer la línea exacta, debe numerarlos a todos, o deshabilitar temporalmente el manejo de errores On Error Goto 0y depurar manualmente. Resumeestá destinado a ser utilizado después de verificar Err.Numbery hacer los ajustes necesarios.
Cees Timmerman
1
Debo señalar que, en un lenguaje moderno, gotosolo funciona con líneas etiquetadas, que parecen haber sido reemplazadas por bucles etiquetados .
Cees Timmerman
7

Goto es más difícil de razonar para los humanos que otras formas de control de flujo.

Programar el código correcto es difícil. Escribir programas correctos es difícil, determinar si los programas son correctos es difícil, probar programas correctos es difícil.

Obtener código para hacer vagamente lo que quieres es fácil en comparación con todo lo demás sobre programación.

Goto resuelve algunos programas con la obtención de código para hacer lo que quieras. No ayuda a facilitar la verificación de la corrección, mientras que sus alternativas a menudo lo hacen.

Hay estilos de programación donde goto es la solución adecuada. Incluso hay estilos de programación donde su gemelo malvado, proveniente de, es la solución adecuada. En ambos casos, se debe tener mucho cuidado para asegurarse de que lo esté utilizando en un patrón comprensible, y mucha comprobación manual de que no está haciendo algo difícil para garantizar su corrección.

Como ejemplo, hay una característica de algunos idiomas llamados corutinas. Las corutinas son hilos sin hilos; estado de ejecución sin un hilo para ejecutarlo. Puede pedirles que se ejecuten, y pueden ejecutar parte de sí mismos y luego suspenderse, devolviendo el control de flujo.

Las corutinas "superficiales" en lenguajes sin soporte de corutina (como C ++ pre-C ++ 20 y C) son posibles utilizando una mezcla de gotos y gestión de estado manual. Las rutinas "profundas" se pueden hacer usando las funciones setjmpy longjmpde C.

Hay casos en los que las corutinas son tan útiles que vale la pena escribirlas manualmente y con cuidado.

En el caso de C ++, se les encuentra lo suficientemente útiles como para ampliar el lenguaje para admitirlos. La gotogestión de estado manual y de estado se oculta detrás de una capa de abstracción de costo cero, lo que permite a los programadores escribirla sin la dificultad de tener que demostrar que su desorden de goto, void**s, construcción manual / destrucción de estado, etc. es correcta.

El goto se oculta detrás de una abstracción de nivel superior, como whileo foro ifo switch. Esas abstracciones de nivel superior son más fáciles de comprobar y verificar.

Si faltaban algunos de ellos en el idioma (como en algunos idiomas modernos faltan corutinas), cojear junto con un patrón que no se ajusta al problema o usar goto se convierten en sus alternativas.

Hacer que una computadora haga vagamente lo que quieres que haga en casos comunes es fácil . Escribir código confiablemente robusto es difícil . Goto ayuda al primero mucho más de lo que ayuda al segundo; por lo tanto, "se considera dañino", ya que es una señal de código superficialmente "funcional" con errores profundos y difíciles de rastrear. Con el esfuerzo suficiente, aún puede hacer que el código con "goto" sea confiablemente robusto, por lo que mantener la regla de absoluto es incorrecto; pero como regla general, es buena.

Yakk
fuente
1
longjmpsolo es legal en C para desenrollar la pila a una setjmpfunción principal. (Como romper varios niveles de anidamiento, pero para llamadas a funciones en lugar de bucles). longjjmpa un contexto de setjmphecho en una función que desde entonces ha regresado no es legal (y sospecho que no funcionará en la práctica en la mayoría de los casos). No estoy familiarizado con las co-rutinas, pero por su descripción de una co-rutina como similar a un hilo de espacio de usuario, creo que necesitaría su propia pila, así como un contexto de registro guardado, y longjmpsolo le da registro -ahorro, no una pila separada.
Peter Cordes
1
Oh, debería haber buscado en Google: fanf.livejournal.com/105413.html describe cómo hacer espacio en la pila para que se ejecute la rutina. (Lo cual, como sospechaba, es necesario además de usar setjmp / longjmp).
Peter Cordes
2
@PeterCordes: No se requieren implementaciones para proporcionar ningún medio por el cual el código pueda crear un par jmp_buff que sea adecuado para su uso como una rutina. Algunas implementaciones especifican un medio a través del cual el código puede crear uno a pesar de que el Estándar no requiere que proporcionen dicha capacidad. No describiría el código que se basa en técnicas como el "estándar C", ya que en muchos casos un jmp_buff será una estructura opaca que no garantiza un comportamiento consistente entre los diferentes compiladores.
supercat
6

Eche un vistazo a este código, de http://www-personal.umich.edu/~axe/research/Software/CC/CC2/TourExec1.1.f.html que en realidad era parte de una gran simulación de Dilema del prisionero. Si ha visto código FORTRAN o BASIC antiguo, se dará cuenta de que no es tan inusual.

C  Not nice rules in second round of tour (cut and pasted 7/15/93)
   FUNCTION K75R(J,M,K,L,R,JA)
C  BY P D HARRINGTON
C  TYPED BY JM 3/20/79
   DIMENSION HIST(4,2),ROW(4),COL(2),ID(2)
   K75R=JA       ! Added 7/32/93 to report own old value
   IF (M .EQ. 2) GOTO 25
   IF (M .GT. 1) GOTO 10
   DO 5 IA = 1,4
     DO 5 IB = 1,2
5  HIST(IA,IB) = 0

   IBURN = 0
   ID(1) = 0
   ID(2) = 0
   IDEF = 0
   ITWIN = 0
   ISTRNG = 0
   ICOOP = 0
   ITRY = 0
   IRDCHK = 0
   IRAND = 0
   IPARTY = 1
   IND = 0
   MY = 0
   INDEF = 5
   IOPP = 0
   PROB = .2
   K75R = 0
   RETURN

10 IF (IRAND .EQ. 1) GOTO 70
   IOPP = IOPP + J
   HIST(IND,J+1) = HIST(IND,J+1) + 1
   IF (M .EQ. 15 .OR. MOD(M,15) .NE. 0 .OR. IRAND .EQ. 2) GOTO 25
   IF (HIST(1,1) / (M - 2) .GE. .8) GOTO 25
   IF (IOPP * 4 .LT. M - 2 .OR. IOPP * 4 .GT. 3 * M - 6) GOTO 25
   DO 12 IA = 1,4
12 ROW(IA) = HIST(IA,1) + HIST(IA,2)

   DO 14 IB = 1,2
     SUM = .0
     DO 13 IA = 1,4
13   SUM = SUM + HIST(IA,IB)
14 COL(IB) = SUM

   SUM = .0
   DO 16 IA = 1,4
     DO 16 IB = 1,2
       EX = ROW(IA) * COL(IB) / (M - 2)
       IF (EX .LE. 1.) GOTO 16
       SUM = SUM + ((HIST(IA,IB) - EX) ** 2) / EX
16 CONTINUE

   IF (SUM .GT. 3) GOTO 25
   IRAND = 1
   K75R = 1
   RETURN

25 IF (ITRY .EQ. 1 .AND. J .EQ. 1) IBURN = 1
   IF (M .LE. 37 .AND. J .EQ. 0) ITWIN = ITWIN + 1
   IF (M .EQ. 38 .AND. J .EQ. 1) ITWIN = ITWIN + 1
   IF (M .GE. 39 .AND. ITWIN .EQ. 37 .AND. J .EQ. 1) ITWIN = 0
   IF (ITWIN .EQ. 37) GOTO 80
   IDEF = IDEF * J + J
   IF (IDEF .GE. 20) GOTO 90
   IPARTY = 3 - IPARTY
   ID(IPARTY) = ID(IPARTY) * J + J
   IF (ID(IPARTY) .GE. INDEF) GOTO 78
   IF (ICOOP .GE. 1) GOTO 80
   IF (M .LT. 37 .OR. IBURN .EQ. 1) GOTO 34
   IF (M .EQ. 37) GOTO 32
   IF (R .GT. PROB) GOTO 34
32 ITRY = 2
   ICOOP = 2
   PROB = PROB + .05
   GOTO 92

34 IF (J .EQ. 0) GOTO 80
   GOTO 90

70 IRDCHK = IRDCHK + J * 4 - 3
   IF (IRDCHK .GE. 11) GOTO 75
   K75R = 1
   RETURN

75 IRAND = 2
   ICOOP = 2
   K75R = 0
   RETURN

78 ID(IPARTY) = 0
   ISTRNG = ISTRNG + 1
   IF (ISTRNG .EQ. 8) INDEF = 3
80 K75R = 0
   ITRY = ITRY - 1
   ICOOP = ICOOP - 1
   GOTO 95

90 ID(IPARTY) = ID(IPARTY) + 1
92 K75R = 1
95 IND = 2 * MY + J + 1
   MY = K75R
   RETURN
   END

Hay muchos problemas aquí que van mucho más allá de la declaración GOTO aquí; Sinceramente, creo que la declaración GOTO fue un chivo expiatorio. Pero el flujo de control no está absolutamente claro aquí, y el código se mezcla de manera que no queda muy claro lo que está sucediendo. Incluso sin agregar comentarios o usar mejores nombres de variables, cambiar esto a una estructura de bloque sin GOTO facilitaría mucho la lectura y el seguimiento.

prosfilaes
fuente
44
tan cierto :-) Vale la pena mencionar: FORTAN y BASIC heredados no tenían estructuras de bloque, por lo que si una cláusula THEN no podía mantenerse en una línea, GOTO era la única alternativa.
Christophe
Las etiquetas de texto en lugar de números, o al menos los comentarios en las líneas numeradas, ayudarían mucho.
Cees Timmerman
En mis días de FORTRAN-IV: restringí mi uso de GOTO para implementar bloques IF / ELSE IF / ELSE, bucles WHILE y BREAK y NEXT en bucles. Alguien me mostró los males del código de espagueti y por qué debería estructurarlo para evitarlo, incluso si tiene que implementar sus estructuras de bloque con GOTO. Más tarde comencé a usar un preprocesador (RATFOR, IIRC). Goto no es intrínsecamente malo, es solo una construcción de bajo nivel demasiado poderosa que se asigna a una instrucción de rama de ensamblador. Si tiene que usarlo porque su idioma es deficiente, úselo para construir o aumentar las estructuras de bloques adecuadas.
nigel222
@CeesTimmerman: Ese es el código FORTRAN IV de la variedad de jardín. Las etiquetas de texto no eran compatibles con FORTRAN IV. No sé si las versiones más recientes del lenguaje los admiten. Los comentarios en la misma línea que el código no se admitían en FORTRAN IV estándar, aunque puede haber extensiones específicas del proveedor para admitirlos. No sé qué dice el estándar FORTRAN "actual": abandoné FORTRAN hace mucho tiempo y no lo extraño. (El código FORTRAN más reciente que he visto tiene un gran parecido con PASCAL de principios de la década de 1970).
John R. Strohm
1
@CeesTimmerman: extensión específica del proveedor. El código en general es REALMENTE tímido en los comentarios. Además, hay una convención que se hizo común en algunos lugares, de poner números de línea solo en las declaraciones CONTINUAR y FORMAR, para que sea más fácil agregar líneas más adelante. Este código no sigue esa convención.
John R. Strohm
0

Uno de los principios de la programación mantenible es la encapsulación . El punto es que usted interactúa con un módulo / rutina / subrutina / componente / objeto utilizando una interfaz definida, y solo esa interfaz, y los resultados serán predecibles (suponiendo que la unidad se haya probado efectivamente).

Dentro de una sola unidad de código, se aplica el mismo principio. Si aplica constantemente la programación estructurada o los principios de programación orientada a objetos, no:

  1. crear rutas inesperadas a través del código
  2. llegar a secciones de código con valores variables indefinidos o no permitidos
  3. salir de una ruta del código con valores variables indefinidos o no permitidos
  4. no completar una unidad de transacción
  5. deja porciones de código o datos en la memoria que están efectivamente muertos, pero que no son elegibles para la limpieza y reasignación de basura, porque no ha indicado su lanzamiento utilizando una construcción explícita que invoca liberarlos cuando la ruta de control de código sale de la unidad

Algunos de los síntomas más comunes de estos errores de procesamiento incluyen pérdidas de memoria, acumulación de memoria, desbordamientos de puntero, bloqueos, registros de datos incompletos, excepciones de agregar / cambiar / eliminar registros, fallas de página de memoria, etc.

Las manifestaciones observables por el usuario de estos problemas incluyen bloqueo de la interfaz de usuario, disminución gradual del rendimiento, registros de datos incompletos, incapacidad para iniciar o completar transacciones, datos corruptos, cortes de red, cortes de energía, fallas de sistemas integrados (por la pérdida de control de misiles a la pérdida de la capacidad de seguimiento y control en los sistemas de control de tráfico aéreo), fallas en el temporizador, y así sucesivamente. El catálogo es muy extenso.

jaxter
fuente
3
Respondiste sin mencionar gotoni una sola vez. :)
Robert Harvey
0

goto es peligroso porque generalmente se usa donde no es necesario. Usar cualquier cosa que no sea necesaria es peligroso, pero especialmente ir . Si googleas, encontrarás muchos errores causados ​​por goto, esto no es una razón per se para no usarlo (los errores siempre ocurren cuando usas funciones de lenguaje porque eso es intrínseco en la programación), pero algunos de ellos están claramente muy relacionados para ir al uso.


Razones para usar / no usar goto :

  • Si necesita un bucle, debe usar whileo for.

  • Si necesita hacer un salto condicional, use if/then/else

  • Si necesita procedimiento, llame a una función / método.

  • Si necesita salir de una función, solo return.

Puedo contar con mis dedos lugares donde he visto gotousar y usar correctamente.

  • CPython
  • libKTX
  • probablemente unos pocos más

En libKTX hay una función que tiene el siguiente código

if(something)
    goto cleanup;

if(bla)
    goto cleanup;

cleanup:
    delete [] array;

Ahora en este lugar gotoes útil porque el lenguaje es C:

  • estamos dentro de una función
  • no podemos escribir una función de limpieza (porque ingresamos a otro ámbito y hacer que el estado de la función de llamada accesible sea más pesado)

Este caso de uso es útil porque en C no tenemos clases, por lo que la forma más sencilla de limpiar es usar a goto.

Si teníamos el mismo código en C ++ ya no hay necesidad de ir a Goto:

class MyClass{
    public:

    void Function(){
        if(something)
            return Cleanup(); // invalid syntax in C#, but valid in C++
        if(bla)
            return Cleanup(); // invalid syntax in C#, but valid in C++
    }

    // access same members, no need to pass state (compiler do it for us).
    void Cleanup(){

    }



}

¿A qué tipo de errores puede conducir? Cualquier cosa. Bucles infinitos, orden de ejecución incorrecto, tornillo de pila.

Un caso documentado es una vulnerabilidad en SSL que permitió ataques Man in the middle causados ​​por el uso incorrecto de goto: aquí está el artículo

Eso es un error tipográfico, pero pasó desapercibido por un tiempo, si el código se estructuraba de otra manera, se probaba correctamente, ese error no podría haber sido posible.

Desarrollador de juegos
fuente
2
Hay muchos comentarios en una respuesta que explican muy claramente que el uso de "goto" era totalmente irrelevante para el error SSL: el error fue causado por la duplicación descuidada de una línea de código, por lo que una vez se ejecutó condicionalmente y una vez incondicionalmente.
gnasher729
Sí, estoy esperando un mejor ejemplo histórico de un error de goto antes de aceptar. Como dijo; Realmente no importaría lo que había en esa línea de código; La falta de llaves se habría ejecutado y habría causado un error independientemente. Aunque tengo curiosidad; ¿Qué es un "tornillo de pila"?
Akiva
1
Un ejemplo del código en el que trabajé anteriormente en PL / 1 CICS. El programa muestra una pantalla que consiste en mapas de una sola línea. Cuando se devolvió la pantalla, encontró el conjunto de mapas que había enviado. Usé los elementos de eso para encontrar el índice en una matriz, y luego usé ese índice en un índice de variables de matriz para hacer un GOTO para poder procesar ese mapa individual. Si el conjunto de mapas enviados era incorrecto (o se había dañado), podría intentar hacer un GOTO con la etiqueta incorrecta, o peor, bloquearse porque accedió a un elemento después del final del conjunto de variables de etiqueta. Reescrito para usar la sintaxis de tipo de caso.
Kickstart