A che tipo di bug portano le dichiarazioni "goto"? Ci sono esempi storicamente significativi?


103

Capisco che, salvo lo scoppio di loop nidificati in loop; l' gotoaffermazione viene elusa e insultata come uno stile di programmazione soggetto a bug, da non usare mai.

XKCD Testo alternativo : "Neal Stephenson pensa che sia carino nominare le sue etichette" dengo ""
Vedi il fumetto originale su: http://xkcd.com/292/

Perché l'ho imparato presto; Non ho davvero alcuna idea o esperienza su quali tipi di bug gotoeffettivamente portano. Quindi di cosa stiamo parlando qui:

  • Instabilità?
  • Codice non mantenibile o illeggibile?
  • Vulnerabilità di sicurezza?
  • Qualcos'altro interamente?

A che tipo di bug portano effettivamente le dichiarazioni "goto"? Ci sono esempi storicamente significativi?


33
Hai letto il cortometraggio originale di Dijkstra su questo argomento? (Questa è una domanda Sì / No, e qualsiasi risposta diversa da un inequivocabile istante "sì" è un "no".)
John R. Strohm

2
Presumo che l'uscita di allarme sia quando si verifica qualcosa di catastrofico. da dove non tornerai mai più. Come mancanza di corrente, dispositivi o file che scompaiono. Alcuni tipi di "on error goto" ... Ma penso che la maggior parte degli errori "quasi catastrofici" possano essere gestiti al giorno d'oggi dai costrutti "try-catch".
Lenne,

13
Nessuno ha ancora menzionato il GOTO calcolato. Quando la terra era giovane, BASIC aveva numeri di linea. È possibile scrivere quanto segue: 100 N = A * 100; GOTO N. Prova a eseguire il debug :)
mcottle,

7
@ JohnR.Strohm Ho letto il documento di Dijkstra. È stato scritto nel 1968 e suggerisce che la necessità di goto potrebbe essere ridotta in lingue che includono caratteristiche avanzate come istruzioni e funzioni switch. È stato scritto in risposta a lingue in cui goto era il metodo principale di controllo del flusso e poteva essere utilizzato per saltare a qualsiasi punto del programma. Mentre nei linguaggi che sono stati sviluppati dopo la stesura di questo documento, ad esempio C, goto può saltare solo ai posti nello stesso frame dello stack e viene generalmente utilizzato solo quando altre opzioni non funzionano bene. (continua ...)
Ray,

10
(... continua) I suoi punti erano validi al momento, ma non sono più pertinenti, indipendentemente dal fatto che goto sia una buona idea o meno. Se vogliamo sostenere che, in un modo o nell'altro, devono essere fatti argomenti relativi alle lingue moderne. A proposito, hai letto la "Programmazione strutturata con dichiarazioni goto" di Knuth?
Ray,

Risposte:


2

Ecco un modo per vederlo che non ho ancora visto nelle altre risposte.

Si tratta di portata. Uno dei pilastri principali delle buone pratiche di programmazione è mantenere il campo di applicazione ristretto. Hai bisogno di uno spazio ristretto perché non hai la capacità mentale di supervisionare e comprendere più di pochi passaggi logici. Quindi crei piccoli blocchi (ognuno dei quali diventa "una cosa") e poi costruisci con quelli per creare blocchi più grandi che diventano una cosa e così via. Ciò mantiene le cose gestibili e comprensibili.

Un goto gonfia efficacemente l'ambito della tua logica all'intero programma. Questo è sicuro di sconfiggere il tuo cervello per tutti i programmi, tranne quelli più piccoli, che coprono solo poche righe.

Quindi non sarai in grado di dire se hai fatto più un errore perché c'è troppo da prendere e controllare la tua povera piccola testa. Questo è il vero problema, i bug sono solo un probabile risultato.


Vai a non locale?
Deduplicatore

thebrain.mcgill.ca/flash/capsules/experience_jaune03.html << La mente umana può tenere traccia di una media di 7 cose contemporaneamente. La tua logica gioca su questa limitazione, in quanto l'ampliamento dell'ambito aggiungerà molte altre cose di cui è necessario tenere traccia. Pertanto, i tipi di bug che verranno introdotti sono probabilmente omissione e dimenticanza. Offrite anche una strategia attenuante e buone pratiche in generale, di "mantenere il campo di applicazione stretto". Come tale, accetto questa risposta. Buon lavoro Martin.
Akiva,

1
Quant'è fico?! Tra oltre 200 voti espressi, vincendo con solo 1!
Martin Maat,

68

Non è che il gotomale sia di per sé. (Dopo tutto, ogni istruzione di salto in un computer è un goto.) Il problema è che esiste uno stile umano di programmazione che precede la programmazione strutturata, quella che potrebbe essere definita programmazione "diagramma di flusso".

Nella programmazione dei diagrammi di flusso (che la gente della mia generazione ha imparato e che è stata utilizzata per il programma Luna di Apollo) fai un diagramma con blocchi per esecuzioni di dichiarazioni e diamanti per decisioni, e puoi collegarli con linee che vanno dappertutto. (Il cosiddetto "codice spaghetti".)

Il problema con il codice spaghetti è che tu, il programmatore, potresti "sapere" che era giusto, ma come hai potuto dimostrarlo , a te stesso oa chiunque altro? In realtà, potrebbe effettivamente avere un potenziale comportamento scorretto e la tua consapevolezza che è sempre corretta potrebbe essere sbagliata.

È arrivata la programmazione strutturata con blocchi di inizio-fine, per, mentre, if-else e così via. Questi avevano il vantaggio di poter ancora fare qualcosa al loro interno, ma se tu fossi affatto attento, potresti essere sicuro che il tuo codice fosse corretto.

