Me sorprende que nadie haya sugerido esta alternativa, así que aunque la pregunta ha existido por un tiempo, la agregaré: una buena manera de abordar este problema es usar variables para realizar un seguimiento del estado actual. Esta es una técnica que se puede utilizar tanto si se utiliza como si no goto
para llegar al código de limpieza. Como cualquier técnica de codificación, tiene pros y contras, y no será adecuada para cada situación, pero si elige un estilo, vale la pena considerarlo, especialmente si desea evitarlo goto
sin terminar con if
s profundamente anidados .
La idea básica es que, por cada acción de limpieza que deba realizarse, existe una variable a partir de cuyo valor podemos saber si la limpieza debe realizarse o no.
goto
Primero mostraré la versión, porque está más cerca del código de la pregunta original.
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Una ventaja de esto sobre algunas de las otras técnicas es que, si se cambia el orden de las funciones de inicialización, la limpieza correcta aún ocurrirá, por ejemplo, usando el switch
método descrito en otra respuesta, si el orden de inicialización cambia, entonces el switch
tiene que editarse con mucho cuidado para evitar intentar limpiar algo que no se inicializó realmente en primer lugar.
Ahora, algunos podrían argumentar que este método agrega una gran cantidad de variables adicionales, y de hecho en este caso eso es cierto, pero en la práctica, a menudo, una variable existente ya rastrea, o se puede hacer que rastree, el estado requerido. Por ejemplo, si en prepare_stuff()
realidad es una llamada a malloc()
, o a open()
, entonces se puede usar la variable que contiene el puntero devuelto o el descriptor de archivo, por ejemplo:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Ahora, si adicionalmente hacemos un seguimiento del estado de error con una variable, podemos evitarlo por goto
completo y aún así limpiar correctamente, sin tener una sangría que se vuelve más y más profunda cuanto más inicialización necesitamos:
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Nuevamente, hay posibles críticas a esto:
- ¿No todos esos "si" perjudican el rendimiento? No, porque en el caso de éxito, debe realizar todas las comprobaciones de todos modos (de lo contrario, no estará comprobando todos los casos de error); y en el caso de falla, la mayoría de los compiladores optimizarán la secuencia de
if (oksofar)
comprobaciones fallidas para un solo salto al código de limpieza (GCC ciertamente lo hace) y, en cualquier caso, el caso de error suele ser menos crítico para el rendimiento.
¿No es esto agregar otra variable más? En este caso sí, pero a menudo la return_value
variable se puede utilizar para desempeñar el papel que oksofar
está jugando aquí. Si estructura sus funciones para devolver errores de manera consistente, incluso puede evitar el segundo if
en cada caso:
int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
Una de las ventajas de codificar así es que la consistencia significa que cualquier lugar donde el programador original se haya olvidado de verificar el valor de retorno sobresale como un pulgar dolorido, lo que hace que sea mucho más fácil encontrar (esa clase de) errores.
Entonces, este es (todavía) un estilo más que se puede usar para resolver este problema. Si se usa correctamente, permite un código muy limpio y consistente, y como cualquier técnica, en las manos equivocadas puede terminar produciendo un código largo y confuso :-)