È mai vantaggioso usare 'goto' in un linguaggio che supporta loop e funzioni? Se è così, perché?


201

Sono stato a lungo sotto l'impressione che gotonon dovrebbe mai essere usato se possibile. Mentre guardavo libavcodec (che è scritto in C) l'altro giorno, ho notato molteplici usi di esso. È mai vantaggioso utilizzare gotoin un linguaggio che supporta loop e funzioni? Se è così, perché?

Risposte:


242

Ci sono alcuni motivi per usare la frase "goto" di cui sono a conoscenza (alcuni ne hanno già parlato):

Uscire in modo pulito da una funzione

Spesso in una funzione, è possibile allocare risorse e dover uscire in più punti. I programmatori possono semplificare il loro codice inserendo il codice di pulizia delle risorse alla fine della funzione e tutti i "punti di uscita" della funzione passerebbero all'etichetta di pulizia. In questo modo, non è necessario scrivere il codice di pulizia in ogni "punto di uscita" della funzione.

Uscita da loop nidificati

Se sei in un ciclo nidificato e devi uscire da tutti i loop, un goto può renderlo molto più pulito e più semplice delle dichiarazioni di interruzione e degli if-check.

Miglioramenti delle prestazioni di basso livello

Questo è valido solo nel codice perf-critico, ma le istruzioni goto vengono eseguite molto rapidamente e possono darti una spinta quando ti muovi attraverso una funzione. Questa è un'arma a doppio taglio, perché un compilatore in genere non può ottimizzare il codice che contiene gotos.

Si noti che in tutti questi esempi, i gotos sono limitati all'ambito di una singola funzione.


15
Il modo giusto per uscire da loop nidificati è di refactoring il loop interno in un metodo separato.
Jason,

129
@Jason - Bah. È un carico di tori. Sostituire gotocon returnè semplicemente stupido. Non si tratta di "refactoring", ma solo di "rinominare" in modo che le persone cresciute in un gotoambiente soppresso (cioè tutti noi) si sentano meglio nell'usare ciò che moralmente equivale a goto. Preferisco di gran lunga vedere il loop dove lo uso e vedere un po ' goto, che di per sé è solo uno strumento , piuttosto che vedere qualcuno che ha spostato il loop da qualche parte non correlato solo per evitare un goto.
Chris Lutz,

18
Ci sono delle sit in cui le goto sono importanti: ad esempio un ambiente C ++ senza eccezioni. Nella fonte Silverlight abbiamo decine di migliaia (o più) di dichiarazioni goto per una funzione sicura che esistono attraverso l'uso di macro: i codec e le librerie dei media chiave spesso funzionano attraverso valori di ritorno e mai eccezioni, ed è difficile combinare questi meccanismi di gestione degli errori in un solo modo performante.
Jeff Wilcox,

79
Vale la pena di notare che tutti break, continue, returnsono fondamentalmente goto, proprio nel bel packaging.
el.pescado,

16
Non vedo come do{....}while(0)dovrebbe essere un'idea migliore di goto, tranne per il fatto che funziona in Java.
Elenco Jeremy

906

Chiunque sia contrario goto, direttamente o indirettamente, all'articolo GoTo considerato dannoso di Edsger Dijkstra a sostegno della propria posizione. Peccato che l'articolo di Dijkstra non abbia praticamente nulla a che fare con il modo in cui le gotodichiarazioni sono usate in questi giorni e quindi ciò che dice l'articolo ha poca o nessuna applicabilità alla moderna scena della programmazione. Il gotomeme senza-orlo ora punta su una religione, fino alle sue scritture dettate dall'alto, i suoi sommi sacerdoti e lo schianto (o peggio) degli eretici percepiti.

Mettiamo il contesto di Dijkstra nel contesto per fare un po 'di luce sull'argomento.

Quando Dijkstra scrisse il suo articolo le lingue popolari dell'epoca erano quelle procedurali non strutturate come BASIC, FORTRAN (i dialetti precedenti) e varie lingue assembleari. Era abbastanza comune per le persone che usano le lingue di livello superiore saltare tutta la loro base di codice in fili contorti e contorti di esecuzione che hanno dato origine al termine "codice spaghetti". Puoi vederlo saltando al classico gioco Trek scritto da Mike Mayfield e cercando di capire come funzionano le cose. Prenditi qualche momento per guardarlo.

QUESTO è "l'uso sfrenato della dichiarazione go to" contro cui Dijkstra stava recitando nel suo documento nel 1968. Questo è l'ambiente in cui viveva che lo ha portato a scrivere quel documento. La capacità di saltare ovunque nel tuo codice in qualsiasi momento ti piacesse era ciò che stava criticando e chiedendo di essere fermato. Confrontandolo con i poteri anemici digoto in C o di altri linguaggi così più moderni è semplicemente risibile.

Posso già sentire i canti sollevati dei cultisti mentre affrontano l'eretico. "Ma" canteranno "puoi rendere il codice molto difficile da leggere gotoin C." O si? Puoi anche rendere il codice molto difficile da leggere senza goto. Come questo:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Non uno gotoin vista, quindi deve essere facile da leggere, giusto? O che ne pensi di questo:

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

Neanche gotolì. Deve quindi essere leggibile.

Qual è il mio punto con questi esempi? Non sono le funzionalità linguistiche che rendono il codice illeggibile e non realizzabile. Non è la sintassi che lo fa. Sono i cattivi programmatori a causare questo. E i cattivi programmatori, come puoi vedere nell'elemento sopra, possono rendere illeggibile e inutilizzabile qualsiasi lingua. Come i forloop lassù. (Li vedi, vero?)

Ora, per essere onesti, alcuni costrutti linguistici sono più facili da abusare di altri. Se sei un programmatore C, scruterei molto più da vicino circa il 50% degli usi di #definemolto prima di andare contro una crociata goto!

