Я удивлен, что никто не предложил эту альтернативу, поэтому, хотя вопрос уже давно, я добавлю его: один хороший способ решения этой проблемы - использовать переменные для отслеживания текущего состояния. Это метод, который можно использовать независимо от того, используется goto
для получения кода очистки. Как и любой метод кодирования, у него есть свои плюсы и минусы, и он не подходит для любой ситуации, но если вы выбираете стиль, который стоит рассмотреть, особенно если вы хотите избежать goto
, не заканчивая глубоко вложенными if
s.
Основная идея заключается в том, что для каждого действия по очистке, которое может потребоваться, существует переменная, по значению которой мы можем определить, нужно ли выполнять очистку.
Сначала я покажу версию goto
, потому что она ближе к коду в исходном вопросе.
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;
}
Одним из преимуществ этого по сравнению с некоторыми другими методами является то, что, если порядок функций инициализации изменяется, правильная очистка все равно будет происходить - например, с использованием метода switch
, описанного в другом ответе, если порядок Что касается изменений инициализации, то switch
необходимо очень тщательно отредактировать, чтобы избежать попыток очистить что-то, что изначально не было инициализировано.
Теперь некоторые могут утверждать, что этот метод добавляет множество дополнительных переменных - и действительно, в этом случае это правда - но на практике часто существующая переменная уже отслеживает или может быть создана для отслеживания требуемого состояния. Например, если prepare_stuff()
на самом деле является вызовом malloc()
или open()
, то можно использовать переменную, содержащую возвращенный указатель или дескриптор файла - например:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Теперь, если мы дополнительно отслеживаем состояние ошибки с помощью переменной, мы можем полностью избежать goto
и по-прежнему корректно очистить, не имея отступов, которые становятся все глубже и глубже, чем больше инициализации нам нужно:
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;
}
Снова, есть потенциальная критика этого:
- Разве все эти "если" не повредили производительности? Нет - потому что в случае успеха вы все равно должны выполнить все проверки (иначе вы не проверяете все случаи ошибок); и в случае сбоя большинство компиляторов оптимизируют последовательность неудачных проверок
if (oksofar)
для одного перехода к коду очистки (конечно, GCC делает) - и в любом случае, ошибка обычно менее критична для производительности.
Разве это не добавляет еще одну переменную? В этом случае да, но часто переменную return_value
можно использовать для того, чтобы играть роль, которую играет здесь oksofar
. Если вы структурируете свои функции для последовательного возврата ошибок, вы даже можете избежать второго if
в каждом случае:
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);
}
Одним из преимуществ подобного кодирования является то, что согласованность означает, что любое место, где первоначальный программист забыл проверить возвращаемое значение, торчит, как больной большой палец, что значительно упрощает поиск (этот класс) ошибок .
Итак - это (пока) еще один стиль, который можно использовать для решения этой проблемы. При правильном использовании он позволяет получить очень чистый, непротиворечивый код - и, как и любая техника, в чужих руках может привести к тому, что код будет длинным и запутанным: -)