Certo, le persone possono ancora scrivere il codice spaghetti anche senza goto. Il metodo comune è scrivere a while(...) switch( iState ){..., in cui casi diversi impostano iStatevalori diversi. In effetti, in C o C ++ si potrebbe scrivere una macro per farlo, e nominarla GOTO, quindi dire che non si sta usando gotoè una distinzione senza differenza.

Come esempio di come la dimostrazione del codice possa precludere senza restrizioni goto, molto tempo fa mi sono imbattuto in una struttura di controllo utile per modificare in modo dinamico le interfacce utente. L'ho chiamata esecuzione differenziale . È completamente Turing universale, ma la sua prova di correttezza dipende programmazione strutturata pura - senza goto, return, continue, break, o eccezioni.

inserisci qui la descrizione dell'immagine


49
La correttezza non può essere dimostrata se non nel più banale dei programmi, anche quando si usa la programmazione strutturata per scriverli. Puoi solo aumentare il tuo livello di fiducia; la programmazione strutturata lo fa.
Robert Harvey,

23
@MikeDunlavey: Correlati: "Attenzione ai bug nel codice sopra; l'ho solo dimostrato corretto, non provato." staff.fnwi.uva.nl/p.vanemdeboas/knuthnote.pdf
Mooing Duck

26
@RobertHarvey Non è del tutto vero che le prove di correttezza sono pratiche solo per programmi banali. Tuttavia, sono necessari strumenti più specializzati rispetto alla programmazione strutturata.
Mike Haskel,

18
@MSalters: è un progetto di ricerca. L'obiettivo di un tale progetto di ricerca è mostrare che potrebbero scrivere un compilatore verificato se avessero le risorse. Se guardi il loro lavoro attuale, vedrai che semplicemente non sono interessati a supportare le versioni successive di C, piuttosto sono interessati ad espandere le proprietà di correttezza che possono provare. E per questo, è semplicemente irrilevante se supportano C90, C99, C11, Pascal, Fortran, Algol o Plankalkül.
Jörg W Mittag,

8
@martineau: sono sospettoso di quelle "frasi del bosone", in cui i programmatori si mettono in fila concordando sul fatto che è cattivo o buono, perché se non lo fanno si siederanno. Devono esserci ragioni più elementari per le cose.
Mike Dunlavey,

63

Perché è pericoloso?

  • gotonon causa instabilità da solo. Nonostante circa 100.000 gotos, il kernel Linux è ancora un modello di stabilità.
  • gotoda solo non dovrebbe causare vulnerabilità di sicurezza. In alcune lingue, tuttavia, la combinazione con i blocchi di gestione try/ delle catcheccezioni potrebbe comportare vulnerabilità, come spiegato nella presente raccomandazione CERT . I compilatori C ++ mainstream contrassegnano e impediscono tali errori, ma sfortunatamente i compilatori più vecchi o più esotici no.
  • gotocausa codice illeggibile e non mantenibile. Questo è anche chiamato codice spaghetti , perché, come in un piatto di spaghetti, è molto difficile seguire il flusso di controllo quando ci sono troppe goto.

Anche se riesci a evitare il codice spaghetti e se usi solo alcune goto, facilitano comunque bug come e perdite di risorse:

  • Il codice che utilizza la programmazione della struttura, con blocchi nidificati chiari e loop o switch, è facile da seguire; il suo flusso di controllo è molto prevedibile. È quindi più facile garantire il rispetto degli invarianti.
  • Con una gotodichiarazione, interrompi quel flusso diretto e rompi le aspettative. Ad esempio, potresti non notare che devi ancora liberare risorse.
  • Molti gotoin luoghi diversi possono inviarti a un singolo target goto. Quindi non è ovvio sapere con certezza lo stato in cui ti trovi quando raggiungi questo posto. Il rischio di formulare ipotesi errate / infondate è quindi piuttosto grande.

Ulteriori informazioni e preventivi:

C fornisce l' gotoistruzione infinitamente abusabile e le etichette a cui ramificare. Formalmente gotonon è mai necessario, e in pratica è quasi sempre facile scrivere codice senza di esso. (...)
Tuttavia, suggeriremo alcune situazioni in cui i goto possono trovare un posto. L'uso più comune è quello di abbandonare l'elaborazione in alcune strutture profondamente annidate, come la rottura di due loop contemporaneamente. (...)
Anche se non siamo dogmatici sulla questione, sembra che le dichiarazioni goto debbano essere usate con parsimonia, se non del tutto .

  • James Gosling e Henry McGilton hanno scritto nel loro white paper sull'ambiente in linguaggio Java del 1995 :

    Non ci sono più dichiarazioni Goto
    Java non ha dichiarazioni goto. Gli studi hanno dimostrato che il goto è (mis) usato più spesso del semplice "perché è lì". L'eliminazione del goto ha portato a una semplificazione del linguaggio (...) Gli studi su circa 100.000 righe di codice C hanno determinato che circa il 90 percento delle dichiarazioni goto sono state utilizzate puramente per ottenere l'effetto della rottura dei loop nidificati. Come accennato in precedenza, l'interruzione multilivello e continua rimuovono la maggior parte della necessità di dichiarazioni goto.

  • Bjarne Stroustrup definisce goto nel suo glossario in questi termini invitanti:

    goto - il famigerato goto. Utile principalmente nel codice C ++ generato dalla macchina.

Quando si può usare Goto?

Come K&R, non sono dogmatico di goto. Ammetto che ci sono situazioni in cui goto potrebbe essere di facilitare la propria vita.

In genere, in C, goto consente l'uscita del loop multilivello o la gestione degli errori che richiede di raggiungere un punto di uscita appropriato che libera / sblocca tutte le risorse che sono state allocate finora (allocazione multipla in sequenza significa più etichette). Questo articolo quantifica i diversi usi di goto nel kernel di Linux.

Personalmente preferisco evitarlo e in 10 anni di C ho usato massimo 10 goto. Preferisco usare ifs nidificati , che penso siano più leggibili. Quando ciò porterebbe a un annidamento troppo profondo, sceglierei di decomporre la mia funzione in parti più piccole o di utilizzare un indicatore booleano in cascata. I compilatori di ottimizzazione di oggi sono abbastanza intelligenti da generare quasi lo stesso codice dello stesso codice con goto.

L'uso di goto dipende fortemente dalla lingua:

  • In C ++, l'uso corretto di RAII fa sì che il compilatore distrugga automaticamente gli oggetti che escono dall'ambito, in modo che le risorse / il blocco vengano comunque ripuliti e che non sia più necessario andare a goto.

  • In Java non c'è alcun bisogno di goto (vedi citazione autore di Java sopra e questa eccellente risposta Stack Overflow ): il garbage collector che pulisce il disordine, break, continue, e try/ catchgestione delle eccezioni coprire tutto il caso in cui gotopotrebbe essere utile, ma in un più sicuro e migliore maniera. La popolarità di Java dimostra che la dichiarazione goto può essere evitata in un linguaggio moderno.

Ingrandisci la famosa vulnerabilità SSL goto fail

Importante dichiarazione di non responsabilità: in considerazione della feroce discussione nei commenti, voglio chiarire che non pretendo che la dichiarazione goto sia l'unica causa di questo errore. Non pretendo che senza goto non ci sarebbe nessun bug. Voglio solo mostrare che un goto può essere coinvolto in un bug grave.

Non so a quanti gravi bug siano collegati gotonella storia della programmazione: i dettagli spesso non vengono comunicati. Tuttavia, c'era un famoso bug Apple SSL che indeboliva la sicurezza di iOS. L'affermazione che ha portato a questo errore è stata gotoun'affermazione errata .

Alcuni sostengono che la causa principale del bug non sia stata l'istruzione goto in sé, ma una copia / incolla errata, un rientro fuorviante, la mancanza di parentesi graffe attorno al blocco condizionale o forse le abitudini di lavoro dello sviluppatore. Non posso né confermarne nessuno: tutti questi argomenti sono ipotesi e interpretazioni probabili. Nessuno lo sa davvero. ( nel frattempo, l'ipotesi di una fusione che è andata storta come qualcuno ha suggerito nei commenti sembra essere un ottimo candidato in vista di altre incongruenze di rientro nella stessa funzione ).

