Estou surpreso que ninguém tenha sugerido essa alternativa, então, embora a questão já esteja por aí, vou acrescentá-la: uma boa maneira de resolver esse problema é usar variáveis para rastrear o estado atual. Essa é uma técnica que pode ser usada, quer seja usada ou não goto
para chegar ao código de limpeza. Como qualquer técnica de codificação, ela tem prós e contras e não é adequada para todas as situações, mas se você estiver escolhendo um estilo, vale a pena considerar - especialmente se você quiser evitar goto
sem terminar com if
s profundamente aninhados .
A ideia básica é que, para cada ação de limpeza que precise ser realizada, existe uma variável de cujo valor podemos dizer se a limpeza precisa ser executada ou não.
Vou mostrar a goto
versão primeiro, porque está mais próxima do código da pergunta 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;
}
Uma vantagem disso sobre algumas das outras técnicas é que, se a ordem das funções de inicialização for alterada, a limpeza correta ainda acontecerá - por exemplo, usando o switch
método descrito em outra resposta, se a ordem de inicialização mudar, então o switch
deve ser editado com muito cuidado para evitar tentar limpar algo que não foi inicializado em primeiro lugar.
Agora, alguns podem argumentar que esse método adiciona muitas variáveis extras - e de fato, neste caso isso é verdade - mas na prática, muitas vezes uma variável existente já rastreia, ou pode ser feita para rastrear, o estado necessário. Por exemplo, se prepare_stuff()
for realmente uma chamada para malloc()
, ou para open()
, então a variável que contém o ponteiro retornado ou o descritor de arquivo pode ser usada - por exemplo:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Agora, se rastrearmos adicionalmente o status de erro com uma variável, podemos evitar goto
totalmente, e ainda limpar corretamente, sem ter um recuo que fica cada vez mais profundo quanto mais inicialização precisamos:
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;
}
Mais uma vez, existem potenciais críticas a isto:
- Todos aqueles "se" não prejudicam o desempenho? Não - porque no caso de sucesso, você tem que fazer todas as verificações de qualquer maneira (caso contrário, você não verificará todos os casos de erro); e, no caso de falha, a maioria dos compiladores otimizará a sequência de
if (oksofar)
verificações com falha para um único salto para o código de limpeza (o GCC certamente o faz) - e em qualquer caso, o caso de erro geralmente é menos crítico para o desempenho.
Isso não é adicionar mais uma variável? Nesse caso, sim, mas muitas vezes a return_value
variável pode ser usada para desempenhar o papel que oksofar
está desempenhando aqui. Se você estruturar suas funções para retornar erros de maneira consistente, poderá até evitar o segundo if
em 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);
}
Uma das vantagens de codificar assim é que a consistência significa que qualquer lugar onde o programador original se esqueceu de verificar o valor de retorno se destaca como um polegar dolorido, tornando muito mais fácil encontrar (aquela classe de) bugs.
Portanto - este é (ainda) mais um estilo que pode ser usado para resolver este problema. Usado corretamente, ele permite um código muito limpo e consistente - e, como qualquer técnica, nas mãos erradas pode acabar produzindo um código prolixo e confuso :-)