Quindi, per coloro che si sono presi la briga di leggere così lontano, ci sono diversi punti chiave da notare.

  1. Il documento di Dijkstra sulle gotodichiarazioni è stato scritto per un ambiente di programmazione in cui gotoera molto più potenzialmente dannoso di quanto non lo sia nella maggior parte dei linguaggi moderni che non sono un assemblatore.
  2. Eliminando automaticamente tutti gli usi di goto per questo è razionale quanto dire "Ho provato a divertirmi una volta ma non mi è piaciuto, quindi ora sono contrario".
  3. Ci sono usi legittimi del moderno (anemico) goto dichiarazioni nel codice che non possono essere adeguatamente sostituite da altri costrutti.
  4. Vi sono, ovviamente, usi illegittimi delle stesse dichiarazioni.
  5. Ci sono anche usi illegittimi delle moderne dichiarazioni di controllo come l '" godo" abominio in cui un dociclo sempre falso viene interrotto dall'uso breakal posto di un goto. Questi sono spesso peggiori dell'uso giudizioso di goto.

42
@pocjoc: scrivere l'ottimizzazione ricorsiva della coda in C, per esempio. Ciò emerge dall'implementazione di interpreti / runtime linguistici funzionali, ma è rappresentativo di un'interessante categoria di problemi. La maggior parte dei compilatori scritti in C usano goto per questo motivo. È un uso di nicchia che non si verifica nella maggior parte delle situazioni, ovviamente, ma nelle situazioni in cui è richiesto ha molto senso da usare.
zxq9,

66
Perché tutti parlano del documento "Vai a considerato dannoso" di Dijkstra senza menzionare la risposta di Knuth, "Programmazione strutturata con dichiarazioni go"?
Oblomov,

22
@ Nietzche-jou "questo è un uomo di paglia palese [...]" Sta usando sarcasticamente una falsa dicotomia per mostrare perché lo stigma è illogico. Un Strawman è un errore logico in cui si travisa intenzionalmente la posizione dell'avversario per far sembrare che la sua posizione sia facilmente sconfitta. Ad esempio "gli atei sono solo atei perché odiano Dio". Una falsa dicotomia si verifica quando si assume che l'estremità opposta sia vera quando esiste almeno un'ulteriore possibilità (cioè bianco e nero). Ad esempio "Dio odia gli omosessuali, quindi adora gli eteros.".
Braden Best

27
@JLRishe Stai leggendo troppo in profondità in qualcosa che non è nemmeno lì. È solo un vero errore logico se la persona che lo ha usato onestamente crede che abbia un senso logico. Ma lo ha detto sarcasticamente, indicando chiaramente che sa che è ridicolo, ed è così che lo sta mostrando. Non "lo usa per giustificare il suo punto"; lo sta usando per mostrare perché è ridicolo dire che le goto trasformano automaticamente il codice in spaghetti. Cioè puoi ancora scrivere codice di merda senza goto. Questo è il suo punto. E la falsa dicotomia sarcastica è il suo metodo per illustrarla in modo umoristico.
Braden Best

32
-1 per aver sollevato un argomento molto ampio sul perché l'osservazione di Dijkstra non è applicabile, dimenticando di mostrare quali gotosono effettivamente i vantaggi (che è la domanda posta)
Maarten Bodewes,

154

Obbedire alle migliori pratiche alla cieca non è una buona pratica. L'idea di evitare gotoaffermazioni come forma primaria di controllo del flusso è quella di evitare la produzione di codice spaghetti illeggibile. Se usati con parsimonia nei posti giusti, a volte possono essere il modo più semplice e chiaro di esprimere un'idea. Walter Bright, il creatore del compilatore C ++ di Zortech e del linguaggio di programmazione D, li usa frequentemente, ma con giudizio. Anche con le gotodichiarazioni, il suo codice è ancora perfettamente leggibile.

In conclusione: evitare gotoper il gusto di evitare gotoè inutile. Quello che vuoi veramente evitare è la produzione di codice illeggibile. Se il tuo gotocodice -laden è leggibile, non c'è nulla di sbagliato in esso.


36

Dal momento che gotorende difficile il ragionamento sul flusso di programma 1 (alias "spaghetti code"), gotoviene generalmente utilizzato solo per compensare le funzioni mancanti: l'uso di gotopuò effettivamente essere accettabile, ma solo se la lingua non offre una variante più strutturata per ottenere lo stesso obiettivo. Prendi l'esempio del dubbio:

La regola con goto che usiamo è che goto va bene per saltare in avanti verso un singolo punto di pulizia di uscita in una funzione.

Questo è vero, ma solo se la lingua non consente la gestione strutturata delle eccezioni con il codice di pulizia (come RAII o finally ), che fa lo stesso lavoro meglio (poiché è appositamente progettato per farlo), o quando c'è una buona ragione per non utilizzare la gestione delle eccezioni strutturata (ma non si avrà mai questo caso se non a un livello molto basso).

Nella maggior parte delle altre lingue, l'unico uso accettabile di gotoè di uscire da loop nidificati. E anche lì è quasi sempre meglio sollevare il circuito esterno in un proprio metodo e utilizzare returninvece.

A parte questo, gotoè un segno che non è stato pensato abbastanza nel particolare pezzo di codice.


1 I linguaggi moderni che supportano l' gotoimplementazione di alcune restrizioni (ad esempio gotopotrebbero non entrare o uscire dalle funzioni) ma il problema rimane sostanzialmente lo stesso.

Per inciso, lo stesso vale ovviamente anche per le altre funzionalità linguistiche, in particolare le eccezioni. E di solito esistono regole rigide per usare queste funzionalità solo dove indicato, come la regola di non usare eccezioni per controllare un flusso di programmi non eccezionale.


4
Solo curioso qui, ma per quanto riguarda il caso di usare gotos per ripulire il codice. Per ripulitura, intendo, non solo la dislocazione della memoria, ma anche la registrazione degli errori. Stavo leggendo un sacco di post e, a quanto pare, nessuno scrive codice che stampa registri .. hmm ?!
shiva,

37
"Fondamentalmente, a causa della natura difettosa di goto (e credo che questo non sia controverso)" -1 e ho smesso di leggere lì.
o0 '.

