Czy kiedykolwiek korzystniejsze jest używanie „goto” w języku obsługującym pętle i funkcje? Jeśli tak, to dlaczego?


201

Od dawna mam wrażenie, że gotonigdy nie należy z niego korzystać, jeśli to możliwe. Kiedy pewnego dnia czytałem libavcodec (napisany w C), zauważyłem wiele jego zastosowań. Czy korzystanie z gotopętli i funkcji jest kiedykolwiek korzystne ? Jeśli tak, to dlaczego?

Odpowiedzi:


242

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.


15
Właściwym sposobem na wyjście z zagnieżdżonych pętli jest przeformułowanie wewnętrznej pętli na osobną metodę.
jason

129
@Jason - Bah. To ładunek byka. Zastąpienie gotogo returnjest 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.
Chris Lutz

18
Są sytuacje, w których ważne są gotos: na przykład środowisko C ++ bez wyjątków. W źródle Silverlight mamy dziesiątki tysięcy (lub więcej) instrukcji goto, aby istniała bezpieczna funkcja dzięki zastosowaniu makr - kluczowe kodeki mediów i biblioteki często działają na podstawie zwracanych wartości, a nie wyjątków, i trudno jest połączyć te mechanizmy obsługi błędów w jeden skuteczny sposób.
Jeff Wilcox,

79
Warto zauważyć, że wszystko break, continue, returnsą w zasadzie gototylko w ładnym opakowaniu.
el.pescado

16
Nie rozumiem, jak do{....}while(0)ma być lepszym pomysłem niż goto, poza tym, że działa w Javie.
Jeremy List

906

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 gotodzisiejszych 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 gotoC 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 gotow 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 gotowidać, 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 gotoistnieje 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 fortamte 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 #definedł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.

  1. Artykuł Dijkstry na temat gotooświadczeń został napisany dla środowiska programistycznego, w którym gotobyło o wiele bardziej potencjalnie szkodliwe niż w większości współczesnych języków, które nie są asemblerem.
  2. Automatycznie odrzuca wszystkie zastosowania 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”.
  3. Istnieją uzasadnione zastosowania współczesnego (anemicznego) goto instrukcji w kodzie, których nie można odpowiednio zastąpić innymi konstrukcjami.
  4. Istnieją oczywiście nielegalne zastosowania tych samych oświadczeń.
  5. Są też nielegalne zastosowania współczesnych instrukcji kontrolnych, takich jak godoobrzydliwość, w której zawsze doużywana jest fałszywa pętla, breakzamiast użycia goto. Często są one gorsze niż rozsądne użycie goto.

42
@pocjoc: Na przykład pisanie optymalizacji rekursywnej ogona w C. Pojawia się to przy wdrażaniu funkcjonalnych interpreterów / środowisk uruchomieniowych, ale reprezentuje interesującą kategorię problemów. Większość kompilatorów napisanych w C korzysta z goto z tego powodu. Jego stosowanie niszowych, które nie występują w większości sytuacji, oczywiście, ale w sytuacji, nazywany jest przez to sprawia, że wiele sensu do użytku.
zxq9

66
Dlaczego wszyscy mówią o pracy Dijkstry „Idź do uważanej za szkodliwą”, nie wspominając o odpowiedzi Knutha na temat: „Programowanie strukturalne z poleceniami iść do”.
Oblomov

22
@ Nietzche-jou "to dość rażący słomkowy człowiek [...]" Sarkastycznie używa fałszywej dychotomii, aby pokazać, dlaczego piętno jest nielogiczne. Strawman jest logicznym błędem, w którym celowo wprowadzasz w błąd pozycję przeciwnika, aby sprawić wrażenie, że jego pozycja jest łatwo pokonana. Np. „Ateiści są po prostu ateistami, ponieważ nienawidzą Boga”. Fałszywa dychotomia ma miejsce, gdy zakładasz, że przeciwna skrajność jest prawdziwa, gdy istnieje co najmniej jedna dodatkowa możliwość (tj. Czarno-biała). Np. „Bóg nienawidzi homoseksualistów, dlatego kocha heteros”.
Braden Best

27
@JLRishe Czytasz zbyt głęboko w coś, czego nawet tam nie ma. To prawdziwy logiczny błąd, jeśli osoba, która go użyła, szczerze uważa, że ​​ma to logiczny sens. Ale powiedział to sarkastycznie, wyraźnie wskazując, że wie, że to niedorzeczne i tak to pokazuje. Nie „używa go do uzasadnienia swojej tezy”; używa go, aby pokazać, dlaczego śmieszne jest twierdzenie, że gotos automatycznie zamieniają kod w spaghetti. To znaczy, że nadal możesz pisać gówniany kod bez gotów. O to właśnie chodzi. A sarkastyczna fałszywa dychotomia to jego metoda zilustrowania tego w humorystyczny sposób.
Braden Best

