style - ¿Se considera que GOTO es inofensivo al saltar a la limpieza al final de la función?
loops c language (10)
La declaración de goto
ha sido examinada extensamente en varias discusiones de SO (vea this y that ), y ciertamente no quiero revivir esos acalorados debates.
En su lugar, me gustaría concentrarme en un caso de uso único de goto
s y discutir su valor y posibles alternativas.
Considere el siguiente fragmento de código, que es común en (al menos mi propio) FSM:
while (state = next_state()) {
switch (state) {
case foo:
/* handle foo, and finally: */
if (error) goto cleanup;
break;
case bar:
/* handle bar, and finally: */
if (error) goto cleanup;
break;
/* ...other cases... */
}
}
return ok;
cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;
Cambiar las cosas de limpieza en una función separada solo para guardar los goto
s parece incómodo. Por otro lado, hemos sido educados para condenar el uso de goto
s siempre que sea posible.
Mi pregunta: ¿mi ejemplo de código es considerado un buen estilo?
Si no, ¿hay alternativas viables disponibles?
Por favor, respete el uso específico de goto
descrito anteriormente. No quiero profundizar en otra discusión sobre el uso general de goto
.
Diría que si el código de limpieza no se puede generalizar, es decir, es específico para la función en la que se está utilizando, el goto es una forma agradable y limpia de hacerlo.
El uso de goto
para limpiar el código rompiendo el anidamiento múltiple del bucle es muy conveniente, en lugar de establecer marcas y verificarlo en cada anidación. Por ejemplo, si no puede abrir un archivo y lo descubre en un anidamiento, simplemente puede goto
al segmento de limpieza y cerrar los archivos y los recursos gratuitos. Puedes ver estos ejemplos de goto
en las fuentes de herramientas de coreutilities.
En lugar de extraer la lógica de limpieza en su propia función y llamarla desde diferentes lugares, consideraría extraer la instrucción switch en una función separada y devolverle un código de error. En su bucle while puede verificar el código de retorno y hacer la limpieza y devolverlo si es necesario.
Si tiene varios recursos compartidos entre el interruptor y la lógica de limpieza, entonces creo que el goto sería preferible a pasar todo este estado.
Goto no es necesario cuando tienes switch
. Usar tanto el switch
como el goto
solo agregan complicación.
while (state) {
switch (state) {
case cleanup:
/* do some cleanup, i.e. free() local heap requests, adjust global state, and then: */
return error;
case foo:
/* handle foo, and finally: */
if (error) { state = cleanup; continue; }
break;
case bar:
/* handle bar, and finally: */
if (error) { state = cleanup; continue; }
break;
/* ...other cases... */
}
state = next_state();
}
return ok;
He visto el uso de goto de esta manera en el kernel de OpenBSD, particularmente en los controladores de dispositivos ATA ( un ejemplo ) y personalmente siento que es un buen estilo, ya que ayuda a ilustrar exactamente lo que está sucediendo y cómo el código coincide con el correspondiente FSM. Cuando se intenta verificar la funcionalidad de un FSM contra una especificación, este uso de goto mejora un poco la claridad.
Mirando la respuesta de Ben Voigt me dio una respuesta alternativa:
while (state = next_state()) {
switch (state) {
case foo:
/* handle foo, and finally: */
/* error is set but not bothered with here */
break;
case bar:
/* handle bar, and finally: */
/* error is set but not bothered with here */
break;
/* ...other cases... */
}
if (error) {
/* do some cleanup, i.e. free() local heap requests, */
/* adjust global state, and then: */
return error;
}
}
return ok;
La desventaja de esto es que debe recordar que, después de procesar un estado, podría limpiarse si se produce un error. Parece que la estructura if
podría ser una cadena if-else
para manejar diferentes tipos de errores.
No he tomado una clase formal sobre FSM, pero me parece que el código que publicaste tiene el mismo comportamiento.
Si solo necesita algún código de limpieza para poder ser llamado desde múltiples lugares en su procedimiento y necesita acceder a los recursos locales, tal vez use una declaración lambda. Defínalo antes de la lógica de su conmutador y simplemente llámelo donde necesite limpiar. Me gusta la idea por un par de razones: 1) es más genial que un goto (y esto siempre es importante) 2) Obtienes la encapsulación limpia de la lógica sin tener que crear un método externo y pasar un montón de parámetros ya que la lambda puede acceder Las mismas variables locales dentro del cierre.
Si todo su código de inicio se realiza antes de ingresar al bucle while, entonces sus gotos son inútiles, puede hacer la limpieza al salir del bucle. Si su máquina de estado tiene que ver con traer cosas en el orden correcto, entonces ¿por qué no, pero ya que tiene una máquina de estado, por qué no la usa para hacer la limpieza?
No estoy en contra de goto al inicializar varias cosas juntas y tener un código de manejo de errores simple, como se explica here . Pero si te tomas la molestia de configurar una máquina de estados, entonces no puedo ver una buena razón para usarlos. OMI, la pregunta es todavía demasiado general, un ejemplo más práctico de máquina de estados sería útil.
Su uso de goto
está bien. No rompe las 2 buenas maneras de usar goto.
-
goto
s DEBE bajar (unas pocas líneas) en la fuente - El bloque más interno de
goto labels
DEBE contener las declaracionesgoto
Un principio general que me gusta seguir es que, cuando sea posible, se debe intentar escribir un código cuyo flujo y diseño se ajuste al del dominio del problema ("lo que el programa debe hacer"). Los lenguajes de programación incluyen estructuras de control y otras características que se adaptan bien a la mayoría de los dominios de problemas, pero no a todos. Tales estructuras deben usarse cuando concuerden con los requisitos del programa. En los casos en que los requisitos de un programa no coinciden con las características de un idioma, prefiero centrarme en escribir un código que exprese lo que el programa debe hacer, que contorsionar el código para ajustarlo a los patrones que, mientras cumplen con los requisitos de otras aplicaciones , realmente no cumple con los requisitos para el programa que se está escribiendo.
En algunos casos, una forma muy natural de convertir las máquinas de estado en código, en los casos en que una rutina no podrá tener que "salir" hasta que una máquina de estado haya llegado a algún tipo de conclusión, es tener una etiqueta goto
representada cada estado, y usa instrucciones if
y goto
para las transiciones de estado. Si las transiciones de estado requeridas serían más adecuadas para otras estructuras de control (por ejemplo, while
bucles), el uso de tales bucles sería mejor que las sentencias goto
, y las sentencias de switch
pueden hacer ciertos tipos de "adaptaciones" (por ejemplo, hacer que una rutina realice una transición de estado) cada vez que se ejecuta, en lugar de requerir que se ejecute inmediatamente hasta su finalización) es mucho más fácil. Por otro lado, dado que una declaración de switch
es en realidad solo un "goto" disfrazado, puede ser más simple usar un goto
directamente que usar una instrucción de interruptor para imitar a uno.