14
L'ammonizione contro goto proviene dall'era della programmazione strutturata, in cui anche i primi ritorni erano considerati malvagi. Lo spostamento di loop nidificati in una funzione scambia un "male" con un altro e crea una funzione che non ha una vera ragione di esistere (al di fuori di "mi ha permesso di evitare di usare un goto!"). È vero che goto è il singolo strumento più potente per produrre spaghetti code se usato senza vincoli, ma questa è un'applicazione perfetta per questo; semplifica il codice. Knuth ha sostenuto questo uso e Dijkstra ha detto "Non cadere nella trappola di credere che io sia terribilmente dogmatico riguardo al goto".
Fango

5
finally? Quindi usare le eccezioni per cose diverse dalla gestione degli errori è buono ma usare gotofa male? Penso che le eccezioni abbiano un nome abbastanza appropriato.
Christian,

3
@Mud: nei casi che incontro (su base giornaliera), la creazione di una funzione "che non ha una vera ragione di esistere" è la soluzione migliore. Motivo: rimuove i dettagli dalla funzione di livello superiore, in modo che il risultato abbia un flusso di controllo di facile lettura. Non incontro personalmente situazioni in cui un goto produce il codice più leggibile. D'altra parte, utilizzo più ritorni, in semplici funzioni, in cui qualcun altro potrebbe preferire andare a un singolo punto di uscita. Mi piacerebbe vedere contro-esempi in cui goto è più leggibile del refactoring in funzioni ben denominate.
ToolmakerSteve

35

Bene, c'è una cosa che è sempre peggio di goto's; strano uso di altri operatori del flusso di programma per evitare un goto:

Esempi:

    // 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)Penso che possa essere considerato idiomatico. Non ti è permesso essere in disaccordo: D
Thomas Eding,

37
@trinithis: se è "idiomatico", questo è solo a causa del culto anti-goto. Se lo guardi da vicino, ti renderai conto che è solo un modo di dire goto after_do_block;senza dirlo davvero. Altrimenti ... un "loop" che gira esattamente una volta? Definirei quell'abuso delle strutture di controllo.
cHao,

5
@ThomasEding Eding C'è un'eccezione al tuo punto. Se hai mai fatto un po 'di programmazione C / C ++ e hai dovuto usare #define, sapresti che {} while (0) è uno degli standard per l'incapsulamento di più righe di codice. Ad esempio: #define do {memcpy (a, b, 1); qualcosa ++;} while (0) è più sicuro e migliore di #define memcpy (a, b, 1); qualcosa ++
Ignas2526

10
@ Ignas2526 Hai appena mostrato come #definesono molte, molte volte molto peggio che goto
usarle

3
+1 per un buon elenco di modi in cui le persone cercano di evitare goto, fino all'estremo; Ho visto menzioni di tali tecniche altrove, ma questa è una lista grande, concisa. Riflettendo sul perché, negli ultimi 15 anni, il mio team non abbia mai avuto bisogno di nessuna di queste tecniche, né usato goto. Anche in un'applicazione client / server con centinaia di migliaia di righe di codice, da più programmatori. C #, java, PHP, python, javascript. Codice client, server e app. Non vantarsi, né fare proselitismo per una posizione, è davvero curioso di sapere perché alcune persone affrontano situazioni che richiedono goto come soluzione più chiara, e altre no ...
ToolmakerSteve

28

In C # interruttore dichiarazione fai non permettere caduta-through . Quindi goto viene utilizzato per trasferire il controllo a un'etichetta specifica per la custodia o l' impostazione predefinita all'etichetta .

Per esempio:

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;
}

Modifica: esiste un'eccezione alla regola "nessun fall-through". Fall-through è consentito se un'istruzione case non ha codice.


3
Interruttore autunno-through è supportato in .NET 2.0 - msdn.microsoft.com/en-us/library/06tc147t(VS.80).aspx
rjzii

9
Solo se il caso non ha un corpo di codice. Se ha un codice, devi usare la parola chiave goto.
Matthew Whited,

26
Questa risposta è così divertente: C # è stato rimosso perché molti lo vedono come dannoso, e questo esempio usa goto (anche visto come dannoso da molti) per rilanciare il comportamento originale, apparentemente dannoso, MA il risultato complessivo è in realtà meno dannoso ( perché il codice chiarisce che il fallthrough è intenzionale!).
thomasrutter,

9
Solo perché una parola chiave è scritta con le lettere GOTO non la rende un goto. Questo caso presentato non è un goto. È un costrutto switch statement per fallthrough. Inoltre, non conosco molto bene C #, quindi potrei sbagliarmi.
Thomas Eding,

1
Bene, in questo caso è un po 'più che fall-through (perché puoi dire goto case 5:quando sei nel caso 1). Sembra che la risposta di Konrad Rudolph sia corretta qui: gotosta compensando una caratteristica mancante (ed è meno chiara di quella reale). Se ciò che vogliamo davvero è fall-through, forse il miglior default non sarebbe fall-through, ma qualcosa come continuerichiederlo esplicitamente.
David Stone,

14

#ifdef TONGUE_IN_CHEEK

Perl ha un gotoche ti permette di implementare le chiamate di coda dei poveri. :-P

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Va bene, quindi non ha nulla a che fare con C's goto. Più seriamente, sono d'accordo con gli altri commenti sull'utilizzo gotoper le pulizie, o per l'implementazione del dispositivo di Duff o simili. Si tratta solo di usare, non di abusare.

