Naprawdę waham się zadać to pytanie, ponieważ nie chcę „zabiegać o debatę, argumenty, ankiety lub rozszerzoną dyskusję”, ale jestem nowy w C i chcę uzyskać lepszy wgląd w typowe wzorce stosowane w języku.
Niedawno usłyszałem niechęć do tego goto
polecenia, ale ostatnio również znalazłem dla niego przyzwoity przypadek użycia.
Kod taki jak ten:
error = function_that_could_fail_1();
if (!error) {
error = function_that_could_fail_2();
if (!error) {
error = function_that_could_fail_3();
...to the n-th tab level!
} else {
// deal with error, clean up, and return error code
}
} else {
// deal with error, clean up, and return error code
}
Jeśli część dotycząca oczyszczania jest bardzo podobna, można ją napisać trochę ładniej (moja opinia?) W ten sposób:
error = function_that_could_fail_1();
if(error) {
goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code
Czy jest to powszechny lub dopuszczalny przypadek użycia goto
w C? Czy istnieje inny / lepszy sposób na zrobienie tego?
c
design-patterns
goto
Robz
źródło
źródło
goto hell;
Odpowiedzi:
goto
Oświadczenie (oraz odpowiadające mu etykiety) to kontrola przepływu prymitywny (wraz z warunkowym wykonaniem instrukcji). Rozumiem przez to, że są tam, aby umożliwić ci budowę sieci kontroli przepływu programu. Możesz myśleć o nich jak o modelowaniu strzałek między węzłami schematu blokowego.Niektóre z nich można natychmiast zoptymalizować, gdy występuje bezpośredni przepływ liniowy (wystarczy użyć sekwencji podstawowych instrukcji). Inne wzorce najlepiej zastępować strukturalnymi strukturami programowania, jeśli są one dostępne; jeśli wygląda jak
while
pętla, użyjwhile
pętli , OK? Strukturyzowane wzorce programowania są zdecydowanie potencjalnie jaśniejsze niż zamiargoto
instrukcji.Jednak C nie obejmuje wszystkich możliwych strukturalnych struktur programowania. (Nie jest dla mnie jasne, że wszystkie istotne zostały już odkryte; tempo odkrycia jest teraz wolne, ale wahałbym się powiedzieć, że wszystkie zostały znalezione.) Z tych, o których wiemy, C zdecydowanie nie ma
try
/catch
/finally
structure (i wyjątki też). Brakuje również wielopoziomowejbreak
pętli. Są to rzeczy, któregoto
można zastosować do wdrożenia. Do tego celu można również użyć innych schematów - wiemy, że C ma wystarczający zestawgoto
prymitywy - ale często wiążą się z tworzeniem zmiennych flagowych i znacznie bardziej złożonych warunków pętli lub ochrony; zwiększenie uwikłania analizy kontrolnej w analizę danych sprawia, że program jest trudniejszy do zrozumienia. Utrudnia to również kompilatorowi optymalizację i procesorowi szybkie wykonywanie (większość konstrukcji kontroli przepływu - i zdecydowaniegoto
- jest bardzo tania).Tak więc, chociaż nie powinieneś używać,
goto
chyba że jest to konieczne, powinieneś zdawać sobie sprawę, że istnieje i że może być potrzebny, a jeśli potrzebujesz, nie powinieneś czuć się źle. Przykładem przypadku, w którym jest to potrzebne, jest dezalokacja zasobów, gdy wywoływana funkcja zwraca błąd. (To znaczytry
/finally
.) Można to napisać bezgoto
zrobienia tego, co może mieć swoje wady, takie jak problemy z utrzymaniem go. Przykład sprawy:Kod może być jeszcze krótszy, ale wystarcza, aby to wykazać.
źródło
try/catch/finally
, abygoto
pomimo podobnego jeszcze bardziej wszechobecnej (jak może rozprzestrzeniać całej wielu funkcji / modułów) postaci kodu spaghetti, które jest możliwe przy użyciutry/catch/finally
?Tak.
Jest używany na przykład w jądrze Linuksa. Oto e-mail z końca wątku sprzed prawie dekady , śmiały mój:
To powiedziawszy, wymaga dużo dyscypliny, aby powstrzymać się od tworzenia kodu spaghetti, gdy przyzwyczaisz się do używania goto, więc jeśli nie piszesz czegoś, co wymaga szybkości i niskiego poziomu pamięci (jak jądro lub system osadzony), powinieneś naprawdę pomyśl o tym, zanim napiszesz pierwsze goto.
źródło
Moim zdaniem opublikowany kod jest przykładem prawidłowego użycia
goto
, ponieważ przeskakujesz tylko w dół i używasz go tylko jako prymitywnej procedury obsługi wyjątków.Jednak ze względu na starą debatę goto, programiści unikali
goto
od około 40 lat i dlatego nie są przyzwyczajeni do czytania kodu za pomocą goto. Jest to ważny powód, aby unikać goto: po prostu nie jest to standard.Przepisałbym kod jako coś łatwiejszego do odczytania przez programistów C:
Zalety tego projektu:
źródło
Słynny artykuł opisujący przypadek prawidłowego użycia to Programowanie strukturalne z oświadczeniem GOTO Donalda E. Knutha (Uniwersytet Stanforda). Artykuł ukazał się w czasach, gdy używanie GOTO było przez niektórych uważane za grzech, a ruch programowania strukturalnego był u szczytu. Możesz rzucić okiem na GoTo uważane za szkodliwe.
źródło
W Javie zrobiłbyś to tak:
Często tego używam. Mimo że nie lubię
goto
języka, w większości innych języków w stylu C używam twojego kodu; nie ma innego dobrego sposobu na zrobienie tego. (Wyskakiwanie z zagnieżdżonych pętli jest podobnym przypadkiem; w Javie używam etykietbreak
i wszędzie indziej używam agoto
.)źródło
Myślę, że jest to dobry przypadek użycia, ale w przypadku, gdy „błąd” jest niczym więcej niż wartością logiczną, istnieje inny sposób na osiągnięcie tego, co chcesz:
Wykorzystuje to ocenę zwarciową operatorów logicznych. Jeśli to „lepsze”, zależy od twojego osobistego gustu i tego, jak jesteś przyzwyczajony do tego idiomu.
źródło
error
wartość może stać się bez znaczenia dla wszystkich OR.Przewodnik po stylu linuxa podaje konkretne powody, dla których warto zastosować te
goto
, które są zgodne z twoim przykładem:https://www.kernel.org/doc/Documentation/process/coding-style.rst
Oświadczenie Nie powinienem udostępniać swojej pracy. Przykłady tutaj są nieco wymyślone, więc proszę o wyrozumiałość.
Jest to dobre do zarządzania pamięcią. Ostatnio pracowałem nad kodem, który dynamicznie przydzielał pamięć (na przykład
char *
zwracany przez funkcję). Funkcja, która patrzy na ścieżkę i sprawdza, czy ścieżka jest poprawna, analizując tokeny ścieżki:Teraz dla mnie poniższy kod jest o wiele ładniejszy i łatwiejszy w utrzymaniu, jeśli musisz dodać
varNplus1
:Teraz kod miał wiele innych problemów, mianowicie, że N był gdzieś powyżej 10, a funkcja miała ponad 450 linii, z 10 poziomami zagnieżdżenia w niektórych miejscach.
Ale zaproponowałem mojemu przełożonemu, żeby to zrefaktoryzował, co zrobiłem, a teraz jest to zbiór funkcji, które są krótkie i wszystkie mają styl linux
Jeśli weźmiemy pod uwagę ekwiwalent bez
goto
s:Dla mnie, w pierwszym przypadku, jest dla mnie oczywiste, że jeśli pierwsza funkcja powróci
NULL
, wyjdziemy stąd i wrócimy0
. W drugim przypadku muszę przewinąć w dół, aby zobaczyć, czy if zawiera całą funkcję. Przyznany pierwszy wskazuje mi to stylistycznie (nazwa „out
”), a drugi robi to składniowo. Pierwszy jest jeszcze bardziej oczywisty.Ponadto zdecydowanie wolę mieć
free()
instrukcje na końcu funkcji. Po części dlatego, że z mojego doświadczeniafree()
stwierdzenia pośrodku funkcji źle pachną i wskazują mi, że powinienem utworzyć podprogram. W tym przypadku utworzyłemvar1
w swojej funkcji i nie mogłem tegofree()
zrobić w podprogramie, ale dlategogoto out_free
styl goto out jest tak praktyczny.Myślę, że należy wychować programistów wierzących, że
goto
to zło. Następnie, gdy są wystarczająco dojrzali, powinni przejrzeć kod źródłowy Linuksa i przeczytać przewodnik po stylu Linux.Powinienem dodać, że używam tego stylu bardzo konsekwentnie, każda funkcja ma int
retval
,out_free
etykietę i etykietę wyjściową. Ze względu na spójność stylistyczną poprawiono czytelność.Bonus: Łamie się i trwa
Powiedz, że masz pętlę while
W tym kodzie występują inne błędy, ale jedną z nich jest instrukcja Continue. Chciałbym przepisać całość, ale miałem za zadanie zmodyfikować ją w niewielki sposób. Refaktoryzacja zajęłaby mi kilka dni w sposób, który mnie satysfakcjonuje, ale faktyczna zmiana zajęła około pół dnia pracy. Problem polega na tym, że nawet jeśli
continue
my nadal musimy uwolnićvar1
ivar2
. Musiałem dodać avar3
, a to sprawiło, że chciałem się zmusić, aby dublować instrukcje free ().Byłem wówczas stosunkowo nowym stażystą, ale już dawno szukałem kodu źródłowego linuksa, więc zapytałem mojego przełożonego, czy mógłbym użyć instrukcji goto. Powiedział tak, a ja to zrobiłem:
Myślę, że kontynuacja jest w najlepszym razie OK, ale dla mnie są jak goto z niewidzialną etykietą. To samo dotyczy przerw. Nadal wolałbym kontynuować lub przerwać, chyba że, tak jak w tym przypadku, zmusi cię to do lustrzanych modyfikacji w wielu miejscach.
Powinienem również dodać, że to użycie
goto next;
inext:
etykieta są dla mnie niezadowalające. Są po prostu lepsze niż odzwierciedlenie wypowiedzifree()
icount++
oświadczeń.goto
są prawie zawsze w błędzie, ale trzeba wiedzieć, kiedy są dobre w użyciu.Jedną z rzeczy, o których nie rozmawiałem, jest obsługa błędów, która została omówiona w innych odpowiedziach.
Występ
Można spojrzeć na implementację strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c
Popraw mnie, jeśli się mylę, ale uważam, że
cont:
etykieta igoto cont;
oświadczenie służą zapewnieniu wydajności (z pewnością nie zwiększają czytelności kodu). W ten sposób można je zastąpić czytelnym kodempomijać ograniczniki. Aby jednak działać jak najszybciej i unikać niepotrzebnych wywołań funkcji, robią to w ten sposób.
Przeczytałem artykuł Dijkstry i uważam go za dość ezoteryczny.
google „oświadczenie dijkstra goto uznane za szkodliwe”, ponieważ nie mam wystarczającej reputacji, aby opublikować więcej niż 2 linki.
Widziałem to jako powód, aby nie używać goto, a czytanie tego niczego nie zmieniło, o ile moje zastosowania goto są pomijane.
Dodatek :
Wymyśliłem porządną zasadę, myśląc o tym wszystkim o ciągłości i zerwaniu.
Nie zawsze jest to możliwe z powodu problemów z zakresem, ale odkryłem, że dzięki temu znacznie łatwiej jest rozumować mój kod. Zauważyłem, że za każdym razem, gdy pętla chwilowa miała przerwę lub kontynuację, wywoływało to złe przeczucie.
źródło
Osobiście przerobiłbym to bardziej tak:
Byłoby to bardziej motywowane przez unikanie głębokiego zagnieżdżania niż unikanie goto (IMO gorszy problem z pierwszą próbką kodu) i oczywiście zależałoby od tego, czy CleanupAfterError jest możliwy poza zakresem (w tym przypadku „parametry” mogą być strukturą zawierającą przydzieloną pamięć, którą musisz zwolnić, PLIK *, który musisz zamknąć lub cokolwiek innego.
Jedną z głównych zalet, które widzę w tym podejściu, jest to, że zarówno łatwiejsze, jak i czystsze jest wybranie hipotetycznego przyszłego dodatkowego kroku między, powiedzmy, FTCF2 i FTCF3 (lub usunięcie istniejącego bieżącego kroku), więc lepiej nadaje się do utrzymania (i osoby, która dziedziczy mój kod, który nie chce mnie zlinczować!) - na bok, w wersji zagnieżdżonej brakuje tego.
źródło
Spójrz na wytyczne kodowania C MISRA (Motor Industry Software Reliability Association), które pozwalają na goto pod ścisłymi kryteriami (które spełnia Twój przykład)
Tam, gdzie pracuję, napisany byłby ten sam kod - nie ma potrzeby. - Unikanie niepotrzebnej debaty religijnej na ich temat jest dużym plusem w każdym domu oprogramowania.
lub „goto in drag” - coś jeszcze bardziej podejrzanego niż goto, ale omija „No goto Ever !!!” obóz) „Na pewno musi być OK, nie używa Goto” ....
Jeśli funkcje mają ten sam typ parametru, umieść je w tabeli i użyj pętli -
źródło
if(flag)
w którym flaga ma zasięg, kazanie każdemu egzemplarzowi pobrać gałąź „if”, a każde odpowiadające mu instrukcje w drugiej kopii - jeszcze". Działania, które ustawiają i usuwają flagę są tak naprawdę „gotami”, które przechodzą między tymi dwiema wersjami kodu. Są chwile, kiedy użycie flag jest czystsze niż jakakolwiek alternatywa, ale dodanie flagi w celu uratowania jednegogoto
celu nie jest dobrym kompromisem.Używam również,
goto
jeśli alternatywnedo/while/continue/break
hakowanie byłoby mniej czytelne.goto
mają tę zaletę, że ich cele mają nazwę i czytajągoto something;
. Może to być bardziej czytelne niżbreak
lubcontinue
jeśli faktycznie nie przerywasz lub nie kontynuujesz.źródło
do ... while(0)
lub innego konstruktu, który nie jest rzeczywistą pętlą, ale zharebrained próbą zapobiegania użyciugoto
.źródło
break
działa dokładnie tak samogoto
, choć nie nosi piętna.Zawsze będą obozy, które mówią, że jeden sposób jest akceptowalny, a drugi nie. Firmy, w których pracowałem, zmarszczyły brwi lub zdecydowanie odradzają goto używać. Osobiście nie mogę myśleć o żadnym momencie, w którym go użyłem, ale to nie znaczy, że tak złe , tylko inny sposób robienia rzeczy.
W C zazwyczaj wykonuję następujące czynności:
W celu przetworzenia na przykładzie goto zrobiłbym to:
error = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }
Nie ma zagnieżdżenia, a wewnątrz klauzul if można wykonać dowolne raportowanie błędów, jeśli krok wygenerował błąd. Więc nie musi być „gorszy” niż metoda wykorzystująca gotos.
Muszę jeszcze natknąć się na przypadek, w którym ktoś ma problemy, których nie da się zrobić inną metodą i jest tak samo czytelny / zrozumiały, i to jest klucz, IMHO.
źródło