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
Uzasadnieniem użycia gotos jest:
- bezwarunkowe stwierdzenia są łatwiejsze do zrozumienia i przestrzegania
- zagnieżdżanie jest zmniejszone
- błędy wynikające z braku aktualizacji poszczególnych punktów wyjścia podczas wprowadzania modyfikacji
- zapisuje pracę kompilatora w celu optymalizacji nadmiarowego kodu;)
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:
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
free(var1);
free(var2);
...
free(varN);
return 1;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
free(var1);
free(var2);
...
free(varN);
return 1;
} else {
free(var1);
free(var2);
...
free(varN);
return 0;
}
}
token = strtok(NULL,delim);
}
free(var1);
free(var2);
...
free(varN);
return 1;
Teraz dla mnie poniższy kod jest o wiele ładniejszy i łatwiejszy w utrzymaniu, jeśli musisz dodać varNplus1
:
int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
retval = 1;
goto out_free;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
retval = 1;
goto out_free;
} else {
retval = 0;
goto out_free;
}
}
token = strtok(NULL,delim);
}
out_free:
free(var1);
free(var2);
...
free(varN);
return retval;
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
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 == NULL ){
retval = 0;
goto out;
}
if( isValid(var1) ){
retval = some_function(var1);
goto out_free;
}
if( isGood(var1) ){
retval = 0;
goto out_free;
}
out_free:
free(var1);
out:
return retval;
}
Jeśli weźmiemy pod uwagę ekwiwalent bez goto
s:
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 != NULL ){
if( isValid(var1) ){
retval = some_function(var1);
} else {
if( isGood(var1) ){
retval = 0;
}
}
free(var1);
} else {
retval = 0;
}
return retval;
}
Dla mnie, w pierwszym przypadku, jest dla mnie oczywiste, że jeśli pierwsza funkcja powróci NULL
, wyjdziemy stąd i wrócimy 0
. 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świadczenia free()
stwierdzenia pośrodku funkcji źle pachną i wskazują mi, że powinienem utworzyć podprogram. W tym przypadku utworzyłem var1
w swojej funkcji i nie mogłem tego free()
zrobić w podprogramie, ale dlatego goto 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
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
if( functionC(var1, var2){
count++
continue;
}
...
a bunch of statements
...
count++;
free(var1);
free(var2);
}
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
i var2
. Musiałem dodać a var3
, 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:
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
var3 = newFunction(line,count);
if( functionC(var1, var2){
goto next;
}
...
a bunch of statements
...
next:
count++;
free(var1);
free(var2);
}
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;
i next:
etykieta są dla mnie niezadowalające. Są po prostu lepsze niż odzwierciedlenie wypowiedzi free()
i count++
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
#include <stddef.h>
#include <string.h>
char *
strtok(s, delim)
register char *s;
register const char *delim;
{
register char *spanp;
register int c, sc;
char *tok;
static char *last;
if (s == NULL && (s = last) == NULL)
return (NULL);
/*
* Skip (span) leading delimiters (s += strspn(s, delim), sort of).
*/
cont:
c = *s++;
for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
if (c == sc)
goto cont;
}
if (c == 0) { /* no non-delimiter characters */
last = NULL;
return (NULL);
}
tok = s - 1;
/*
* Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
* Note that delim must have one NUL; we stop if we see that, too.
*/
for (;;) {
c = *s++;
spanp = (char *)delim;
do {
if ((sc = *spanp++) == c) {
if (c == 0)
s = NULL;
else
s[-1] = 0;
last = s;
return (tok);
}
} while (sc != 0);
}
/* NOTREACHED */
}
Popraw mnie, jeśli się mylę, ale uważam, że cont:
etykieta i goto 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 kodem
while( isDelim(*s++,delim));
pomijać 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.
- Jeśli w pętli while masz ciąg dalszy, wówczas treść pętli while powinna być funkcją, a kontynuacja powinna być instrukcją return.
- Jeśli w pętli while masz instrukcję break, to sama pętla while powinna być funkcją, a break powinien stać się instrukcją return.
- Jeśli masz oba te elementy, coś może być nie tak.
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.