(Lo stesso commento può essere applicato a longjmpeccezioni call/cce simili --- hanno usi legittimi, ma possono essere facilmente abusati. Ad esempio, lanciare un'eccezione puramente per sfuggire a una struttura di controllo profondamente annidata, in circostanze completamente non eccezionali .)


Penso che questa sia l'unica ragione per usare goto in Perl.
Brad Gilbert,

12

Nel corso degli anni ho scritto più di poche righe di linguaggio assembly. Alla fine, ogni linguaggio di alto livello si compila in goto. Va bene, chiamali "rami" o "salti" o qualsiasi altra cosa, ma sono goto. Qualcuno può scrivere assembler goto-less?

Ora certo, puoi indicare a un programmatore Fortran, C o BASIC che eseguire una rivolta con goto è una ricetta per spaghetti alla bolognese. La risposta però non è evitarli, ma usarli con cura.

Un coltello può essere usato per preparare cibo, liberare qualcuno o uccidere qualcuno. Facciamo senza coltelli per paura di quest'ultimo? Allo stesso modo il goto: usato con noncuranza ostacola, usato con cura aiuta.


1
Forse vuoi leggere perché credo che questo sia fondamentalmente sbagliato su stackoverflow.com/questions/46586/…
Konrad Rudolph,

15
Chiunque avanzi il "tutto si compila in JMP comunque!" l'argomento sostanzialmente non comprende il punto alla base della programmazione in un linguaggio di livello superiore.
Nietzche-jou,

7
Tutto ciò di cui hai veramente bisogno è sottrarre e ramificare. Tutto il resto è per convenienza o prestazione.
David Stone,

8

Dai un'occhiata a Quando usare Goto durante la programmazione in C :

Sebbene l'uso di goto sia quasi sempre una cattiva pratica di programmazione (sicuramente puoi trovare un modo migliore di fare XYZ), ci sono momenti in cui non è davvero una cattiva scelta. Alcuni potrebbero persino sostenere che, quando è utile, è la scelta migliore.

La maggior parte di ciò che ho da dire su goto si applica in realtà solo a C. Se stai usando C ++, non c'è motivo valido per usare goto al posto delle eccezioni. In C, tuttavia, non hai la potenza di un meccanismo di gestione delle eccezioni, quindi se desideri separare la gestione degli errori dal resto della logica del programma e vuoi evitare di riscrivere il codice di pulizia più volte nel codice, allora goto può essere una buona scelta.

Cosa voglio dire? Potresti avere un codice simile al seguente:

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];
}

Questo va bene fino a quando non ti rendi conto che è necessario modificare il codice di pulizia. Quindi devi passare e apportare 4 modifiche. Ora, potresti decidere che puoi semplicemente incapsulare tutta la pulizia in un'unica funzione; non è una cattiva idea. Ma significa che dovrai stare attento con i puntatori: se prevedi di liberare un puntatore nella tua funzione di pulizia, non c'è modo di impostarlo su NULL a meno che non passi un puntatore a un puntatore. In molti casi, non utilizzerai nuovamente quel puntatore, quindi potrebbe non essere un grosso problema. D'altra parte, se aggiungi un nuovo puntatore, un handle di file o altre cose che necessitano di pulizia, dovrai cambiare di nuovo la tua funzione di pulizia; e quindi dovrai cambiare gli argomenti in quella funzione.

Usando goto, lo sarà

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;
}

Il vantaggio qui è che il codice che segue ha accesso a tutto ciò che sarà necessario per eseguire la pulizia e sei riuscito a ridurre considerevolmente il numero di punti di modifica. Un altro vantaggio è che sei passato dall'avere più punti di uscita per la tua funzione a uno solo; non c'è possibilità che tu possa tornare accidentalmente dalla funzione senza ripulire.

Inoltre, poiché goto viene utilizzato solo per saltare a un singolo punto, non è come se si stesse creando una massa di codice spaghetti saltando avanti e indietro nel tentativo di simulare le chiamate di funzione. Piuttosto, goto in realtà aiuta a scrivere codice più strutturato.


In una parola, gotodovrebbe sempre essere usato con parsimonia e come ultima risorsa - ma c'è un tempo e un posto per questo. La domanda non dovrebbe essere "devi usarla" ma "è la scelta migliore" per usarla.


7

Uno dei motivi per cui goto è male, oltre allo stile di codifica è che puoi usarlo per creare loop sovrapposti , ma non nidificati :

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

Ciò creerebbe la struttura del flusso di controllo bizzarra, ma forse legale, in cui una sequenza come (a, b, c, b, a, b, a, b, ...) è possibile, il che rende infelici gli hacker del compilatore. Apparentemente ci sono una serie di trucchi di ottimizzazione intelligenti che si basano su questo tipo di struttura che non si verifica. (Dovrei controllare la mia copia del libro dei draghi ...) Il risultato di questo potrebbe (usando alcuni compilatori) essere che altre ottimizzazioni non sono fatte per il codice che contienegoto s.

Potrebbe essere utile se lo sai solo "oh, a proposito", capita di convincere il compilatore ad emettere codice più veloce. Personalmente, preferirei provare a spiegare al compilatore cosa è probabile e cosa no prima di usare un trucco come goto, ma probabilmente, potrei anche provare gotoprima di hackerare assemblatore.


3
Bene ... mi riporta ai giorni della programmazione di FORTRAN in una società di informazioni finanziarie. Nel 2007.
Marcin,

5
Qualsiasi struttura linguistica può essere abusata in modi che la rendono illeggibile o poco performante.
SOLO IL MIO OPINIONE corretta,

@JUST: Il punto non riguarda la leggibilità o le scarse prestazioni, ma ipotesi e garanzie sul grafico del flusso di controllo. Qualsiasi abuso di goto sarebbe per migliorare le prestazioni (o la leggibilità).
Anders Eurenius,

3
Direi che uno dei motivi per cui gotoè utile è che ti permette di costruire loop come questo, che altrimenti richiederebbe un mucchio di contorsioni logiche. Direi inoltre che se l'ottimizzatore non sa come riscriverlo, va bene . Un ciclo come questo non dovrebbe essere fatto per prestazioni o leggibilità, ma perché è esattamente nell'ordine in cui le cose devono accadere. In tal caso, non vorrei che l'ottimizzatore lo rovinasse.
cHao,

1
... una traduzione semplice dell'algoritmo richiesto in codice basato su GOTO può essere molto più facile da convalidare di un pasticcio di variabili flag e costrutti di loop abusati.
supercat

7