L'unico fatto oggettivo è che un duplicato ha gotoportato all'uscita prematura della funzione. Guardando il codice, l'unica altra singola istruzione che avrebbe potuto causare lo stesso effetto sarebbe stata un ritorno.

L'errore è in funzione SSLEncodeSignedServerKeyExchange()in questo file :

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
        goto fail;
    if ((err =...) !=0)
        goto fail;
    if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
        goto fail;
        goto fail;  // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
    if (...)
        goto fail;

    ... // Do some cryptographic operations here

fail:
    ... // Free resources to process error

In effetti le parentesi graffe attorno al blocco condizionale avrebbero potuto prevenire l'errore:
avrebbe portato a un errore di sintassi durante la compilazione (e quindi a una correzione) o a un goto innocuo ridondante. A proposito, GCC 6 sarebbe in grado di individuare questi errori grazie al suo avviso opzionale per rilevare rientri incoerenti.

Ma in primo luogo, tutte queste goto avrebbero potuto essere evitate con un codice più strutturato. Quindi goto è almeno indirettamente una causa di questo errore. Esistono almeno due modi diversi che avrebbero potuto evitarlo:

Approccio 1: se clausola o nidificati ifs

Invece di testare molte condizioni per errori in sequenza, e ogni volta che si invia a failun'etichetta in caso di problemi, si potrebbe optare per l'esecuzione delle operazioni crittografiche in una ifdichiarazione che lo farebbe solo se non ci fosse una pre-condizione errata:

    if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
        (err = ...) == 0 ) &&
        (err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
        ...
        (err = ...) == 0 ) )
    {
         ... // Do some cryptographic operations here
    }
    ... // Free resources

Approccio 2: utilizzare un accumulatore di errori

Questo approccio si basa sul fatto che quasi tutte le istruzioni qui richiamano una funzione per impostare un errcodice di errore ed eseguono il resto del codice solo se errera 0 (ovvero, funzione eseguita senza errore). Una buona alternativa sicura e leggibile è:

bool ok = true;
ok =  ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok =  ok && (err = NextFunction(...)) == 0;
...
ok =  ok && (err = ...) == 0;
... // Free resources

Qui, non c'è un singolo goto: nessun rischio di saltare rapidamente al punto di uscita del guasto. E visivamente sarebbe facile individuare una linea disallineata o dimenticata ok &&.

Questo costrutto è più compatto. Si basa sul fatto che in C, la seconda parte di un logico e ( &&) viene valutata solo se la prima parte è vera. In effetti, l'assemblatore generato da un compilatore ottimizzatore è quasi equivalente al codice originale con gotos: l'ottimizzatore rileva molto bene la catena di condizioni e genera codice, che al primo valore di ritorno non nullo salta alla fine ( prova online ).

Si potrebbe anche prevedere un controllo di coerenza al termine della funzione che durante la fase di test potrebbe identificare discrepanze tra il flag ok e il codice di errore.

assert( (ok==false && err!=0) || (ok==true && err==0) );

Errori di questo tipo ==0sostituiti inavvertitamente con errori di un !=0connettore logico o sarebbero facilmente individuati durante la fase di debug.

Come detto: non pretendo che costrutti alternativi avrebbero evitato qualsiasi errore. Voglio solo dire che avrebbero potuto rendere più difficile il verificarsi del bug.


