Dziwię się, że nikt nie zasugerował tej alternatywy, więc nawet jeśli to pytanie trwało jakiś czas, dodam je: jednym z dobrych sposobów rozwiązania tego problemu jest użycie zmiennych do śledzenia bieżącego stanu. Jest to technika, której można użyć bez względu na to, czy goto
jest używana do uzyskania kodu porządkującego. Jak każda technika kodowania ma zalety i wady i nie będzie pasować do każdej sytuacji, ale jeśli wybierasz styl, warto go rozważyć - zwłaszcza jeśli chcesz tego uniknąć, goto
nie kończąc na głęboko zagnieżdżonych if
s.
Podstawową ideą jest to, że dla każdego działania porządkującego, które może być konieczne, istnieje zmienna, z której wartości możemy określić, czy czyszczenie wymaga wykonania, czy nie.
goto
Najpierw pokażę wersję, ponieważ jest bliżej kodu z oryginalnego pytania.
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;
}
Zaletą tego w porównaniu z niektórymi innymi technikami jest to, że jeśli zmieni się kolejność funkcji inicjalizacyjnych, poprawne czyszczenie będzie nadal miało miejsce - na przykład przy użyciu switch
metody opisanej w innej odpowiedzi, jeśli kolejność inicjalizacji ulegnie zmianie, to switch
musi być bardzo ostrożnie edytowany, aby uniknąć próby wyczyszczenia czegoś, co w rzeczywistości nie zostało zainicjowane.
Niektórzy mogą twierdzić, że ta metoda dodaje całą masę dodatkowych zmiennych - i rzeczywiście w tym przypadku jest to prawda - ale w praktyce często istniejąca zmienna już śledzi lub może być zmuszona do śledzenia wymaganego stanu. Na przykład, jeśli prepare_stuff()
faktycznie jest wywołaniem do malloc()
lub do open()
, wówczas można użyć zmiennej przechowującej zwrócony wskaźnik lub deskryptor pliku - na przykład:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Teraz, jeśli dodatkowo śledzimy stan błędu za pomocą zmiennej, możemy goto
całkowicie uniknąć i nadal poprawnie wyczyścić, bez wcięć, które stają się coraz głębsze, im więcej potrzebujemy inicjalizacji:
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;
}
Ponownie istnieje potencjalna krytyka tego:
- Czy te wszystkie „jeśli” nie szkodzą wydajności? Nie - ponieważ w przypadku sukcesu i tak musisz wykonać wszystkie sprawdzenia (w przeciwnym razie nie sprawdzisz wszystkich przypadków błędów); aw przypadku niepowodzenia większość kompilatorów zoptymalizuje sekwencję zakończonych niepowodzeniem
if (oksofar)
sprawdzeń do pojedynczego skoku do kodu czyszczącego (z pewnością tak robi GCC) - aw każdym razie przypadek błędu jest zwykle mniej krytyczny dla wydajności.
Czy to nie dodaje kolejnej zmiennej? W tym przypadku tak, ale często return_value
zmienna może być używana do odgrywania roli, która oksofar
tutaj gra. Jeśli skonstruujesz swoje funkcje tak, aby zwracały błędy w spójny sposób, możesz nawet uniknąć drugiego if
w każdym przypadku:
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);
}
Jedną z zalet takiego kodowania jest to, że spójność oznacza, że każde miejsce, w którym pierwotny programista zapomniał sprawdzić zwracaną wartość, wystaje jak bolący kciuk, co znacznie ułatwia znajdowanie (tej jednej klasy) błędów.
A więc - to (jeszcze) jeszcze jeden styl, który można wykorzystać do rozwiązania tego problemu. Użyty poprawnie pozwala na bardzo czysty, spójny kod - i jak każda technika, w niepowołanych rękach może skończyć się tworzeniem kodu, który jest rozwlekły i zagmatwany :-)