Trovo divertente che alcune persone arrivino al punto di fornire un elenco di casi in cui goto è accettabile, dicendo che tutti gli altri usi sono inaccettabili. Pensi davvero di conoscere ogni caso in cui goto è la scelta migliore per esprimere un algoritmo?

Per illustrarti, ti faccio un esempio che nessuno qui ha ancora mostrato:

Oggi stavo scrivendo un codice per inserire un elemento in una tabella hash. La tabella hash è una cache di calcoli precedenti che possono essere sovrascritti a piacimento (influendo sulle prestazioni ma non sulla correttezza).

Ogni bucket della tabella hash ha 4 slot e ho un sacco di criteri per decidere quale elemento sovrascrivere quando un bucket è pieno. In questo momento questo significa fare fino a tre passaggi attraverso un secchio, in questo modo:

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

Se non usassi goto, come sarebbe questo codice?

Qualcosa come questo:

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

Sembrerebbe sempre peggio se venissero aggiunti più passaggi, mentre la versione con goto mantiene sempre lo stesso livello di rientro ed evita l'uso di false istruzioni if ​​il cui risultato è implicito dall'esecuzione del ciclo precedente.

Quindi c'è un altro caso in cui goto rende il codice più pulito e più facile da scrivere e capire ... Sono sicuro che ce ne sono molti altri, quindi non far finta di conoscere tutti i casi in cui goto è utile, dissumando quelli buoni che non potresti non pensare.


1
Nell'esempio che hai dato, preferirei riformattare in modo piuttosto significativo. In generale, cerco di evitare i commenti di una riga che dicono cosa sta facendo il prossimo pezzo di codice. Invece, lo divido nella sua stessa funzione che è chiamata simile al commento. Se dovessi effettuare una tale trasformazione, questa funzione fornirebbe una panoramica di alto livello su ciò che la funzione sta facendo e ciascuna delle nuove funzioni indicherà come fai ogni passaggio. Ritengo che molto più importante di qualsiasi opposizione gotosia avere ogni funzione allo stesso livello di astrazione. Che eviti gotoè un bonus.
David Stone,

2
Non hai davvero spiegato come l'aggiunta di più funzioni elimini il goto, i livelli multipli di rientro e le dichiarazioni spurie se ...
Ricardo

Sarebbe simile a questo, usando la notazione contenitore standard: container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();trovo che quel tipo di programmazione sia molto più facile da leggere. Ogni funzione in questo caso potrebbe essere scritta come una funzione pura, che è molto più facile ragionare. La funzione principale ora spiega cosa fa il codice semplicemente con il nome delle funzioni, e poi se vuoi, puoi guardare le loro definizioni per scoprire come lo fa.
David Stone,

3
Il fatto che qualcuno debba fornire esempi, di come alcuni algoritmi dovrebbero naturalmente usare un goto - è una triste riflessione su quanto poco vada avanti il ​​pensiero algoritmico !! Naturalmente, l'esempio di @ Ricardo è (uno dei tanti) esempi perfetti di dove goto è elegante ed evidente.
Fattie,

6

La regola con goto che usiamo è che goto va bene per saltare in avanti verso un singolo punto di pulizia di uscita in una funzione. In funzioni davvero complesse rilassiamo questa regola per consentire altri salti in avanti. In entrambi i casi evitiamo istruzioni nidificate se spesso si verificano con il controllo del codice di errore, il che aiuta la leggibilità e la manutenzione.


1
Potrei vedere qualcosa di simile che è utile in un linguaggio come C. Tuttavia, quando hai il potere dei costruttori / distruttori C ++, questo non è generalmente così utile.
David Stone,

1
"In funzioni davvero complesse rilassiamo quella regola per consentire altri salti in avanti" Senza un esempio, sembra, se stai rendendo le funzioni complesse ancora più complesse usando i salti. Non sarebbe l'approccio "migliore" quello di refactoring e dividere quelle funzioni complesse?
MikeMB,

1
Questo è stato l'ultimo scopo per cui ho usato goto. Non mi manca goto in Java, perché ha try-finalmente che può fare lo stesso lavoro.
Patricia Shanahan,

5

La discussione più ponderata e approfondita delle dichiarazioni di goto, dei loro usi legittimi e dei costrutti alternativi che possono essere usati al posto di "dichiarazioni di goto virtuose" ma che possono essere abusate facilmente come le dichiarazioni di goto, è l'articolo di Donald Knuth " Programmazione strutturata con dichiarazioni di goto " , nel dicembre 1974 Computing Surveys (volume 6, n. 4. pp. 261 - 301).

Non sorprende che alcuni aspetti di questo documento di 39 anni siano datati: aumenti degli ordini di grandezza della potenza di elaborazione rendono alcuni dei miglioramenti delle prestazioni di Knuth impercettibili per problemi di dimensioni moderate, e da allora sono stati inventati nuovi costrutti del linguaggio di programmazione. (Ad esempio, i blocchi try-catch fanno parte del Construct di Zahn, sebbene vengano usati raramente in quel modo.) Ma Knuth copre tutti gli aspetti dell'argomento e dovrebbe essere richiesto di leggere prima che qualcuno ripeti nuovamente il problema.


3

In un modulo Perl, occasionalmente vuoi creare subroutine o chiusure al volo. Il fatto è che una volta creata la subroutine, come ci si arriva. Potresti semplicemente chiamarlo, ma se la subroutine lo utilizza caller()non sarà così utile come potrebbe essere. È qui che la goto &subroutinevariazione può essere utile.

Ecco un breve esempio:

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

È inoltre possibile utilizzare questo modulo gotoper fornire una forma rudimentale di ottimizzazione della coda.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

( Nel versione 16 di Perl 5 sarebbe meglio scrivere come goto __SUB__;)

C'è un modulo che importerà un tailmodificatore e uno che importerà recurse non ti piace usare questo modulo digoto .

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 );
}

La maggior parte degli altri motivi per utilizzare gotosono fatti meglio con altre parole chiave.

Come redoun po 'di codice:

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

O andando a lastun po 'di codice da più punti:

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

2

Se è così, perché?