34
No, il bug di Apple goto fail è stato causato da un abile programmatore che ha deciso di non includere le parentesi graffe dopo un'istruzione if. :-) Quindi, quando è arrivato il prossimo programmatore di manutenzione (o la stessa, stessa differenza) e ha aggiunto un'altra riga di codice che avrebbe dovuto essere eseguito solo quando l'istruzione if ha valutato true, l'istruzione è stata eseguita ogni volta. Bam, il bug "goto fail", che era un grosso bug di sicurezza. Ma essenzialmente non aveva nulla a che fare con il fatto che fosse usata un'istruzione goto.
Craig,

47
Il bug "goto fail" di Apple è stato causato direttamente da una riga di codice duplicata. Il particolare effetto di quel bug è stato causato dal fatto che quella linea è una frase gotointerna senza parentesi graffa if. Ma se stai suggerendo che evitare gotorenderà il tuo codice immune agli effetti di linee duplicate casualmente, ho brutte notizie per te.
user253751

12
Stai usando il comportamento dell'operatore booleano di scorciatoia per eseguire un'istruzione attraverso l'effetto collaterale e affermando che è più chiaro di ifun'istruzione? Momenti divertenti. :)
Craig,

38
Se invece di "goto fail" ci fosse stata un'affermazione "fail = true" sarebbe accaduta esattamente la stessa cosa.
gnasher729,

15
Tale errore è stato causato da uno sviluppatore sciatto che non prestava attenzione a ciò che stava facendo e non leggeva le proprie modifiche al codice. Niente di più, niente di meno. Okay, magari potresti anche fare delle prove.
Razze di leggerezza in orbita

34

Il famoso articolo Dijkstra è stato scritto in un momento in cui alcuni linguaggi di programmazione erano effettivamente in grado di creare subroutine con più punti di entrata e uscita. In altre parole, potresti letteralmente saltare nel mezzo di una funzione e saltare in qualsiasi punto all'interno della funzione, senza effettivamente chiamare quella funzione o tornare da essa in modo convenzionale. Questo è ancora vero per il linguaggio assembly. Nessuno sostiene mai che un simile approccio sia superiore al metodo strutturato di scrittura del software che ora utilizziamo.

Nella maggior parte dei linguaggi di programmazione moderni, le funzioni sono definite in modo molto specifico con un punto di entrata e un punto di uscita. Il punto di ingresso è il punto in cui si specificano i parametri per la funzione e la si chiama, mentre il punto di uscita è il punto in cui si restituisce il valore risultante e si continua l'esecuzione sull'istruzione seguendo la chiamata di funzione originale.

All'interno di quella funzione, dovresti essere in grado di fare ciò che desideri, entro limiti ragionevoli. Se mettere una o due funzioni nella funzione rende più chiara o migliora la tua velocità, perché no? Il punto centrale di una funzione è sequestrare un po 'di funzionalità chiaramente definite, in modo da non dover più pensare a come funziona internamente. Una volta scritto, lo usi e basta.

E sì, puoi avere più dichiarazioni di ritorno all'interno di una funzione; c'è sempre un posto in una funzione corretta da cui si ritorna (la parte posteriore della funzione, in sostanza). Non è affatto la stessa cosa che saltare fuori da una funzione con un goto prima che abbia la possibilità di tornare correttamente.

inserisci qui la descrizione dell'immagine

Quindi non si tratta davvero di usare goto. Si tratta di evitare il loro abuso. Tutti concordano sul fatto che puoi creare un programma terribilmente contorto usando goto, ma puoi fare lo stesso anche abusando delle funzioni (è solo molto più facile abusare di goto).

