Я здивований, що ніхто не пропонував цю альтернативу, тому, хоча питання було довгий час, я додаю його: один хороший спосіб вирішення цієї проблеми - використання змінних для відстеження поточного стану. Це техніка, яка може бути використана незалежно від того, використовується чи не 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);
}
Однією з переваг подібного кодування є те, що узгодженість означає, що будь-яке місце, де оригінальний програміст забув перевірити повернене значення, стирчить як хворий великий палець, що значно полегшує пошук (цього класу) помилок.
Отже - це (поки) ще один стиль, який можна використовувати для вирішення цієї проблеми. При правильному використанні він дає дуже чистий, послідовний код - і, як і будь-яка інша техніка, в чужих руках він може в кінцевому підсумку створити довготривалий і заплутаний код :-)