C non ha interruzioni multi-livello / etichettate e non tutti i flussi di controllo possono essere facilmente modellati con l'iterazione di C e le primitive decisionali. le goto fanno molto per rimediare a questi difetti.

A volte è più chiaro usare una variabile flag di qualche tipo per effettuare una sorta di interruzione pseudo-multi-livello, ma non è sempre superiore al goto (almeno un goto consente di determinare facilmente dove va il controllo, a differenza di una variabile flag ) e a volte semplicemente non si desidera pagare il prezzo di esecuzione di bandiere / altre contorsioni per evitare il goto.

libavcodec è un pezzo di codice sensibile alle prestazioni. L'espressione diretta del flusso di controllo è probabilmente una priorità, perché tenderà a funzionare meglio.


2

Allo stesso modo nessuno ha mai implementato la frase "COME FROM" ...


8
O in C ++, C #, Java, JS, Python, Ruby .... ecc ecc ecc. Solo loro lo chiamano "eccezioni".
cao

2

Trovo che do {} mentre (falso) utilizzo sia assolutamente ribelle. È concepibile potrebbe convincermi che è necessario in qualche caso strano, ma mai che sia un codice pulito e sensibile.

Se è necessario eseguire alcuni di questi cicli, perché non rendere esplicita la dipendenza dalla variabile flag?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

Non dovrebbe /*empty*/essere stepfailed = 1? In ogni caso, come è meglio di un do{}while(0)? In entrambi, devi breakuscirne (o nel tuo stepfailed = 1; continue;). Mi sembra superfluo.
Thomas Eding,

2

1) L'uso più comune di goto che conosco è emulare la gestione delle eccezioni in linguaggi che non lo offrono, in particolare in C. (Il codice fornito da Nuclear sopra è proprio questo.) Guarda il codice sorgente di Linux e tu ' Vedrò un bazillion goto usato in quel modo; c'erano circa 100.000 goto nel codice Linux secondo un rapido sondaggio condotto nel 2013: http://blog.regehr.org/archives/894 . L'utilizzo di Goto è anche menzionato nella guida allo stile di codifica Linux: https://www.kernel.org/doc/Documentation/CodingStyle . Proprio come la programmazione orientata agli oggetti viene emulata usando strutture popolate con puntatori a funzioni, goto ha il suo posto nella programmazione C. Quindi chi ha ragione: Dijkstra o Linus (e tutti i codificatori del kernel Linux)? Fondamentalmente è teoria contro pratica.

Esiste tuttavia il solito gotcha per non avere supporto a livello di compilatore e controlli per costrutti / schemi comuni: è più facile usarli in modo errato e introdurre bug senza controlli in fase di compilazione. Windows e Visual C ++ ma in modalità C offrono la gestione delle eccezioni tramite SEH / VEH proprio per questo: le eccezioni sono utili anche al di fuori dei linguaggi OOP, ovvero in un linguaggio procedurale. Ma il compilatore non può sempre salvare la tua pancetta, anche se offre supporto sintattico per le eccezioni nella lingua. Considera come esempio di quest'ultimo caso il famoso bug "goto fail" di Apple SSL, che ha appena duplicato un goto con conseguenze disastrose ( 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

Puoi avere esattamente lo stesso bug usando le eccezioni supportate dal compilatore, ad esempio in C ++:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

Ma entrambe le varianti del bug possono essere evitate se il compilatore analizza e ti avverte di un codice non raggiungibile. Ad esempio la compilazione con Visual C ++ a livello di avviso / W4 trova il bug in entrambi i casi. Java, per esempio, proibisce il codice irraggiungibile (dove può trovarlo!) Per una ragione abbastanza buona: è probabile che sia un bug nel codice medio di Joe. Fintanto che il costrutto goto non consente obiettivi che il compilatore non può facilmente capire, come goto per indirizzi calcolati (**), non è più difficile per il compilatore trovare un codice irraggiungibile all'interno di una funzione con goto che usare Dijkstra codice approvato.

(**) Nota a piè di pagina: sono possibili goto a numeri di riga calcolati in alcune versioni di Basic, ad es. GOTO 10 * x dove x è una variabile. Piuttosto confusamente, in Fortran "goto calcolato" si riferisce a un costrutto equivalente a un'istruzione switch in C. Lo standard C non consente goto calcolati nella lingua, ma solo goto a etichette dichiarate staticamente / sintatticamente. GNU C ha tuttavia un'estensione per ottenere l'indirizzo di un'etichetta (operatore unario, prefisso &&) e consente anche di passare a una variabile di tipo void *. Vedi https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html per ulteriori informazioni su questo oscuro argomento secondario. Il resto di questo post non si preoccupa di quella oscura funzionalità di GNU C.

Le gotos standard C (cioè non calcolate) non sono in genere il motivo per cui non è possibile trovare il codice non raggiungibile al momento della compilazione. Il solito motivo è il codice logico come il seguente. Dato

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

È altrettanto difficile per un compilatore trovare il codice non raggiungibile in uno dei seguenti 3 costrutti:

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&) {
  }
}

(Scusa il mio stile di codifica correlato alla parentesi graffa, ma ho cercato di mantenere gli esempi il più compatti possibile.)

Visual C ++ / W4 (anche con / Ox) non riesce a trovare il codice irraggiungibile in nessuno di questi e, come probabilmente saprai, il problema di trovare un codice irraggiungibile in generale è indecidibile. (Se non ci credi: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf )

Come problema correlato, C goto può essere usato per emulare eccezioni solo all'interno del corpo di una funzione. La libreria C standard offre una coppia di funzioni setjmp () e longjmp () per emulare uscite / eccezioni non locali, ma queste presentano alcuni seri inconvenienti rispetto a ciò che offrono altre lingue. L'articolo di Wikipedia http://en.wikipedia.org/wiki/Setjmp.h spiega abbastanza bene quest'ultima questione. Questa coppia di funzioni funziona anche su Windows ( http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx ), ma quasi nessuno li usa perché SEH / VEH è superiore. Anche su Unix, penso che setjmp e longjmp siano usati molto raramente.