Per quello che vale, da quando mi sono laureato da programmi BASIC in stile numero di riga alla programmazione strutturata usando i linguaggi Pascal e parentesi graffe, non ho mai dovuto usare un goto. L'unica volta che sono stato tentato di usarne uno è fare un'uscita anticipata da un ciclo nidificato (in lingue che non supportano l'uscita anticipata multilivello dai cicli), ma di solito riesco a trovare un altro modo più pulito.


2
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

11

A che tipo di bug portano le dichiarazioni "goto"? Ci sono esempi storicamente significativi?

Usavo le gotodichiarazioni quando scrivevo i programmi BASIC da bambino come un modo semplice per ottenere e mentre i cicli (Commodore 64 BASIC non aveva cicli while, ed ero troppo immaturo per imparare la sintassi e l'uso corretti dei cicli for ). Il mio codice era spesso banale, ma qualsiasi bug del loop poteva essere immediatamente attribuito al mio uso di goto.

Ora uso principalmente Python, un linguaggio di programmazione di alto livello che ha determinato che non ha bisogno di andare a Goto.

Quando Edsger Dijkstra dichiarò "Goto considerato dannoso" nel 1968, non fornì una manciata di esempi in cui i bug relativi potevano essere biasimati sul goto, piuttosto, dichiarò che nongoto era necessario per le lingue di livello superiore e che doveva essere evitato a favore di ciò che oggi consideriamo il normale flusso di controllo: loop e condizionali. Le sue parole:

L'uso sfrenato del go to ha una conseguenza immediata che diventa terribilmente difficile trovare un insieme significativo di coordinate in cui descrivere l'avanzamento del processo.
[...]
La dichiarazione go to com'è è troppo primitiva; è troppo un invito a rovinare il proprio programma.

Probabilmente aveva montagne di esempi di bug da ogni volta che eseguiva il debug del codice goto. Ma il suo articolo era una dichiarazione di posizione generalizzata sostenuta da prove che gotonon erano necessarie per le lingue di livello superiore. Il bug generalizzato è che potresti non avere la possibilità di analizzare staticamente il codice in questione.

Il codice è molto più difficile da analizzare staticamente con le gotoistruzioni, specialmente se si fa un salto indietro nel flusso di controllo (cosa che ero solito fare) o in una sezione di codice non correlata. Il codice può ed è stato scritto in questo modo. È stato fatto per ottimizzare al massimo risorse di calcolo molto scarse e quindi le architetture delle macchine.

Un esempio apocrifo

Esisteva un programma di blackjack che un manutentore trovava essere ottimizzato in modo abbastanza elegante ma anche impossibile da "correggere" a causa della natura del codice. È stato programmato in un codice macchina che dipende fortemente dalle foto, quindi penso che questa storia sia abbastanza rilevante. Questo è il miglior esempio canonico a cui riesco a pensare.

Un contro-esempio

Tuttavia, l'origine C di CPython (l'implementazione Python più comune e di riferimento) utilizza le gotoistruzioni con grande efficacia. Sono utilizzati per bypassare il flusso di controllo irrilevante all'interno delle funzioni per arrivare alla fine delle funzioni, rendendo le funzioni più efficienti senza perdere la leggibilità. Questo rispetta uno degli ideali della programmazione strutturata : avere un unico punto di uscita per le funzioni.

Per la cronaca, trovo che questo contro-esempio sia abbastanza facile da analizzare staticamente.


5
La "Storia di Mel" che ho incontrato riguardava una strana architettura in cui (1) ogni istruzione del linguaggio macchina esegue un goto dopo il suo funzionamento e (2) era terribilmente inefficiente posizionare una sequenza di istruzioni in posizioni di memoria consecutive. E il programma è stato scritto in assembly ottimizzato a mano, non in un linguaggio compilato.

@MilesRout Penso che potresti essere corretto qui, ma la pagina di Wikipedia sulla programmazione strutturata dice: "La deviazione più comune, trovata in molte lingue, è l'uso di una dichiarazione di ritorno per l'uscita anticipata da una subroutine. Ciò comporta più punti di uscita , anziché il singolo punto di uscita richiesto dalla programmazione strutturata ". - stai dicendo che non è corretto? Hai una fonte da citare?
Aaron Hall,

@AaronHall Questo non è il significato originale.
Miles Rout,

11

Quando il wigwam era l'arte architettonica attuale, i suoi costruttori potevano senza dubbio darti solidi consigli pratici sulla costruzione di wigwam, su come far uscire il fumo e così via. Fortunatamente, i costruttori di oggi possono probabilmente dimenticare gran parte di quel consiglio.

Quando la diligenza era l'attuale arte dei trasporti, i loro conducenti potevano senza dubbio darti consigli pratici su cavalli di diligenza, su come difendersi dagli uomini delle strade e così via. Fortunatamente, gli automobilisti di oggi possono dimenticare anche gran parte di quel consiglio.

Quando le schede perforate erano l'arte della programmazione attuale, i loro praticanti potevano anche darti dei buoni consigli pratici sull'organizzazione delle carte, su come numerare le dichiarazioni e così via. Non sono sicuro che questo consiglio sia molto rilevante oggi.

Sei abbastanza grande persino per conoscere la frase "numerare le dichiarazioni" ? Non è necessario saperlo, ma in caso contrario, non si ha familiarità con il contesto storico in cui l'avvertimento nei confronti gotoera principalmente rilevante.

L'avvertimento contro gotonon è molto rilevante oggi. Con una formazione di base su while / for loop e chiamate di funzione, non penserai nemmeno di emetterne una gotomolto spesso. Quando ci pensi, probabilmente hai un motivo, quindi vai avanti.

Ma l' gotoaffermazione non può essere abusata?

Risposta: certo, può essere abusato, ma il suo abuso è un problema piuttosto piccolo nell'ingegneria del software rispetto ad errori molto, molto più comuni come l'uso di una variabile dove servirà una costante, o come la programmazione taglia e incolla (altrimenti noto come abbandono al refattore). Dubito che tu sia in grande pericolo. A meno che non si stia utilizzando longjmpo altrimenti trasferendo il controllo a un codice remoto, se si pensa di utilizzare un gotoo si desidera semplicemente provarlo per divertimento, andare avanti. Starai bene.

Potresti notare la mancanza di recenti storie dell'orrore in cui gotointerpreta il cattivo. La maggior parte o tutte queste storie sembrano avere 30 o 40 anni. Ti trovi su un terreno solido se consideri quelle storie per lo più obsolete.


1
Vedo che ho attratto almeno un downvote. Bene, questo è prevedibile. Possono arrivare altri downvotes. Non darò la risposta convenzionale, tuttavia, quando decenni di esperienza di programmazione mi hanno insegnato che, in questo caso, la risposta convenzionale sembra essere sbagliata. Questa è la mia opinione, in ogni caso. Ho dato le mie ragioni. Il lettore può decidere.
thb

1
Il tuo post è troppo bello, non posso sottovalutare i commenti.
Pieter B,

7

Per aggiungere una cosa alle altre risposte eccellenti, con le gotodichiarazioni può essere difficile dire esattamente come sei arrivato a un determinato posto nel programma. Puoi sapere che si è verificata un'eccezione su una riga specifica, ma se ci sono gotodei codici non c'è modo di dire quali istruzioni eseguite per provocare lo stato che causa questa eccezione senza cercare l'intero programma. Non esiste uno stack di chiamate e nessun flusso visivo. È possibile che ci sia un'istruzione a 1000 righe di distanza che ti mette in uno stato cattivo eseguito gotoa alla riga che ha sollevato un'eccezione.


1
Visual Basic 6 e VBA hanno una gestione degli errori dolorosa, ma se si utilizza On Error Goto errh, è possibile utilizzare Resumeper riprovare, Resume Nextsaltare la linea offensiva e Erlconoscere quale linea era (se si utilizzano i numeri di riga).
Cees Timmerman,

@CeesTimmerman Ho fatto ben poco con Visual Basic, non lo sapevo. Aggiungerò un commento al riguardo.
qfwfq,

2
@CeesTimmerman: La semantica VB6 di "resume" è orribile se si verifica un errore all'interno di una subroutine che non ha il proprio blocco on-error. Resumeeseguirà nuovamente quella funzione dall'inizio e Resume Nextsalterà tutto nella funzione che non ha ancora.
supercat,

2
@supercat Come ho detto, è doloroso. Se è necessario conoscere la riga esatta, è necessario numerarli tutti o disabilitare temporaneamente la gestione degli errori con On Error Goto 0e manualmente il debug. Resumedeve essere utilizzato dopo aver verificato Err.Numbere apportato le modifiche necessarie.
Cees Timmerman,

1
Dovrei notare che in un linguaggio moderno, gotofunziona solo con linee etichettate, che sembrano essere state sostituite da anelli etichettati .
Cees Timmerman,

7

goto è più difficile da ragionare per gli umani rispetto ad altre forme di controllo del flusso.

La programmazione del codice corretto è difficile. Scrivere programmi corretti è difficile, determinare se i programmi sono corretti è difficile, provare i programmi corretti è difficile.

Ottenere il codice per fare vagamente quello che vuoi è facile rispetto a tutto il resto della programmazione.

goto risolve alcuni programmi ottenendo che il codice faccia quello che vuoi. Non aiuta a rendere più facile il controllo della correttezza, mentre le sue alternative spesso lo fanno.

Esistono stili di programmazione in cui goto è la soluzione appropriata. Esistono persino stili di programmazione in cui il suo gemello malvagio, proveniente dal mondo, è la soluzione appropriata. In entrambi questi casi, è necessario prestare la massima attenzione per assicurarsi che lo si stia utilizzando in un modello compreso e un sacco di controlli manuali che non fanno qualcosa di difficile per garantirne la correttezza.

Ad esempio, esiste una caratteristica di alcune lingue chiamata coroutine. Le coroutine sono fili senza filo; stato di esecuzione senza thread su cui eseguirlo. Puoi chiedere loro di eseguire, e possono eseguire parte di se stessi e quindi sospendersi, restituendo il controllo del flusso.

Le coroutine "superficiali" in lingue senza supporto coroutine (come C ++ pre-C ++ 20 e C) sono possibili usando una combinazione di goto e gestione manuale dello stato. Le coroutine "profonde" possono essere eseguite utilizzando le funzioni setjmpe longjmpdi C.

Ci sono casi in cui le coroutine sono così utili che vale la pena scriverle manualmente e con attenzione.

Nel caso del C ++, sono stati trovati abbastanza utili da estendere il linguaggio per supportarli. La gotogestione manuale e manuale dello stato viene nascosta dietro un livello di astrazione a costo zero, consentendo ai programmatori di scriverli senza la difficoltà di dover dimostrare che il loro disordine di goto, void**costruzione manuale / distruzione di stato, ecc. È corretto.

Il goto viene nascosto dietro un'astrazione di livello superiore, come whileo foro ifo switch. Quelle astrazioni di livello superiore sono più facili da dimostrare corrette e da controllare.

Se alla lingua mancavano alcuni di essi (come in alcune lingue moderne mancano le coroutine), zoppicare insieme a uno schema che non si adatta al problema o usare goto, diventa la tua alternativa.

Ottenere un computer per fare vagamente quello che vuoi che faccia nei casi comuni è facile . Scrivere un codice affidabile è difficile . Goto aiuta il primo molto più di quanto non aiuti il ​​secondo; quindi "goto considerato dannoso", in quanto è un segno di codice superficialmente "funzionante" con bug profondi e difficili da rintracciare. Con sufficiente sforzo è ancora possibile creare codice con "goto" affidabile in modo affidabile, quindi mantenere la regola di come assoluto è sbagliato; ma come regola generale, è buono.


1
longjmpè legale in C solo per il riavvolgimento dello stack setjmpin una funzione genitore. (Come uscire da più livelli di annidamento, ma per chiamate di funzione anziché loop). longjjmpa un contesto da setjmpfatto a una funzione che da allora è tornata non è legale (e sospetto che nella maggior parte dei casi non funzionerà in pratica). Non ho familiarità con le routine condivise, ma dalla tua descrizione di una routine comune simile a un thread dello spazio utente, penso che avrebbe bisogno del suo stack e di un contesto di registro salvato e longjmpti dà solo il registro -salvare, non uno stack separato.
Peter Cordes,

1
Oh, avrei dovuto cercare su Google : fanf.livejournal.com/105413.html descrive come creare lo spazio dello stack per l'esecuzione della coroutine. (Che come sospettavo, è necessario solo usando setjmp / longjmp).
Peter Cordes,

2
@PeterCordes: le implementazioni non sono necessarie per fornire alcun mezzo con cui il codice possa creare una coppia jmp_buff adatta per l'uso come coroutine. Alcune implementazioni specificano un mezzo attraverso il quale il codice può crearne uno anche se lo Standard non richiede loro di fornire tale capacità. Non descriverei un codice che si basa su tecniche come "standard C", poiché in molti casi un jmp_buff sarà una struttura opaca che non garantisce il comportamento coerente tra i diversi compilatori.
supercat

6

Dai un'occhiata a questo codice, da http://www-personal.umich.edu/~axe/research/Software/CC/CC2/TourExec1.1.f.html che in realtà faceva parte di una grande simulazione del dilemma del prigioniero. Se hai visto il vecchio codice FORTRAN o BASIC, ti renderai conto che non è così insolito.

C  Not nice rules in second round of tour (cut and pasted 7/15/93)
   FUNCTION K75R(J,M,K,L,R,JA)
C  BY P D HARRINGTON
C  TYPED BY JM 3/20/79
   DIMENSION HIST(4,2),ROW(4),COL(2),ID(2)
   K75R=JA       ! Added 7/32/93 to report own old value
   IF (M .EQ. 2) GOTO 25
   IF (M .GT. 1) GOTO 10
   DO 5 IA = 1,4
     DO 5 IB = 1,2
5  HIST(IA,IB) = 0

   IBURN = 0
   ID(1) = 0
   ID(2) = 0
   IDEF = 0
   ITWIN = 0
   ISTRNG = 0
   ICOOP = 0
   ITRY = 0
   IRDCHK = 0
   IRAND = 0
   IPARTY = 1
   IND = 0
   MY = 0
   INDEF = 5
   IOPP = 0
   PROB = .2
   K75R = 0
   RETURN

10 IF (IRAND .EQ. 1) GOTO 70
   IOPP = IOPP + J
   HIST(IND,J+1) = HIST(IND,J+1) + 1
   IF (M .EQ. 15 .OR. MOD(M,15) .NE. 0 .OR. IRAND .EQ. 2) GOTO 25
   IF (HIST(1,1) / (M - 2) .GE. .8) GOTO 25
   IF (IOPP * 4 .LT. M - 2 .OR. IOPP * 4 .GT. 3 * M - 6) GOTO 25
   DO 12 IA = 1,4
12 ROW(IA) = HIST(IA,1) + HIST(IA,2)

   DO 14 IB = 1,2
     SUM = .0
     DO 13 IA = 1,4
13   SUM = SUM + HIST(IA,IB)
14 COL(IB) = SUM

   SUM = .0
   DO 16 IA = 1,4
     DO 16 IB = 1,2
       EX = ROW(IA) * COL(IB) / (M - 2)
       IF (EX .LE. 1.) GOTO 16
       SUM = SUM + ((HIST(IA,IB) - EX) ** 2) / EX
16 CONTINUE

   IF (SUM .GT. 3) GOTO 25
   IRAND = 1
   K75R = 1
   RETURN

25 IF (ITRY .EQ. 1 .AND. J .EQ. 1) IBURN = 1
   IF (M .LE. 37 .AND. J .EQ. 0) ITWIN = ITWIN + 1
   IF (M .EQ. 38 .AND. J .EQ. 1) ITWIN = ITWIN + 1
   IF (M .GE. 39 .AND. ITWIN .EQ. 37 .AND. J .EQ. 1) ITWIN = 0
   IF (ITWIN .EQ. 37) GOTO 80
   IDEF = IDEF * J + J
   IF (IDEF .GE. 20) GOTO 90
   IPARTY = 3 - IPARTY
   ID(IPARTY) = ID(IPARTY) * J + J
   IF (ID(IPARTY) .GE. INDEF) GOTO 78
   IF (ICOOP .GE. 1) GOTO 80
   IF (M .LT. 37 .OR. IBURN .EQ. 1) GOTO 34
   IF (M .EQ. 37) GOTO 32
   IF (R .GT. PROB) GOTO 34
32 ITRY = 2
   ICOOP = 2
   PROB = PROB + .05
   GOTO 92

34 IF (J .EQ. 0) GOTO 80
   GOTO 90

70 IRDCHK = IRDCHK + J * 4 - 3
   IF (IRDCHK .GE. 11) GOTO 75
   K75R = 1
   RETURN

75 IRAND = 2
   ICOOP = 2
   K75R = 0
   RETURN

78 ID(IPARTY) = 0
   ISTRNG = ISTRNG + 1
   IF (ISTRNG .EQ. 8) INDEF = 3
80 K75R = 0
   ITRY = ITRY - 1
   ICOOP = ICOOP - 1
   GOTO 95

90 ID(IPARTY) = ID(IPARTY) + 1
92 K75R = 1
95 IND = 2 * MY + J + 1
   MY = K75R
   RETURN
   END

Ci sono molti problemi qui che vanno ben oltre la dichiarazione GOTO qui; Sinceramente penso che l'affermazione GOTO sia stata un po 'un capro espiatorio. Ma il flusso di controllo non è assolutamente chiaro qui, e il codice è mescolato insieme in modi che rendono molto poco chiaro cosa sta succedendo. Anche senza aggiungere commenti o utilizzare nomi di variabili migliori, cambiarlo in una struttura a blocchi senza GOTO renderebbe molto più facile leggere e seguire.


4
così vero :-) Vale la pena ricordare: l'eredità FORTAN e BASIC non aveva strutture a blocchi, quindi se una clausola THEN non poteva contenere su una riga, GOTO era l'unica alternativa.
Christophe,