32
-1 za wysunięcie bardzo dużego argumentu, dlaczego uwaga Dijkstry nie ma zastosowania, przy jednoczesnym zapominaniu o pokazaniu, jakie są w gotorzeczywistości zalety (jakie jest postawione pytanie)
Maarten Bodewes

154

Ślepe przestrzeganie najlepszych praktyk nie jest najlepszą praktyką. Ideą unikania gotostwierdzeń 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 gotooświadczeniami jego kod jest nadal doskonale czytelny.

Konkluzja: Unikanie gotow celu uniknięcia gotojest bezcelowe. Tym, czego naprawdę chcesz uniknąć, jest tworzenie nieczytelnego kodu. Jeśli twój- gotoobciążony kod jest czytelny, to nie ma w tym nic złego.


36

Ponieważ gotoutrudnia rozumowanie na temat przepływu programu 1 (alias. „Kod spaghetti”), gotojest zwykle używany tylko w celu kompensacji brakujących funkcji: użycie gotomoż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 gotojest 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ć returnzamiast niej.

Poza tym gotojest znakiem, że w danym fragmencie kodu nie ma wystarczającej ilości myśli.


1 Współczesne języki, które obsługują gotoimplementują pewne ograniczenia (np. gotoNie 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.


4
Ciekawe, ale co z przypadkiem użycia gotos do czyszczenia kodu. Przez oczyszczenie rozumiem nie tylko dezalokację pamięci, ale także, powiedzmy, rejestrowanie błędów. Czytałem kilka postów i najwyraźniej nikt nie pisze kodu, który drukuje dzienniki .. hmm ?!
shiva

37
„Zasadniczo z powodu wadliwego charakteru goto (i uważam, że jest to kontrowersyjne)” -1 i przestałem tam czytać.
o0 ”.

14
Ostrzeżenie przed goto pochodzi z epoki programowania strukturalnego, w której wczesne powroty były również uważane za złe. Przeniesienie zagnieżdżonych pętli do funkcji powoduje zamianę jednego „zła” na drugie i tworzy funkcję, która nie ma żadnego prawdziwego powodu (poza „pozwoliło mi to uniknąć używania goto!”). To prawda, że ​​goto jest najsilniejszym narzędziem do tworzenia kodu spaghetti, jeśli jest używane bez ograniczeń, ale jest to idealna aplikacja do tego; upraszcza kod. Knuth argumentował za takim użyciem, a Dijkstra powiedział: „Nie wpadnij w pułapkę wiary, że jestem strasznie dogmatyczny w kwestii goto”.
Błoto

5
finally? Więc stosowanie wyjątków dla rzeczy innych niż obsługa błędów jest dobre, ale używanie gotojest złe? Myślę, że wyjątki są dość trafnie nazwane.
Christian

3
@Mud: w przypadkach, które spotykam (codziennie), najlepszym rozwiązaniem jest utworzenie funkcji „bez prawdziwego powodu” . Powód: usuwa szczegóły z funkcji najwyższego poziomu, dzięki czemu wynik ma łatwy do odczytania przepływ sterowania. Nie spotykam osobiście sytuacji, w których goto produkuje najbardziej czytelny kod. Z drugiej strony używam wielu zwrotów w prostych funkcjach, gdzie ktoś inny może chcieć dostać jeden punkt wyjścia. Chciałbym zobaczyć kontrprzykłady, w których goto jest bardziej czytelne niż przekształcanie w dobrze nazwane funkcje.
ToolmakerSteve,

35

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

2
do{}while(false)Myślę, że można to uznać za idiomatyczne. Nie możesz się nie zgodzić: D
Thomas Eding

37
@trinithis: Jeśli jest to „idiomatyczne”, dzieje się tak tylko z powodu kultu anty-goto. Jeśli przyjrzysz się temu uważnie, zdasz sobie sprawę, że to tylko sposób na powiedzenie tego 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.
cHao

5
@ThomasEding Eding Jest jeden wyjątek od twojego punktu. Jeśli kiedykolwiek zrobiłeś jakieś programowanie w C / C ++ i musiałeś użyć #define, to wiesz, że {}, podczas gdy (0) jest jednym ze standardów enkapsulacji wielu linii kodu. Na przykład: #define do {memcpy (a, b, 1); coś ++;} while (0) jest bezpieczniejsze i lepsze niż #define memcpy (a, b, 1); coś ++
Ignas2526

10
@ Ignas2526 Po prostu bardzo ładnie pokazałeś, jak #definewiele jest, wiele razy znacznie gorszych niż używanie gotoraz na jakiś czas: D
Luaan

3
+1 za dobrą listę sposobów, w jakie ludzie starają się unikać gotów, do skrajności; Wspomniałem o takich technikach gdzie indziej, ale jest to świetna, zwięzła lista. Zastanawiając się, dlaczego przez ostatnie 15 lat mój zespół nigdy nie potrzebował żadnej z tych technik ani nie używał gotos. Nawet w aplikacji klient / serwer z setkami tysięcy wierszy kodu, przez wielu programistów. C #, java, PHP, python, javascript. Kod klienta, serwera i aplikacji. Nie chwalenie się ani nawracanie na jedno stanowisko, po prostu naprawdę ciekawi, dlaczego niektórzy ludzie spotykają się z sytuacjami, które błagają o gotos jako najbardziej przejrzyste rozwiązanie, a inni nie ...
ToolmakerSteve

28

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.


3
Przełączanie przełączania jest obsługiwane w .NET 2.0 - msdn.microsoft.com/en-us/library/06tc147t(VS.80).aspx
rjzii 16.0908

9
Tylko jeśli sprawa nie ma treści kodu. Jeśli zawiera kod, musisz użyć słowa kluczowego goto.
Matthew Whited,

26
Ta odpowiedź jest bardzo zabawna - C # usunął awarię, ponieważ wielu uważa ją za szkodliwą, a w tym przykładzie zastosowano goto (również postrzegane jako szkodliwe przez wielu) w celu przywrócenia pierwotnego, rzekomo szkodliwego zachowania, ALE ogólny wynik jest w rzeczywistości mniej szkodliwy ( ponieważ kod wyjaśnia, że ​​omijanie jest celowe!).
thomasrutter

9
Tylko dlatego, że słowo kluczowe jest zapisane literami GOTO, nie czyni go goto. Ten przedstawiony przypadek nie jest goto. Jest to konstrukcja instrukcji switch na wypadek upadku. Z drugiej strony tak naprawdę nie znam bardzo dobrze C #, więc mogę się mylić.
Thomas Eding,

1
Cóż, w tym przypadku jest to coś więcej niż wpadka (ponieważ możesz powiedzieć, goto case 5:kiedy jesteś w przypadku 1). Wygląda na to, że odpowiedź Konrada Rudolfa jest tutaj poprawna: gotokompensuje 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 continuewyraźnego żądania.
David Stone

14

#ifdef TONGUE_IN_CHEEK

Perl ma taki, gotoktó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 gotodo czyszczenia lub wdrażania urządzenia Duffa itp. Chodzi o używanie, a nie nadużywanie.

(Ten sam komentarz może dotyczyć longjmpwyjątków call/cci 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 .)


Myślę, że to jedyny powód, aby używać goto w Perlu.
Brad Gilbert,

12

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.


1
Być może chcesz przeczytać, dlaczego uważam, że jest to zasadniczo błędne na stackoverflow.com/questions/46586/...
Konrad Rudolph,

15
Każdy, kto posuwa się naprzód „to wszystko kompiluje się do JMP!” argument zasadniczo nie rozumie sensu programowania w języku wyższego poziomu.
Nietzche-jou

7
Wszystko, czego naprawdę potrzebujesz, to odejmowanie i rozgałęzianie. Cała reszta służy wygodzie lub wydajności.
David Stone

8

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 gotobę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, gotonależ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ć.


7

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 gotos.

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ć gotoprzed zhakowaniem asemblera.


3
Cóż ... cofam się do czasów programowania FORTRAN w firmie z branży informacji finansowej. W 2007 r.
Marcin

5
Każda struktura językowa może być nadużywana w sposób, który sprawia, że ​​jest ona nieczytelna lub źle działa.
WŁAŚNIE MOJA poprawna OPINIA

@JUST: Nie chodzi o czytelność lub słabą wydajność, ale o założenia i gwarancje dotyczące wykresu kontroli. Każde nadużycie goto byłoby w celu zwiększenia wydajności (lub czytelności).
Anders Eurenius

3
Twierdzę, że jednym z powodów, dla których gotojest 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ł.
cHao

1
... proste tłumaczenie wymaganego algorytmu na kod oparty na GOTO może być znacznie łatwiejsze do zweryfikowania niż bałagan zmiennych flag i nadużywane konstrukcje zapętlania.
supercat

7

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ę.


1
W podanym przez ciebie przykładzie wolę to znacznie zmodyfikować. Ogólnie staram się unikać komentarzy jednowierszowych, które mówią, co robi następny fragment kodu. Zamiast tego dzielę to na własną funkcję o nazwie podobnej do komentarza. Jeśli miałbyś dokonać takiej transformacji, ta funkcja dałaby ogólny przegląd tego, co robi funkcja, a każda z nowych funkcji określałaby sposób wykonywania każdego kroku. Uważam, że znacznie ważniejsze niż jakikolwiek sprzeciw wobec tego, aby gotokażda funkcja była na tym samym poziomie abstrakcji. To, czego unika, gototo bonus.
David Stone

2
Naprawdę nie wyjaśniłeś, jak dodanie większej liczby funkcji pozbywa się goto, wielu poziomów wcięcia i fałszywych instrukcji if ...
Ricardo

Przy użyciu standardowej notacji kontenera wyglądałoby to 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.
David Stone

3
Fakt, że każdy musi podać przykłady tego, w jaki sposób niektóre algorytmy powinny naturalnie używać goto - jest smutną refleksją na temat tego, jak mało dzisiaj trwa myślenie algorytmiczne !! Oczywiście przykład @ Ricardo jest (jednym z wielu) doskonałych przykładów tego, gdzie goto jest elegancki i oczywisty.
Fattie,

6

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.


1
Widziałem, że coś takiego jest użyteczne w języku takim jak C. Jednakże, gdy masz moc konstruktorów / destrukterów C ++, generalnie nie jest to tak przydatne.
David Stone

1
„W naprawdę złożonych funkcjach rozluźnimy tę zasadę, aby umożliwić innym skokom do przodu”. Bez przykładu brzmi to tak, jakbyś robił złożone funkcje jeszcze bardziej złożonymi za pomocą skoków. Czy nie byłoby „lepszym” podejściem do refaktoryzacji i podziału tych złożonych funkcji?
MikeMB

1
To był ostatni cel, dla którego użyłem goto. Nie tęsknię za goto w Javie, ponieważ ma ona możliwość wykonania tej samej pracy.
Patricia Shanahan

5

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.


3

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.

Oto szybki przykład:

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, gotoaby 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 tailmodyfikator i taki, który zaimportuje, recurjeś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 );
}