2) Penso che il secondo uso più comune di goto in C sia l'implementazione di interruzioni multilivello o continui multilivello, che è anche un caso d'uso abbastanza controverso. Ricorda che Java non consente di andare all'etichetta, ma consente di interrompere l'etichetta o continuare l'etichetta. Secondo http://www.oracle.com/technetwork/java/simple-142616.html , questo è in realtà il caso d'uso più comune di goto in C (90% dicono), ma nella mia esperienza soggettiva, il codice di sistema tende utilizzare gotos per la gestione degli errori più spesso. Forse nel codice scientifico o in cui il sistema operativo offre la gestione delle eccezioni (Windows), le uscite multilivello sono il caso d'uso dominante. In realtà non forniscono alcun dettaglio sul contesto del loro sondaggio.

Modificato per aggiungere: risulta che questi due schemi di utilizzo si trovano nel libro C di Kernighan e Ritchie, intorno a pagina 60 (a seconda dell'edizione). Un'altra cosa da notare è che entrambi i casi d'uso riguardano solo goto in avanti. E si scopre che l'edizione MISRA C 2012 (a differenza dell'edizione 2004) ora consente goto, purché siano solo avanti.


Destra. L '"elefante nella stanza" è che il kernel Linux, per l'amor del cielo, come esempio di una base di codice critico per il mondo - è caricato con goto. Ovviamente è. Ovviamente. L '"anti-goto-meme" è solo una curiosità di decenni fa. Naturalmente, ci sono un certo numero di cose nella programmazione (in particolare "statica", in effetti "globali" e ad esempio "altro") che possono essere abusati da non professionisti. Quindi, se tuo cugino bambino sta imparando il programma 2, dici loro "oh non usare i globali" e "non usare mai altro".
Fattie,

Il bug goto fail non ha nulla a che fare con goto. Il problema è causato dall'istruzione if senza parentesi graffe in seguito. Quasi ogni copia dell'istruzione incollata due volte lì avrebbe causato un problema. Ritengo che quel tipo di nudo senza bretelle sia molto più dannoso di goto.
muusbolla,

2

Alcuni dicono che non c'è motivo per andare in C ++. Alcuni affermano che nel 99% dei casi ci sono alternative migliori. Questo non è un ragionamento, solo impressioni irrazionali. Ecco un solido esempio in cui goto porta a un bel codice, qualcosa come un ciclo do-while migliorato:

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;

Confrontalo con il codice goto-free:

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;

Vedo queste differenze:

  • {}è necessario il blocco nidificato (anche se do {...} whilesembra più familiare)
  • loopè necessaria una variabile extra , utilizzata in quattro punti
  • ci vuole più tempo per leggere e capire il lavoro con loop
  • il loopnon possiede dati, li controlla il flusso di esecuzione, che è meno comprensibile che semplice etichetta

C'è un altro esempio

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?
  }
}

Ora liberiamoci del "male" 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);
}

Vedi che è lo stesso tipo di utilizzo di goto, è un modello ben strutturato e non è goto in avanti come molti promuovono come l'unico modo consigliato. Sicuramente vuoi evitare un codice "intelligente" come questo:

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
  }
}

Il punto è che goto può essere facilmente abusato, ma Goto stesso non è da biasimare. Si noti che l'etichetta ha l'ambito della funzione in C ++, quindi non inquina l'ambito globale come nell'assemblaggio puro, in cui i circuiti sovrapposti hanno il loro posto e sono molto comuni - come nel codice seguente per 8051, in cui il display a 7 segmenti è collegato a P1. Il programma avvolge il segmento dei fulmini attorno a:

; 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

C'è un altro vantaggio: goto può servire come loop denominati, condizioni e altri flussi:

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)

Oppure puoi usare goto equivalente con rientro, quindi non hai bisogno di commenti se scegli saggiamente il nome dell'etichetta:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;

1

In Perl, usa un'etichetta per "andare" da un ciclo - usando un'istruzione "last", che è simile a break.

Ciò consente un migliore controllo sui loop nidificati.

Anche la tradizionale etichetta goto è supportata, ma non sono sicuro che ci siano troppi casi in cui questo è l'unico modo per ottenere ciò che vuoi: subroutine e loop dovrebbero essere sufficienti per la maggior parte dei casi.


Penso che l'unica forma di goto che tu abbia mai usato in Perl sia goto &subroutine. Che avvia la subroutine con l'attuale @_, sostituendo la subroutine corrente nello stack.
Brad Gilbert,

1

Il problema con "goto" e l'argomento più importante del movimento di "programmazione goto-less" è che se lo usi troppo frequentemente il tuo codice, sebbene possa comportarsi correttamente, diventa illeggibile, non mantenibile, non visualizzabile ecc. Nel 99,99% di il caso 'goto' porta al codice spaghetti. Personalmente, non riesco a pensare a nessuna buona ragione per cui dovrei usare 'goto'.


11
Dire "Non riesco a pensare a nessun motivo" nel tuo argomento è, formalmente, en.wikipedia.org/wiki/Argument_from_ignorance , anche se preferisco la terminologia "prova per mancanza di immaginazione".
SOLO IL MIO OPINIONE corretta,

2
@SOLO IL MIO OPINIONE corretta: è un errore logico solo se viene impiegato post-hoc. Dal punto di vista di un progettista di lingue potrebbe essere un argomento valido valutare il costo dell'inclusione di una funzione ( goto). L'utilizzo di @ cschol è simile: mentre forse non sta progettando una lingua in questo momento, sta fondamentalmente valutando lo sforzo del progettista.
Konrad Rudolph,

1
@KonradRudolph: IMHO, avere un linguaggio consentito gototranne che nei contesti in cui porterebbe all'esistenza delle variabili, potrebbe essere più economico del cercare di supportare ogni tipo di struttura di controllo di cui qualcuno potrebbe aver bisogno. Scrivere codice con gotopotrebbe non essere bello come usare qualche altra struttura, ma essere in grado di scrivere tale codice gotoaiuterà ad evitare "buchi nell'espressività" - costrutti per i quali un linguaggio non è in grado di scrivere codice efficiente.
supercat