Le etichette di testo anziché i numeri, o almeno i commenti sulle righe numerate, sarebbero di grande aiuto.
Cees Timmerman,

Nei miei giorni FORTRAN-IV: ho limitato il mio uso di GOTO per implementare blocchi IF / ELSE IF / ELSE, cicli WHILE e BREAK e NEXT nei loop. Qualcuno mi ha mostrato i mali del codice spaghetti e perché dovresti strutturarlo per evitarlo anche se devi implementare le tue strutture a blocchi con GOTO. Più tardi ho iniziato a utilizzare un pre-processore (RATFOR, IIRC). Goto non è intrinsecamente malvagio, è solo un costrutto di basso livello troppo potente che si associa a un'istruzione di ramo dell'assemblatore. Se devi usarlo perché il tuo linguaggio è carente, usalo per costruire o aumentare le strutture a blocchi appropriate.
nigel222,

@CeesTimmerman: questo è il codice FORTRAN IV di varietà da giardino. Le etichette di testo non erano supportate in FORTRAN IV. Non so se le versioni più recenti della lingua li supportino. I commenti sulla stessa riga del codice non erano supportati nello standard FORTRAN IV, anche se potrebbero esserci state estensioni specifiche del fornitore per supportarli. Non so cosa dice lo standard "attuale" FORTRAN: ho abbandonato FORTRAN molto tempo fa e non mi manca. (Il codice FORTRAN più recente che ho visto ha una forte somiglianza con il PASCAL dei primi anni '70).
John R. Strohm,

1
@CeesTimmerman: estensione specifica del fornitore. Il codice in generale è DAVVERO timido nei commenti. Inoltre, c'è una convenzione che è diventata comune in alcuni luoghi, di mettere i numeri di riga solo sulle istruzioni CONTINUE e FORMAT, per rendere più semplice l'aggiunta di righe in un secondo momento. Questo codice non segue quella convenzione.
John R. Strohm,

0

Uno dei principi della programmazione sostenibile è l' incapsulamento . Il punto è che si interfaccia con un modulo / routine / subroutine / componente / oggetto utilizzando un'interfaccia definita e solo quell'interfaccia e i risultati saranno prevedibili (supponendo che l'unità sia stata effettivamente testata).