Większość innych powodów do użycia goto lepiej jest wykonać przy użyciu innych słów kluczowych.

Jak redona przykład trochę kodu:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

Lub przechodząc do lastodrobiny kodu z wielu miejsc:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

2

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.


2

Równie dobrze nikt nigdy nie wdrożył instrukcji „COME FROM” ....


8
Lub w C ++, C #, Java, JS, Python, Ruby .... itd. Itd. Tylko oni nazywają to „wyjątkami”.
cHao

2

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*/)

Czy nie powinno tak /*empty*/być stepfailed = 1? W każdym razie, jak to jest lepsze niż do{}while(0)? W obu przypadkach musisz breakwyjść z tego (lub w swoim stepfailed = 1; continue;). Nie wydaje mi się to konieczne.
Thomas Eding,

2

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.


Dobrze. „Słoń w pokoju” polega na tym, że jądro Linuksa, na litość boską, jako jeden z przykładów kodu o znaczeniu krytycznym dla świata - jest załadowane goto. Oczywiście, że jest. Oczywiście. „Anti-goto-meme” jest ciekawostką sprzed kilkudziesięciu lat. OCZYWIŚCIE, w programowaniu jest wiele rzeczy (w szczególności „statyka”, a nawet „globalne” i na przykład „elseif”), które mogą być nadużywane przez nieprofesjonalistów. Jeśli więc jesteś kuzynem, który uczy się w programie 2, powiedz im „och, nie używaj globałów” i „nigdy nie używaj inaczej”.
Fattie,