1
@supercat Temo che veniamo da scuole fondamentalmente diverse di design del linguaggio. Mi oppongo al fatto che le lingue siano al massimo espressive al prezzo della comprensibilità (o correttezza). Preferirei avere un linguaggio restrittivo piuttosto che permissivo.
Konrad Rudolph,

1
@thb Sì, certo che lo faccio. Spesso rende il codice molto più difficile da leggere e ragionare. Praticamente ogni volta che qualcuno pubblica codice che contiene gotosul sito di revisione del codice, l'eliminazione gotosemplifica enormemente la logica del codice.
Konrad Rudolph,

1

Il GOTO può essere usato, ovviamente, ma c'è una cosa più importante dello stile del codice, o se il codice è o non è leggibile che devi tenere a mente quando lo usi: il codice all'interno potrebbe non essere robusto come te pensa .

Ad esempio, guarda i seguenti due frammenti di codice:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

Un codice equivalente con GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

La prima cosa che pensiamo è che il risultato di entrambi i bit di codice sarà quel "Valore di A: 0" (supponiamo un'esecuzione senza parallelismo, ovviamente)

Non è corretto: nel primo esempio, A sarà sempre 0, ma nel secondo campione (con l'istruzione GOTO) A potrebbe non essere 0. Perché?

Il motivo è perché da un altro punto del programma posso inserire un GOTO FINALsenza controllare il valore di A.

Questo esempio è molto ovvio, ma man mano che i programmi diventano più complicati, aumenta la difficoltà di vedere questo tipo di cose.

Materiale correlato può essere trovato nel famoso articolo di Dijkstra "Un caso contro la dichiarazione GO TO"


6
Questo era spesso il caso del BASIC della vecchia scuola. Nelle varianti moderne, tuttavia, non ti è permesso saltare nel mezzo di un'altra funzione ... o in molti casi, anche oltre la dichiarazione di una variabile. Fondamentalmente (gioco di parole non inteso), le lingue moderne hanno ampiamente eliminato i GOTO "sfrenati" di cui Dijkstra stava parlando ... e l'unico modo per usarlo mentre stava discutendo, è commettere alcuni altri peccati atroci. :)
cHao

1

Uso goto nel seguente caso: quando necessario per tornare da funzioni in luoghi diversi, e prima di tornare è necessario eseguire alcune inizializzazioni:

versione non 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;     
}

vai alla versione:

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;     
}

La seconda versione semplifica, quando è necessario modificare qualcosa nelle istruzioni di deallocazione (ognuna viene utilizzata una volta nel codice) e riduce la possibilità di saltare una di esse quando si aggiunge un nuovo ramo. Spostarli in una funzione non aiuta qui, perché la deallocazione può essere fatta a diversi "livelli".


3
Questo è il motivo per cui abbiamo finallyblocchi in C #
John Saunders,

^ come ha detto @JohnSaunders. Questo è un esempio di "usare goto perché un linguaggio manca del costrutto di controllo appropriato". TUTTAVIA è un odore di codice richiedere MOLTISSIMI punti di goto vicino al ritorno. Esiste uno stile di programmazione più sicuro (più facile da non rovinare) E che non richiede goto, che funziona bene anche nelle lingue senza "finalmente": progettare quelle chiamate "ripulite" in modo che siano innocue da fare quando non pulite è richiesto. Fattorizza tutto tranne la pulizia in un metodo che utilizza il design a ritorno multiplo. Chiamare quel metodo, quindi eseguire le chiamate di pulizia.
ToolmakerSteve

Si noti che l'approccio che descrivo richiede un ulteriore livello di chiamata al metodo (ma solo in una lingua che manca finally). In alternativa, utilizzare gotos, ma per un punto di uscita comune , che esegue sempre tutte le operazioni di pulizia. Ma ogni metodo di pulizia può gestire un valore che è nullo o già pulito, oppure è protetto da un test condizionale, quindi ignorato quando non è appropriato.
ToolmakerSteve

@ToolmakerSteve questo non è un odore di codice; infatti, è un modello estremamente comune in C ed è considerato uno dei modi più validi per usare goto. Vuoi che crei 5 metodi, ognuno con il proprio if-test non necessario, solo per gestire la pulizia da questa funzione? Ora hai creato codice E prestazioni eccessive. O potresti semplicemente usare goto.
muusbolla,

@muusbolla - 1) "crea 5 metodi" - No. Sto suggerendo un singolo metodo che pulisce tutte le risorse che hanno un valore non nullo. O vedi la mia alternativa, che usa gotos che vanno tutti allo stesso punto di uscita, che ha la stessa logica (che richiede un extra se per risorsa, come dici tu). Ma non importa, quando usi Cte hai ragione - qualunque sia la ragione per cui il codice è in C, è quasi certamente un compromesso che favorisce il codice più "diretto". (Il mio suggerimento gestisce situazioni complesse in cui una determinata risorsa può o non può essere stata allocata. Ma sì, in questo caso eccessivo.)
ToolmakerSteve

0

Edsger Dijkstra, un informatico che ha avuto importanti contributi sul campo, era anche famoso per aver criticato l'uso di GoTo. C'è un breve articolo sul suo argomento su Wikipedia .


0

Di tanto in tanto è utile per l'elaborazione delle stringhe in termini di caratteri.

Immagina qualcosa come questo esempio printf-esque:

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
    }
}

Potresti ricondurlo goto handle_literala una chiamata di funzione, ma se sta modificando diverse variabili locali diverse, dovresti passare i riferimenti a ciascuna a meno che la tua lingua non supporti chiusure mutabili. Dovresti ancora usare acontinue un'istruzione (che è probabilmente una forma di goto) dopo la chiamata per ottenere la stessa semantica se la tua logica non fa funzionare un altro caso.

Ho anche usato le foto in modo giudizioso nei lexer, in genere per casi simili. Non hai bisogno di loro per la maggior parte del tempo, ma sono carini da avere per quegli strani casi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.