All'interno di una singola unità di codice, si applica lo stesso principio. Se applichi coerentemente principi di programmazione strutturata o di programmazione orientata agli oggetti, non dovrai:

  1. creare percorsi imprevisti attraverso il codice
  2. arriva in sezioni di codice con valori variabili indefiniti o non ammessi
  3. uscire da un percorso del codice con valori variabili non definiti o non consentiti
  4. impossibile completare un'unità di transazione
  5. lasciare parti di codice o dati in memoria che sono effettivamente morti, ma non idonei per la pulizia e la riallocazione dell'immondizia, perché non è stato segnalato il loro rilascio utilizzando un costrutto esplicito che li invoca rilasciandolo quando il percorso di controllo del codice esce dall'unità

Alcuni dei sintomi più comuni di questi errori di elaborazione includono perdite di memoria, accumulo di memoria, overflow dei puntatori, arresti anomali, record di dati incompleti, eccezioni di aggiunta / modifica / eliminazione dei record, errori della pagina di memoria e così via.

Le manifestazioni osservabili dall'utente di questi problemi includono il blocco dell'interfaccia utente, la riduzione graduale delle prestazioni, i set di dati incompleti, l'impossibilità di avviare o completare transazioni, dati danneggiati, interruzioni di rete, interruzioni dell'alimentazione, guasti ai sistemi incorporati (dalla perdita del controllo dei missili alla perdita di capacità di tracciamento e controllo nei sistemi di controllo del traffico aereo), guasti del timer e così via e così via. Il catalogo è molto vasto.