Błąd „goto fail” nie ma nic wspólnego z goto. Problem jest spowodowany później instrukcją if bez nawiasów klamrowych. Prawie jakakolwiek kopia instrukcji wklejona tam dwukrotnie spowodowałaby problem. Uważam, że ten rodzaj nagich bez ramiączek jest o wiele bardziej szkodliwy niż goto.
muusbolla

2

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 {...} whilewygląda bardziej znajomo)
  • looppotrzebna jest dodatkowa zmienna, używana w czterech miejscach
  • przeczytanie i zrozumienie pracy z. zajmuje więcej czasu loop
  • loopnie posiada żadnych danych, po prostu steruje przepływem wykonania, która jest mniej zrozumiały niż prosty etykiecie

Jest 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:;

1

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.


Myślę, że jedyną formą goto, której kiedykolwiek używałbyś w Perlu, jest goto &subroutine. Który uruchamia podprogram z bieżącym @_, jednocześnie zastępując bieżący podprogram w stosie.
Brad Gilbert,

1

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”.


11
Mówiąc „nie mogę wymyślić żadnego powodu” w twoim argumencie jest formalnie en.wikipedia.org/wiki/Argument_from_ignorance , chociaż wolę terminologię „dowód przez brak wyobraźni”.
WŁAŚNIE MOJA poprawna OPINIA

