Я удивлен, что никто не предложил эту альтернативу, поэтому, хотя вопрос был довольно давно, я добавлю его: один хороший способ решения этой проблемы - использовать переменные для отслеживания текущего состояния. Это метод, который можно использовать независимо от того 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;
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;
}
return_value = do_the_thing(bar);
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;
if (oksofar) {
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;
}
}
if (oksofar) {
return_value = do_the_thing(bar);
}
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);
}
Одно из преимуществ такого кодирования заключается в том, что согласованность означает, что любое место, где исходный программист забыл проверить возвращаемое значение, торчит, как больной палец, что значительно упрощает поиск (этого одного класса) ошибок.
Итак - это (пока) еще один стиль, который можно использовать для решения этой проблемы. При правильном использовании он позволяет получить очень чистый, последовательный код - и, как и любой другой метод, попадание в чужие руки может привести к созданию многословного и запутанного кода :-)