3
Hai risposto senza menzionare gotonemmeno una volta. :)
Robert Harvey,

0

goto è pericoloso perché di solito viene utilizzato dove non è necessario. Utilizzando tutto ciò che non è necessario è pericoloso, ma goto in particolare. Se vai su Google troverai molti errori causati da goto, questo non è di per sé un motivo per non usarlo (i bug si verificano sempre quando usi le funzionalità del linguaggio perché è intrinseco nella programmazione), ma alcuni di essi sono chiaramente altamente correlati a goto utilizzo.


Motivi per usare / non usare goto :

  • Se hai bisogno di un ciclo, devi usare whileo for.

  • Se è necessario eseguire il salto condizionale, utilizzare if/then/else

  • Se è necessaria una procedura, chiamare una funzione / metodo.

  • Se hai bisogno di uscire da una funzione, basta return.

Posso contare sui miei punti delle dita dove ho visto gotousato e usato correttamente.

  • CPython
  • libKTX
  • probabilmente pochi altri

In libKTX c'è una funzione che ha il seguente codice

if(something)
    goto cleanup;

if(bla)
    goto cleanup;

cleanup:
    delete [] array;

Ora in questo posto gotoè utile perché la lingua è C:

  • siamo dentro una funzione
  • non possiamo scrivere una funzione di pulizia (perché entriamo in un altro ambito e rendere lo stato della funzione chiamante accessibile è più oneroso)

Questo caso d'uso è utile perché in C non abbiamo classi, quindi il modo più semplice per ripulire è usare a goto.

Se avessimo lo stesso codice in C ++ non è più necessario goto:

class MyClass{
    public:

    void Function(){
        if(something)
            return Cleanup(); // invalid syntax in C#, but valid in C++
        if(bla)
            return Cleanup(); // invalid syntax in C#, but valid in C++
    }

    // access same members, no need to pass state (compiler do it for us).
    void Cleanup(){

    }



}

A che tipo di bug può portare? Nulla. Anelli infiniti, ordine di esecuzione errato, vite impilata.

Un caso documentato è una vulnerabilità in SSL che ha permesso a Man in the middle attacchi causati da un uso errato di goto: ecco l'articolo

Questo è un errore di battitura, ma è passato inosservato per un po ', se il codice fosse strutturato in un altro modo, testato correttamente tale errore non sarebbe stato possibile.


2
Ci sono molti commenti a una risposta che spiegano molto chiaramente che l'uso di "goto" era totalmente irrilevante per il bug SSL: il bug era causato da una duplicazione trascurata di una riga di codice, quindi una volta veniva eseguita in modo condizionale e una volta incondizionatamente.
gnasher729,

Sì, sto aspettando un migliore esempio storico di un bug goto prima di accettare. Come detto; Non avrebbe davvero importanza quello che c'era su quella riga di codice; La mancanza di parentesi graffe avrebbe eseguito e causato un errore a prescindere. Sono curioso però; che cos'è una "Stack Screw"?
Akiva,

1
Un esempio dal codice su cui ho lavorato in precedenza in PL / 1 CICS. Il programma pubblica una schermata composta da mappe a linea singola. Quando lo schermo è stato restituito ha trovato la matrice di mappe che aveva inviato. Utilizzato gli elementi di quello per trovare l'indice in un array, quindi ha usato quell'indice in un indice di variabili di array per fare un GOTO in modo che potesse elaborare quella singola mappa. Se la matrice di mappe inviate era errata (o era stata corrotta), poteva provare a eseguire un GOTO sull'etichetta sbagliata, o peggio ancora, perché accedeva a un elemento dopo la fine della matrice di variabili dell'etichetta. Riscritto per utilizzare la sintassi del tipo di caso.
Calcio d'
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.