2
@ PO PROSTU MOJA poprawna OPINIA: To logiczny błąd tylko wtedy, gdy jest stosowany post-hoc. Z punktu widzenia projektanta języka ważnym argumentem może być rozważenie kosztu włączenia funkcji ( goto). Wykorzystanie @ cschol jest podobne: chociaż może nie projektuje teraz języka, zasadniczo ocenia wysiłek projektanta.
Konrad Rudolph

1
@KonradRudolph: IMHO, posiadanie dozwolonego języka, gotoz 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 gotomoże nie być tak przyjemne jak używanie jakiejś innej struktury, ale możliwość pisania takiego kodu gotopomoże uniknąć „dziur w ekspresji” - konstrukcjach, dla których język nie jest w stanie napisać wydajnego kodu.
supercat

1
@ supercat Obawiam się, że jesteśmy z zasadniczo różnych szkół projektowania języków. Sprzeciwiam się, aby języki były maksymalnie ekspresyjne za cenę zrozumiałości (lub poprawności). Wolę język restrykcyjny niż liberalny.
Konrad Rudolph

1
@ thb Tak, oczywiście, że tak. Często sprawia, że ​​kod jest znacznie trudniejszy do odczytania i uzasadnienia. Praktycznie za każdym razem, gdy ktoś publikuje kod gotona stronie przeglądu kodu, wyeliminowanie gotoznacznie upraszcza logikę kodu.
Konrad Rudolph

1

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 FINALbez 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”


6
Tak było często w oldschoolowym języku BASIC. Jednak we współczesnych wariantach nie wolno przeskakiwać na środek innej funkcji ... lub w wielu przypadkach nawet poza deklarację zmiennej. Zasadniczo (kalambur nie zamierzony) współczesne języki w dużej mierze wyeliminowały „nieokiełznane” GOTO, o których mówił Dijkstra ... a jedynym sposobem na użycie tego, jak się kłócił, jest popełnienie niektórych innych ohydnych grzechów. :)
cHao

1

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”.


3
Właśnie dlatego mamy finallybloki w języku C #
John Saunders,

^ jak @JohnSaunders powiedział. Jest to przykład „używania goto, ponieważ w języku brakuje odpowiedniej konstrukcji kontrolnej”. JEDNAK to zapach kodu, który wymaga WIELU punktów goto w pobliżu zwrotu. Istnieje styl programowania, który jest bezpieczniejszy (łatwiej go nie zepsuć) ORAZ nie wymaga gotos, który działa dobrze nawet w językach bez „w końcu”: zaprojektuj te wywołania „czyszczenia”, aby były nieszkodliwe, gdy nie będą czyszczone wymagane jest w górę. Uwzględnij wszystko oprócz czyszczenia w metodzie wykorzystującej projekt wielokrotnego zwrotu. Wywołaj tę metodę, a następnie wykonaj wywołania czyszczenia.
ToolmakerSteve

Zauważ, że podejście, które opisuję, wymaga dodatkowego poziomu wywołania metody (ale tylko w języku, w którym brakuje finally). Alternatywnie użyj gotos, 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.
ToolmakerSteve,

@ToolmakerSteve to nie jest zapach kodu; w rzeczywistości jest to bardzo powszechny wzorzec w C i jest uważany za jeden z najbardziej prawidłowych sposobów używania goto. Chcesz, żebym utworzył 5 metod, z których każda ma własny niepotrzebny test if, tylko po to, aby obsłużyć czyszczenie z tej funkcji? Utworzyłeś teraz kod ORAZ wzdęcie wydajności. Lub możesz po prostu użyć goto.
muusbolla

@muusbolla - 1) „utwórz 5 metod” - Nie. Sugeruję pojedynczą metodę, która oczyszcza wszelkie zasoby o wartości innej niż null. LUB zobacz moją alternatywę, która używa gotos, ż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 Cmasz 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.)
ToolmakerSteve

0

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 .


0

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_literaldo 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ć continueinstrukcji (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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.