Odpowiedzi:
Jest kilka powodów, dla których używam wyrażenia „goto”, o których wiem (niektórzy już o tym mówili):
Czyste wyjście z funkcji
Często w funkcji możesz przydzielić zasoby i musisz wyjść w wielu miejscach. Programiści mogą uprościć swój kod, umieszczając kod czyszczenia zasobów na końcu funkcji, a wszystkie „punkty wyjścia” funkcji przejdą na etykietę czyszczenia. W ten sposób nie musisz pisać kodu czyszczenia w każdym „punkcie wyjścia” funkcji.
Wyjście z zagnieżdżonych pętli
Jeśli jesteś w zagnieżdżonej pętli i potrzebujesz wyrwać się ze wszystkich pętli, goto może uczynić to o wiele czystszym i prostszym niż instrukcje break i if-check.
Ulepszenia wydajności niskiego poziomu
Jest to poprawne tylko w kodzie krytycznym, ale instrukcje goto wykonują się bardzo szybko i mogą przyspieszyć działanie funkcji. Jest to jednak obosieczny miecz, ponieważ kompilator zwykle nie może zoptymalizować kodu zawierającego gotos.
Zauważ, że we wszystkich tych przykładach gotos są ograniczone do zakresu pojedynczej funkcji.
goto
go return
jest po prostu głupie. To nie jest „refaktoryzacja” niczego, to po prostu „zmiana nazwy”, tak aby ludzie, którzy dorastali w goto
środowisku z tłumem (tj. Wszyscy z nas), czuli się lepiej, używając tego, co moralnie oznacza goto
. Wolę widzieć pętlę tam, gdzie jej używam i widzieć trochę goto
, co samo w sobie jest tylko narzędziem , niż widzieć, że ktoś przesunął pętlę gdzieś niezwiązany tylko po to, aby uniknąć goto
.
break
, continue
, return
są w zasadzie goto
tylko w ładnym opakowaniu.
do{....}while(0)
ma być lepszym pomysłem niż goto, poza tym, że działa w Javie.
Każdy, kto jest przeciwnikiem goto
, bezpośrednio lub pośrednio, w GoTo Edsger Dijkstra za artykuł szkodliwy, uzasadniający swoje stanowisko. Szkoda, że artykuł Dijkstry nie ma praktycznie nic wspólnego ze sposobem, w jaki w goto
dzisiejszych czasach używane są instrukcje, a zatem to, co mówi ten artykuł, ma niewiele lub nie ma zastosowania do współczesnej sceny programowania. Thegoto
nie- meme graniczy teraz na religię, aż do jego pism dyktowany z wysokości, jego arcykapłanów i unikanie (lub gorzej) postrzeganych heretyków.
Umieśćmy tekst Dijkstry w kontekście, aby rzucić nieco światła na ten temat.
Kiedy Dijkstra napisał swój artykuł, popularnymi językami tamtych czasów były nieustrukturyzowane języki proceduralne, takie jak BASIC, FORTRAN (wcześniejsze dialekty) i różne języki asemblera. Ludzie często używający języków wyższego poziomu przeskakiwali całą bazę kodu w skręconych, wypaczonych wątkach wykonania, co dało początek określeniu „kod spaghetti”. Możesz to zobaczyć, przeskakując do klasycznej gry Trek napisanej przez Mike'a Mayfielda i próbując dowiedzieć się, jak to działa. Poświęć chwilę, aby to przemyśleć.
TO jest „niepohamowane użycie oświadczenia”, przeciwko któremu Dijkstra balansował w swoim artykule w 1968 roku. To środowisko, w którym żył, doprowadziło go do napisania tego artykułu. Możliwość skakania w dowolnym miejscu w kodzie w dowolnym momencie był tym, co krytykował i wymagał zatrzymania. Porównywanie tego do anemicznych mocy języka goto
C lub innych bardziej nowoczesnych języków jest po prostu ryzykowne.
Już słyszę wzniosłe śpiewy kultystów, gdy spotykają się z heretykiem. „Ale” - będą skandować - możesz bardzo utrudniać czytanie kodu goto
w C. ” O tak? Możesz także bardzo utrudniać czytanie kodu goto
. Jak ten:
#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
}
Nie goto
widać, więc musi być łatwy do odczytania, prawda? A może ten:
a[900]; b;c;d=1 ;e=1;f; g;h;O; main(k,
l)char* *l;{g= atoi(* ++l); for(k=
0;k*k< g;b=k ++>>1) ;for(h= 0;h*h<=
g;++h); --h;c=( (h+=g>h *(h+1)) -1)>>1;
while(d <=g){ ++O;for (f=0;f< O&&d<=g
;++f)a[ b<<5|c] =d++,b+= e;for( f=0;f<O
&&d<=g; ++f)a[b <<5|c]= d++,c+= e;e= -e
;}for(c =0;c<h; ++c){ for(b=0 ;b<k;++
b){if(b <k/2)a[ b<<5|c] ^=a[(k -(b+1))
<<5|c]^= a[b<<5 |c]^=a[ (k-(b+1 ))<<5|c]
;printf( a[b<<5|c ]?"%-4d" :" " ,a[b<<5
|c]);} putchar( '\n');}} /*Mike Laman*/
Nie goto
istnieje też. Dlatego musi być czytelny.
Jaki jest sens tych przykładów? To nie funkcje językowe tworzą nieczytelny, niemożliwy do utrzymania kod. To nie jest składnia. Powodują to źli programiści. A źli programiści, jak widać w powyższym punkcie, mogą sprawić, że każda funkcja języka będzie nieczytelna i nie będzie używana. Jak for
tamte pętle. (Możesz je zobaczyć, prawda?)
Szczerze mówiąc, niektóre konstrukcje językowe są łatwiejsze do nadużyć niż inne. Jeśli jednak jesteś programistą C, przyjrzałbym się bliżej około 50% zastosowań na #define
długo przed rozpoczęciem krucjaty przeciwko goto
!
Zatem dla tych, którzy próbowali czytać do tej pory, jest kilka kluczowych punktów do odnotowania.
goto
oświadczeń został napisany dla środowiska programistycznego, w którym goto
było o wiele
bardziej potencjalnie szkodliwe niż w większości współczesnych języków, które nie są asemblerem.goto
powodu jest tak racjonalne, jak powiedzenie „Próbowałem się kiedyś dobrze bawić, ale mi się nie podobało, więc teraz jestem temu przeciwny”.goto
instrukcji w kodzie, których nie można odpowiednio zastąpić innymi konstrukcjami.godo
obrzydliwość, w której zawsze do
używana jest fałszywa pętla, break
zamiast użycia goto
. Często są one gorsze niż rozsądne użycie goto
.goto
rzeczywistości zalety (jakie jest postawione pytanie)
Ślepe przestrzeganie najlepszych praktyk nie jest najlepszą praktyką. Ideą unikania goto
stwierdzeń jako podstawowej formy kontroli przepływu jest unikanie tworzenia nieczytelnego kodu spaghetti. Jeśli są stosowane oszczędnie w odpowiednich miejscach, mogą czasem być najprostszym i najczystszym sposobem wyrażenia pomysłu. Walter Bright, twórca kompilatora Zortech C ++ i języka programowania D, używa ich często, ale rozsądnie. Nawet z goto
oświadczeniami jego kod jest nadal doskonale czytelny.
Konkluzja: Unikanie goto
w celu uniknięcia goto
jest bezcelowe. Tym, czego naprawdę chcesz uniknąć, jest tworzenie nieczytelnego kodu. Jeśli twój- goto
obciążony kod jest czytelny, to nie ma w tym nic złego.
Ponieważ goto
utrudnia rozumowanie na temat przepływu programu 1 (alias. „Kod spaghetti”), goto
jest zwykle używany tylko w celu kompensacji brakujących funkcji: użycie goto
może być w rzeczywistości dopuszczalne, ale tylko wtedy, gdy język nie oferuje bardziej strukturalnego wariantu do uzyskania ten sam cel. Weźmy przykład Wątpliwości:
Reguła z goto, której używamy, jest taka, że goto jest w porządku, aby przeskoczyć do przodu do jednego punktu czyszczenia wyjścia w funkcji.
Jest to prawda - ale tylko wtedy, gdy język nie pozwala na obsługę wyjątków strukturalnych z kodem czyszczenia (takim jak RAII lub finally
), który wykonuje to samo zadanie lepiej (ponieważ jest specjalnie zbudowany do tego), lub gdy jest ku temu dobry powód zastosować ustrukturyzowaną obsługę wyjątków (ale nigdy nie będziesz mieć tej sprawy, chyba że na bardzo niskim poziomie).
W większości innych języków jedynym dopuszczalnym zastosowaniem goto
jest wyjście z zagnieżdżonych pętli. I nawet tam prawie zawsze lepiej jest podnieść zewnętrzną pętlę do własnej metody i zastosować return
zamiast niej.
Poza tym goto
jest znakiem, że w danym fragmencie kodu nie ma wystarczającej ilości myśli.
1 Współczesne języki, które obsługują goto
implementują pewne ograniczenia (np. goto
Nie mogą przeskakiwać do funkcji lub z nich wychodzić), ale problem zasadniczo pozostaje ten sam.
Nawiasem mówiąc, to samo dotyczy oczywiście innych funkcji językowych, w szczególności wyjątków. Zazwyczaj obowiązują ścisłe reguły korzystania z tych funkcji tylko tam, gdzie jest to wskazane, takie jak reguła, aby nie używać wyjątków do kontrolowania nietypowego przebiegu programu.
finally
? Więc stosowanie wyjątków dla rzeczy innych niż obsługa błędów jest dobre, ale używanie goto
jest złe? Myślę, że wyjątki są dość trafnie nazwane.
Jest jedna rzecz, która zawsze jest gorsza niż goto's
; dziwne użycie innych operatorów przepływu programów, aby uniknąć goto:
Przykłady:
// 1
try{
...
throw NoErrorException;
...
} catch (const NoErrorException& noe){
// This is the worst
}
// 2
do {
...break;
...break;
} while (false);
// 3
for(int i = 0;...) {
bool restartOuter = false;
for (int j = 0;...) {
if (...)
restartOuter = true;
if (restartOuter) {
i = -1;
}
}
etc
etc
do{}while(false)
Myślę, że można to uznać za idiomatyczne. Nie możesz się nie zgodzić: D
goto after_do_block;
bez mówienia tego. W przeciwnym razie ... „pętla”, która działa dokładnie raz? Nazwałbym to nadużyciem struktur kontrolnych.
#define
wiele jest, wiele razy znacznie gorszych niż używanie goto
raz na jakiś czas: D
W instrukcji C # switch nie można pozwolić na awarię . Tak goto jest używana do kontroli transferu do określonej etykiety switch-case lub domyślnie etykiecie.
Na przykład:
switch(value)
{
case 0:
Console.Writeln("In case 0");
goto case 1;
case 1:
Console.Writeln("In case 1");
goto case 2;
case 2:
Console.Writeln("In case 2");
goto default;
default:
Console.Writeln("In default");
break;
}
Edycja: Istnieje jeden wyjątek od reguły „bez awarii”. Awaria jest dozwolona, jeśli instrukcja case nie ma kodu.
goto case 5:
kiedy jesteś w przypadku 1). Wygląda na to, że odpowiedź Konrada Rudolfa jest tutaj poprawna: goto
kompensuje brakującą cechę (i jest mniej jasna niż byłaby prawdziwa cecha). Jeśli to, czego tak naprawdę chcemy, to przejście awaryjne, być może najlepszym rozwiązaniem domyślnym byłby brak wyjścia awaryjnego, ale coś w rodzaju continue
wyraźnego żądania.
#ifdef TONGUE_IN_CHEEK
Perl ma taki, goto
który pozwala na implementację ogonów biedaka. :-P
sub factorial {
my ($n, $acc) = (@_, 1);
return $acc if $n < 1;
@_ = ($n - 1, $acc * $n);
goto &factorial;
}
#endif
Ok, tak, że nie ma nic wspólnego z C na goto
. Mówiąc poważniej, zgadzam się z innymi komentarzami dotyczącymi używania goto
do czyszczenia lub wdrażania urządzenia Duffa itp. Chodzi o używanie, a nie nadużywanie.
(Ten sam komentarz może dotyczyć longjmp
wyjątków call/cc
i tym podobnych - mają one uzasadnione zastosowania, ale mogą być łatwo nadużywane. Na przykład, rzucając wyjątek wyłącznie w celu uniknięcia głęboko zagnieżdżonej struktury kontroli, w całkowicie nietypowych okolicznościach .)
Przez lata napisałem ponad kilka wierszy języka asemblera. Ostatecznie każdy język wysokiego poziomu kompiluje się do gotos. Ok, nazywaj je „gałęziami”, „skokami” lub czymkolwiek innym, ale są gotowi. Czy ktoś może napisać asembler bez goto?
Teraz możesz wskazać programistom z Fortran, C lub BASIC, że prowadzenie zamieszek za pomocą gotos to przepis na bolońskie spaghetti. Odpowiedzią nie jest jednak unikanie ich, ale ostrożne ich używanie.
Noża można użyć do przygotowania jedzenia, uwolnienia kogoś lub zabicia kogoś. Czy robimy to bez noży przez strach przed nimi? Podobnie goto: używane niedbale utrudnia, używane ostrożnie pomaga.
Spójrz na Kiedy używać Goto podczas programowania w C :
Chociaż użycie goto jest prawie zawsze złą praktyką programistyczną (na pewno można znaleźć lepszy sposób wykonywania XYZ), są chwile, kiedy tak naprawdę nie jest to zły wybór. Niektórzy mogą nawet argumentować, że gdy jest to przydatne, jest to najlepszy wybór.
Większość tego, co mam do powiedzenia na temat goto, tak naprawdę dotyczy tylko C. Jeśli używasz C ++, nie ma rozsądnego powodu, aby używać goto zamiast wyjątków. Jednak w C nie masz możliwości mechanizmu obsługi wyjątków, więc jeśli chcesz oddzielić obsługę błędów od reszty logiki programu i chcesz uniknąć wielokrotnego przepisywania kodu czyszczenia w całym kodzie, wtedy goto może być dobrym wyborem.
Co mam na myśli? Możesz mieć kod, który wygląda następująco:
int big_function()
{
/* do some work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* clean up*/
return [success];
}
Jest to w porządku, dopóki nie uświadomisz sobie, że musisz zmienić kod czyszczenia. Następnie musisz przejść i wprowadzić 4 zmiany. Teraz możesz zdecydować, że możesz po prostu zamknąć wszystkie porządki w jedną funkcję; to nie jest zły pomysł. Ale to oznacza, że musisz uważać na wskaźniki - jeśli planujesz zwolnić wskaźnik w funkcji czyszczenia, nie ma sposobu, aby ustawić go tak, aby wskazywał na NULL, chyba że podasz wskaźnik do wskaźnika. W wielu przypadkach i tak nie będziesz ponownie używać tego wskaźnika, więc może to nie stanowić poważnego problemu. Z drugiej strony, jeśli dodasz nowy wskaźnik, uchwyt pliku lub inną rzecz wymagającą czyszczenia, musisz ponownie zmienić funkcję czyszczenia; i wtedy musisz zmienić argumenty na tę funkcję.
Przy użyciu goto
będzie
int big_function()
{
int ret_val = [success];
/* do some work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
end:
/* clean up*/
return ret_val;
}
Korzyścią jest to, że Twój kod po zakończeniu ma dostęp do wszystkiego, co będzie potrzebne do przeprowadzenia czyszczenia, i udało ci się znacznie zmniejszyć liczbę punktów zmiany. Kolejną korzyścią jest to, że przeszedłeś z posiadania wielu punktów wyjścia dla swojej funkcji do jednego; nie ma szans, że przypadkowo wrócisz z funkcji bez sprzątania.
Co więcej, ponieważ goto jest używane tylko do przeskakiwania do jednego punktu, nie jest tak, że tworzysz masę kodu spaghetti skaczącego tam iz powrotem, próbując symulować wywołania funkcji. Goto raczej pomaga napisać bardziej uporządkowany kod.
Jednym słowem, goto
należy zawsze używać go oszczędnie i w ostateczności - ale jest na to czas i miejsce. Pytanie nie powinno brzmieć „musisz go użyć”, ale „czy to najlepszy wybór”, aby go użyć.
Jednym z powodów, dla których goto jest zły, oprócz stylu kodowania, jest to, że można go używać do tworzenia nakładających się , ale nie zagnieżdżonych pętli:
loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2
Stworzyłoby to dziwaczną, ale być może legalną strukturę kontroli, w której możliwa jest sekwencja (a, b, c, b, a, b, a, b, ...), co czyni hakerów kompilatora nieszczęśliwymi. Najwyraźniej istnieje wiele sprytnych sztuczek optymalizacyjnych, które polegają na tym, że taka struktura nie występuje. (Powinienem sprawdzić moją kopię książki o smokach ...) Może to (przy użyciu niektórych kompilatorów) spowodować, że nie zostaną wykonane inne optymalizacje dla kodu zawierającego goto
s.
Może to być przydatne, jeśli wiesz, że „och, nawiasem mówiąc”, zdarza się, aby przekonać kompilator do emitowania szybszego kodu. Osobiście wolałbym spróbować wyjaśnić kompilatorowi, co jest prawdopodobne, a co nie, zanim użyję sztuczki takiej jak goto, ale prawdopodobnie mógłbym również spróbować goto
przed zhakowaniem asemblera.
goto
jest przydatny, jest to, że pozwala on tworzyć takie pętle, które w przeciwnym razie wymagałyby wielu logicznych zniekształceń. Dalej argumentowałbym, że jeśli optymalizator nie wie, jak to przepisać, to dobrze . Pętli takiej nie należy wykonywać dla wydajności lub czytelności, ale ponieważ jest to dokładnie kolejność, w której rzeczy muszą się dziać. W takim przypadku nie chciałbym, żeby optymalizator się z nim kręcił.
Zabawne jest dla mnie to, że niektórzy posunęli się do podania listy przypadków, w których goto jest dopuszczalne, mówiąc, że wszystkie inne zastosowania są niedopuszczalne. Czy naprawdę uważasz, że znasz każdy przypadek, w którym goto jest najlepszym wyborem do wyrażenia algorytmu?
Aby to zilustrować, dam wam przykład, którego nikt tutaj jeszcze nie pokazał:
Dzisiaj pisałem kod do wstawiania elementu do tablicy mieszającej. Tabela skrótów jest pamięcią podręczną poprzednich obliczeń, które można dowolnie nadpisywać (wpływając na wydajność, ale nie na poprawność).
Każde wiadro tabeli skrótów ma 4 miejsca i mam kilka kryteriów, które decydują, który element zastąpić, gdy wiadro jest pełne. W tej chwili oznacza to wykonanie do trzech przejść przez wiadro, takie jak to:
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
goto add;
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
goto add;
// Additional passes go here...
add:
// element is written to the hash table here
Gdybym nie użył goto, jak wyglądałby ten kod?
Coś takiego:
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
break;
if (add_index >= ELEMENTS_PER_BUCKET) {
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
break;
if (add_index >= ELEMENTS_PER_BUCKET)
// Additional passes go here (nested further)...
}
// element is written to the hash table here
Gdyby dodać więcej przejść, wyglądałoby to coraz gorzej, podczas gdy wersja z goto utrzymuje ten sam poziom wcięcia przez cały czas i unika stosowania fałszywych instrukcji if, których wynik wynika z wykonania poprzedniej pętli.
Więc jest inny przypadek, w którym goto sprawia, że kod jest czystszy i łatwiejszy do napisania i zrozumienia ... Jestem pewien, że jest ich o wiele więcej, więc nie udawaj, że znasz wszystkie przypadki, w których goto jest użyteczny, odrzucając wszelkie dobre, których nie możesz o tym nie myślę.
goto
każda funkcja była na tym samym poziomie abstrakcji. To, czego unika, goto
to bonus.
container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();
mniej więcej tak : Uważam, że tego rodzaju programowanie jest znacznie łatwiejsze do odczytania. Każda funkcja w tym przypadku może być zapisana jako funkcja czysta, o której łatwiej jest myśleć. Główna funkcja wyjaśnia teraz, co robi kod, po nazwie funkcji, a następnie, jeśli chcesz, możesz spojrzeć na ich definicje, aby dowiedzieć się, jak to robi.
Reguła z goto, której używamy, jest taka, że goto jest w porządku, aby przeskoczyć do przodu do jednego punktu czyszczenia wyjścia w funkcji. W naprawdę złożonych funkcjach rozluźniamy tę zasadę, aby umożliwić innym skok do przodu. W obu przypadkach unikamy głęboko zagnieżdżonych instrukcji, które często występują podczas sprawdzania kodu błędu, co pomaga w czytelności i konserwacji.
Najbardziej przemyślaną i dokładną dyskusją na temat instrukcji goto, ich uzasadnionych zastosowań i alternatywnych konstrukcji, które można stosować zamiast „cnotliwych instrukcji goto”, ale które można nadużywać równie łatwo, jak instrukcje goto, jest artykuł Donalda Knutha „ Programowanie strukturalne za pomocą instrukcji goto ” , w grudniowych obliczeniach ankietowych z 1974 r. (tom 6, nr 4, s. 261–301).
Nic dziwnego, że niektóre aspekty tego 39-letniego artykułu są datowane: wzrosty mocy obliczeniowej rzędu rzędów sprawiają, że niektóre ulepszenia wydajności Knutha są niezauważalne w przypadku problemów o średniej wielkości, a od tego czasu opracowano nowe konstrukcje języka programowania. (Na przykład bloki try-catch zajmują Konstrukcję Zahna, chociaż są rzadko używane w ten sposób.) Ale Knuth obejmuje wszystkie strony argumentu i powinien zostać przeczytany, zanim ktokolwiek ponownie podejmie ten problem.
W module Perla od czasu do czasu chcesz tworzyć podprogramy lub zamknięcia w locie. Chodzi o to, że po utworzeniu podprogramu, jak się do niego dostać. Możesz to po prostu nazwać, ale jeśli podprogram caller()
się zastosuje, nie będzie tak pomocny, jak mógłby być. To gdziegoto &subroutine
zmiana może być pomocna.
sub AUTOLOAD{
my($self) = @_;
my $name = $AUTOLOAD;
$name =~ s/.*:://;
*{$name} = my($sub) = sub{
# the body of the closure
}
goto $sub;
# nothing after the goto will ever be executed.
}
Możesz także użyć tej formy, goto
aby zapewnić podstawową formę optymalizacji przywoławczej.
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
$tally *= $n--;
@_ = ($n,$tally);
goto &factorial;
}
(W wersji 16 Perla 5 lepiej byłoby napisać jakogoto __SUB__;
)
Istnieje moduł, który zaimportuje tail
modyfikator i taki, który zaimportuje, recur
jeśli nie lubisz używać tej formy goto
.
use Sub::Call::Tail;
sub AUTOLOAD {
...
tail &$sub( @_ );
}
use Sub::Call::Recur;
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
recur( $n-1, $tally * $n );
}
goto
lepiej jest wykonać przy użyciu innych słów kluczowych.Jak redo
na przykład trochę kodu:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
Lub przechodząc do last
odrobiny kodu z wielu miejsc:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}
Jeśli tak, to dlaczego?
C nie ma podziału wielopoziomowego / oznaczonego i nie wszystkie przepływy sterowania można łatwo modelować za pomocą iteracji i operacji podstawowych C. muszę przejść długą drogę, by naprawić te wady.
Czasami łatwiej jest użyć pewnego rodzaju zmiennej flagowej, aby wywołać rodzaj pseudo-wielopoziomowej przerwy, ale nie zawsze jest ona lepsza od goto (przynajmniej goto pozwala łatwo określić, dokąd idzie kontrola, w przeciwieństwie do zmiennej flagowej ), a czasem po prostu nie chcesz płacić ceny wykonania flag / innych wygięć, aby uniknąć goto.
libavcodec to wrażliwy na wydajność fragment kodu. Bezpośrednie wyrażenie przepływu sterowania jest prawdopodobnie priorytetem, ponieważ będzie działało lepiej.
Uważam, że do {} podczas (fałszywego) użytkowania jest całkowicie obrzydliwe. Można sobie wyobrazić, że może mnie to przekonać w niektórych dziwnych przypadkach, ale nigdy nie jest to czysty i rozsądny kod.
Jeśli musisz wykonać taką pętlę, dlaczego nie wyjaśnić zależności od zmiennej flagi?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
/*empty*/
być stepfailed = 1
? W każdym razie, jak to jest lepsze niż do{}while(0)
? W obu przypadkach musisz break
wyjść z tego (lub w swoim stepfailed = 1; continue;
). Nie wydaje mi się to konieczne.
1) Najczęstszym zastosowaniem goto, o którym wiem, jest emulacja obsługi wyjątków w językach, które go nie oferują, a mianowicie w C. (Kod podany powyżej przez Nuclear jest właśnie taki.) Spójrz na kod źródłowy Linuksa, a ty ' Zobaczę, jak w ten sposób wykorzystano gotill bazillion; według szybkiej ankiety przeprowadzonej w 2013 roku: około 100 000 gotów w kodzie systemu Linux: http://blog.regehr.org/archives/894 . Korzystanie z Goto jest nawet wspomniane w przewodniku po stylach kodowania Linuksa: https://www.kernel.org/doc/Documentation/CodingStyle . Podobnie jak emulowanie programowania obiektowego za pomocą struktur wypełnionych wskaźnikami funkcyjnymi, goto ma swoje miejsce w programowaniu C. Więc kto ma rację: Dijkstra lub Linus (i wszystkie kodery jądra Linuksa)? Zasadniczo jest to teoria a praktyka.
Istnieje jednak zwykła gotcha na brak obsługi na poziomie kompilatora i sprawdzanie typowych konstrukcji / wzorców: łatwiej jest ich używać nieprawidłowo i wprowadzać błędy bez sprawdzania czasu kompilacji. Windows i Visual C ++, ale w trybie C oferują obsługę wyjątków przez SEH / VEH z tego właśnie powodu: wyjątki są przydatne nawet poza językami OOP, tj. W języku proceduralnym. Ale kompilator nie zawsze może zapisać twój bekon, nawet jeśli oferuje wsparcie składniowe dla wyjątków w języku. Rozważmy jako przykład tego drugiego przypadku słynny błąd „goto fail” Apple SSL, który właśnie zduplikował jedno goto z katastrofalnymi konsekwencjami ( https://www.imperialviolet.org/2014/02/22/applebug.html ):
if (something())
goto fail;
goto fail; // copypasta bug
printf("Never reached\n");
fail:
// control jumps here
Możesz mieć dokładnie ten sam błąd, korzystając z wyjątków obsługiwanych przez kompilator, np. W C ++:
struct Fail {};
try {
if (something())
throw Fail();
throw Fail(); // copypasta bug
printf("Never reached\n");
}
catch (Fail&) {
// control jumps here
}
Oba warianty błędu można jednak uniknąć, jeśli kompilator przeanalizuje i ostrzeże o nieosiągalnym kodzie. Na przykład kompilacja z Visual C ++ na poziomie ostrzegawczym / W4 znajdzie błąd w obu przypadkach. Na przykład Java zabrania dostępu do kodu nieosiągalnego (tam, gdzie można go znaleźć!) Z całkiem dobrego powodu: prawdopodobnie jest to błąd w kodzie przeciętnego Joe. Dopóki konstrukcja goto nie zezwala na cele, których kompilator nie jest w stanie łatwo zrozumieć, takie jak gotos do obliczonych adresów (**), kompilatorowi nie jest trudniej znaleźć nieosiągalny kod wewnątrz funkcji z gotos niż użycie Dijkstry -akceptowany kod.
(**) Przypis: Gotos do obliczonych numerów linii jest możliwy w niektórych wersjach Basic, np. GOTO 10 * x, gdzie x jest zmienną. Raczej myląco, w Fortranie „obliczone goto” odnosi się do konstrukcji, która jest równoważna instrukcji switch w C. Standard C nie zezwala na obliczone goto w języku, ale tylko gotos na statycznie / składniowo zadeklarowane etykiety. GNU C ma jednak rozszerzenie umożliwiające uzyskanie adresu etykiety (operator unary, operator prefiksu &&), a także pozwala na przejście do zmiennej typu void *. Zobacz https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html, aby uzyskać więcej informacji na temat tego niejasnego podtematu. Reszta tego postu nie dotyczy tej niejasnej funkcji GNU C.
Gotowe standardowe C (tj. Nie obliczone) zwykle nie są przyczyną, dla której nie można znaleźć nieosiągalnego kodu w czasie kompilacji. Typowym powodem jest następujący kod logiczny. Dany
int computation1() {
return 1;
}
int computation2() {
return computation1();
}
Kompilatorowi równie trudno jest znaleźć nieosiągalny kod w jednej z następujących 3 konstrukcji:
void tough1() {
if (computation1() != computation2())
printf("Unreachable\n");
}
void tough2() {
if (computation1() == computation2())
goto out;
printf("Unreachable\n");
out:;
}
struct Out{};
void tough3() {
try {
if (computation1() == computation2())
throw Out();
printf("Unreachable\n");
}
catch (Out&) {
}
}
(Przepraszam za mój styl kodowania związany z nawiasami klamrowymi, ale starałem się zachować jak najmniejsze przykłady).
Visual C ++ / W4 (nawet z / Ox) nie może znaleźć nieosiągalnego kodu w żadnym z nich, a jak zapewne wiesz, problem znalezienia nieosiągalnego kodu jest ogólnie nierozstrzygalny. (Jeśli mi w to nie wierzysz: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf )
Jako pokrewny problem można użyć C goto do emulacji wyjątków tylko w treści funkcji. Standardowa biblioteka C oferuje parę funkcji setjmp () i longjmp () do emulacji nielokalnych wyjść / wyjątków, ale mają one poważne wady w porównaniu z innymi językami. Artykuł w Wikipedii http://en.wikipedia.org/wiki/Setjmp.h dość dobrze wyjaśnia ten ostatni problem. Ta para funkcji działa również w systemie Windows ( http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx ), ale prawie nikt ich tam nie używa, ponieważ SEH / VEH jest lepszy. Nawet w Uniksie myślę, że setjmp i longjmp są bardzo rzadko używane.
2) Myślę, że drugim najczęstszym zastosowaniem goto w C jest implementacja wielopoziomowej przerwy lub kontynuacji wielopoziomowej, co jest również dość kontrowersyjnym przypadkiem użycia. Przypomnij sobie, że Java nie zezwala na etykietę goto, ale umożliwia przerwanie etykiety lub kontynuowanie etykiety. Według http://www.oracle.com/technetwork/java/simple-142616.html jest to w rzeczywistości najczęstszy przypadek użycia gotos w C (90% mówią), ale z mojego subiektywnego doświadczenia wynika, że kod systemu ma tendencję do częściej używać gotos do obsługi błędów. Być może w kodzie naukowym lub gdy system operacyjny oferuje obsługę wyjątków (Windows), wyjścia wielopoziomowe są dominującym przypadkiem użycia. Tak naprawdę nie podają żadnych szczegółów dotyczących kontekstu ankiety.
Zmodyfikowano w celu dodania: okazuje się, że te dwa wzorce użycia znajdują się w książce C Kernighana i Ritchie, na stronie 60 (w zależności od wydania). Należy również zauważyć, że oba przypadki użycia dotyczą tylko gotowych zmian. Okazuje się, że edycja MISRA C 2012 (w przeciwieństwie do edycji z 2004 r.) Pozwala teraz na gotos, o ile są one tylko nowymi.
Niektórzy twierdzą, że nie ma powodu, aby goto w C ++. Niektórzy twierdzą, że w 99% przypadków istnieją lepsze alternatywy. To nie jest rozumowanie, tylko irracjonalne wrażenia. Oto solidny przykład, w którym goto prowadzi do ładnego kodu, czegoś w rodzaju ulepszonej pętli do-while:
int i;
PROMPT_INSERT_NUMBER:
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
goto PROMPT_INSERT_NUMBER;
}
std::cout << "your number is " << i;
Porównaj to z kodem wolnym od goto:
int i;
bool loop;
do {
loop = false;
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
loop = true;
}
} while(loop);
std::cout << "your number is " << i;
Widzę te różnice:
{}
potrzebny jest zagnieżdżony blok (choć do {...} while
wygląda bardziej znajomo)loop
potrzebna jest dodatkowa zmienna, używana w czterech miejscachloop
loop
nie posiada żadnych danych, po prostu steruje przepływem wykonania, która jest mniej zrozumiały niż prosty etykiecieJest inny przykład
void sort(int* array, int length) {
SORT:
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
goto SORT; // it is very easy to understand this code, right?
}
}
Pozbądźmy się teraz „złego” goto:
void sort(int* array, int length) {
bool seemslegit;
do {
seemslegit = true;
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
seemslegit = false;
}
} while(!seemslegit);
}
Widzisz, że jest to ten sam rodzaj używania goto, ma dobrze ustrukturyzowany wzorzec i nie jest goto do przodu tak często, jak tylko jest to zalecane. Na pewno chcesz uniknąć takiego „inteligentnego” kodu:
void sort(int* array, int length) {
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
i = -1; // it works, but WTF on the first glance
}
}
Chodzi o to, że goto można łatwo nadużywać, ale nie można go winić. Zauważ, że etykieta ma zakres funkcji w C ++, więc nie zanieczyszcza zasięgu globalnego, tak jak w czystym asemblerze, w którym nakładające się pętle mają swoje miejsce i są bardzo powszechne - jak w poniższym kodzie dla 8051, gdzie 7-segmentowy wyświetlacz jest podłączony do P1. Program zapętla segment błyskawicy wokół:
; P1 states loops
; 11111110 <-
; 11111101 |
; 11111011 |
; 11110111 |
; 11101111 |
; 11011111 |
; |_________|
init_roll_state:
MOV P1,#11111110b
ACALL delay
next_roll_state:
MOV A,P1
RL A
MOV P1,A
ACALL delay
JNB P1.5, init_roll_state
SJMP next_roll_state
Jest jeszcze jedna zaleta: goto może służyć jako nazwane pętle, warunki i inne przepływy:
if(valid) {
do { // while(loop)
// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket
} while(loop);
} // if(valid)
Lub możesz użyć równoważnego goto z wcięciem, więc nie potrzebujesz komentarza, jeśli mądrze wybierzesz nazwę etykiety:
if(!valid) goto NOTVALID;
LOOPBACK:
// more than one page of code here
if(loop) goto LOOPBACK;
NOTVALID:;
W Perlu: użycie etykiety do „goto” z pętli - użycie instrukcji „last”, która jest podobna do break.
Pozwala to na lepszą kontrolę nad zagnieżdżonymi pętlami.
Obsługiwana jest również tradycyjna etykieta goto , ale nie jestem pewien, czy istnieje zbyt wiele przypadków, w których jest to jedyny sposób na osiągnięcie tego, czego chcesz - w większości przypadków wystarczą podprogramy i pętle.
goto &subroutine
. Który uruchamia podprogram z bieżącym @_, jednocześnie zastępując bieżący podprogram w stosie.
Problem z „goto” i najważniejszym argumentem ruchu „programowanie bez goto” polega na tym, że jeśli użyjesz go zbyt często, twój kod, chociaż może zachowywać się poprawnie, staje się nieczytelny, niemożliwy do utrzymania, nie można go zobaczyć itp. W 99,99% przypadki „goto” prowadzą do kodu spaghetti. Osobiście nie mogę wymyślić żadnego dobrego powodu, dla którego miałbym używać „goto”.
goto
). Wykorzystanie @ cschol jest podobne: chociaż może nie projektuje teraz języka, zasadniczo ocenia wysiłek projektanta.
goto
z wyjątkiem sytuacji, w których spowodowałoby to istnienie zmiennych, może być tańsze niż próba obsługi każdego rodzaju struktury kontroli, której ktoś może potrzebować. Pisanie kodu goto
może nie być tak przyjemne jak używanie jakiejś innej struktury, ale możliwość pisania takiego kodu goto
pomoże uniknąć „dziur w ekspresji” - konstrukcjach, dla których język nie jest w stanie napisać wydajnego kodu.
goto
na stronie przeglądu kodu, wyeliminowanie goto
znacznie upraszcza logikę kodu.
Z GOTO można oczywiście korzystać, ale jest jedna ważniejsza rzecz niż styl kodu, lub jeśli kod jest czytelny lub nieczytelny, musisz o tym pamiętać: kod w nim może nie być tak solidny, jak ty myśleć .
Na przykład spójrz na następujące dwa fragmenty kodu:
If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)
Odpowiednik kodu z GOTO
If A == 0 Then GOTO FINAL EndIf
A = 0
FINAL:
Write("Value of A:" + A)
Najpierw uważamy, że wynikiem obu bitów kodu będzie „Wartość A: 0” (oczywiście wykonanie bez równoległości)
To nie jest poprawne: w pierwszej próbce A będzie zawsze wynosić 0, ale w drugiej próbce (z instrukcją GOTO) A może nie być 0. Dlaczego?
Powodem jest to, że z innego punktu programu mogę wstawić GOTO FINAL
bez kontrolowania wartości A.
Ten przykład jest bardzo oczywisty, ale w miarę jak programy się komplikują, trudność z dostrzeżeniem tego rodzaju rzeczy wzrasta.
Powiązany materiał można znaleźć w słynnym artykule pana Dijkstry „Sprawa przeciwko oświadczeniu GO TO”
Używam goto w następującym przypadku: w razie potrzeby, aby powrócić z funkcji w różnych miejscach, a przed zwrotem należy wykonać pewne niezainicjowanie:
wersja nie goto:
int doSomething (struct my_complicated_stuff *ctx)
{
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
db_disconnect(conn);
return -1;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
free(temp_data);
db_disconnect(conn);
return -2;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -3;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -4;
}
if (ctx->something_else->additional_check) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -5;
}
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return 0;
}
wersja goto:
int doSomething_goto (struct my_complicated_stuff *ctx)
{
int ret=0;
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
ret=-1;
goto exit_db;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
ret=-2;
goto exit_freetmp;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
ret=-3;
goto exit;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
ret=-4;
goto exit_freekey;
}
if (ctx->something_else->additional_check) {
ret=-5;
goto exit_freekey;
}
exit_freekey:
rsa_free(key);
exit:
pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
free(temp_data);
exit_db:
db_disconnect(conn);
return ret;
}
Druga wersja ułatwia, gdy trzeba coś zmienić w instrukcjach dezalokacji (każda jest używana raz w kodzie), i zmniejsza szansę na pominięcie któregoś z nich podczas dodawania nowej gałęzi. Przeniesienie ich w funkcji nie pomoże tutaj, ponieważ zwolnienie można wykonać na różnych „poziomach”.
finally
bloki w języku C #
finally
). Alternatywnie użyj goto
s, ale do wspólnego punktu wyjścia, który zawsze wykonuje wszystkie czyszczenie. Ale każda metoda czyszczenia może obsłużyć wartość zerową lub już wyczyszczoną lub jest chroniona przez test warunkowy, więc jest pomijana, gdy nie jest odpowiednia.
goto
s, że wszystkie idą do tego samego punktu wyjścia, który ma tę samą logikę (która, jak mówisz, wymaga dodatkowego, jeśli na zasób). Ale nieważne, kiedy używasz C
masz rację - bez względu na powód, dla którego kod znajduje się w C, prawie na pewno kompromis faworyzuje najbardziej „bezpośredni” kod. (Moja sugestia radzi sobie ze złożonymi sytuacjami, w których dowolny zasób mógł zostać przydzielony lub nie. Ale tak, przesadzenie w tym przypadku.)
Edsger Dijkstra, informatyk, który miał duży wkład w tę dziedzinę, słynął również z krytyki użycia GoTo. W Wikipedii znajduje się krótki artykuł na temat jego argumentacji .
Od czasu do czasu przydaje się do przetwarzania ciągów znaków.
Wyobraź sobie coś takiego jak ten przykładowy printf:
for cur_char, next_char in sliding_window(input_string) {
if cur_char == '%' {
if next_char == '%' {
cur_char_index += 1
goto handle_literal
}
# Some additional logic
if chars_should_be_handled_literally() {
goto handle_literal
}
# Handle the format
}
# some other control characters
else {
handle_literal:
# Complicated logic here
# Maybe it's writing to an array for some OpenGL calls later or something,
# all while modifying a bunch of local variables declared outside the loop
}
}
Możesz to zmienić goto handle_literal
do wywołania funkcji, ale jeśli modyfikuje on kilka różnych zmiennych lokalnych, będziesz musiał przekazać odniesienia do każdej z nich, chyba że Twój język obsługuje zmienne zamknięcia. Nadal będziesz musiał użyć continue
instrukcji (która jest prawdopodobnie formą goto) po wywołaniu, aby uzyskać tę samą semantykę, jeśli twoja logika sprawia, że inna sprawa nie działa.
Gotos używałem również ostrożnie w leksykonach, zazwyczaj w podobnych przypadkach. Nie potrzebujesz ich przez większość czasu, ale fajnie jest mieć je w tych dziwnych przypadkach.