Cosa c'è di così male con goto quando viene usato per questi casi ovvi e rilevanti?

36

Ho sempre saputo che goto è qualcosa di brutto, chiuso in uno scantinato da qualche parte che non si vede mai per sempre, ma oggi mi sono imbattuto in un esempio di codice che ha perfettamente senso usare goto .

Ho un IP in cui ho bisogno di verificare se è all'interno di un elenco di IP e quindi procedere con il codice, altrimenti lanciare un'eccezione.

<?php

$ip = '192.168.1.5';
$ips = [
    '192.168.1.3',
    '192.168.1.4',
    '192.168.1.5',
];

foreach ($ips as $i) {
    if ($ip === $i) {
        goto allowed;
    }
}

throw new Exception('Not allowed');

allowed:

...

Se non utilizzo goto allora devo usare alcune variabili come

$allowed = false;

foreach ($ips as $i) {
    if ($ip === $i) {
        $allowed = true;
        break;
    }
}

if (!$allowed) {
    throw new Exception('Not allowed');
}

La mia domanda è: cosa c'è di così male con goto quando viene utilizzato per casi così ovvi e rilevanti?

    
posta php_nub_qq 25.08.2016 - 12:33
fonte

10 risposte

117

GOTO stesso non è un problema immediato, sono le macchine implicite dello stato che le persone tendono ad implementare con esso. Nel tuo caso, vuoi il codice che controlla se l'indirizzo IP è nella lista degli indirizzi consentiti, quindi

if (!contains($ips, $ip)) throw new Exception('Not allowed');

quindi il tuo codice vuole verificare una condizione. L'algoritmo per implementare questo controllo non dovrebbe preoccupare qui, nello spazio mentale del tuo programma principale il controllo è atomico. Ecco come dovrebbe essere.

Ma se metti il codice che fa il check nel tuo programma principale, lo perdi. Si introduce lo stato mutabile, in modo esplicito:

$list_contains_ip = undef;        # STATE: we don't know yet

foreach ($ips as $i) {
  if ($ip === $i) {
      $list_contains_ip = true;   # STATE: positive
      break;
  }
                                  # STATE: we still don't know yet, huh?                                                          
                                  # Well, then...
  $list_contains_ip = false;      # STATE: negative
}

if (!$list_contains_ip) {
  throw new Exception('Not allowed');
}

dove $list_contains_ip è la tua unica variabile di stato, o implicitamente:

                             # STATE: unknown
foreach ($ips as $i) {       # What are we checking here anyway?
  if ($ip === $i) {
    goto allowed;            # STATE: positive
  }
                             # STATE: unknown
}
                             # guess this means STATE: negative
throw new Exception('Not allowed');

allowed:                     # Guess we jumped over the trap door

Come vedi, c'è una variabile di stato non dichiarata nel costrutto GOTO. Questo non è un problema di per sé, ma queste variabili di stato sono come i ciottoli: portarne uno non è difficile, portare una borsa piena di questi ti farà sudare. Il tuo codice non rimarrà lo stesso: il mese prossimo ti verrà chiesto di distinguere tra indirizzi privati e pubblici. Il mese successivo, il tuo codice dovrà supportare intervalli IP. L'anno prossimo, qualcuno ti chiederà di supportare gli indirizzi IPv6. In pochissimo tempo, il tuo codice sarà simile a questo:

if ($ip =~ /:/) goto IP_V6;
if ($ip =~ /\//) goto IP_RANGE;
if ($ip =~ /^10\./) goto IP_IS_PRIVATE;

foreach ($ips as $i) { ... }

IP_IS_PRIVATE:
   foreach ($ip_priv as $i) { ... }

IP_V6:
   foreach ($ipv6 as $i) { ... }

IP_RANGE:
   # i don't even want to know how you'd implement that

ALLOWED:
   # Wait, is this code even correct?
   # There seems to be a bug in here.

E chiunque debba eseguire il debug di questo codice maledirà te e i tuoi figli.

Dijkstra lo mette in questo modo:

The unbridled use of the go to statement has as an immediate consequence that it becomes terribly hard to find a meaningful set of coordinates in which to describe the process progress.

Ed è per questo che GOTO è considerato dannoso.

    
risposta data 25.08.2016 - 19:07
fonte
41

Esistono alcuni casi di utilizzo legittimi per GOTO . Ad esempio per la gestione degli errori e la pulizia in C o per l'implementazione di alcune forme di macchine a stati. Ma questo non è uno di questi casi. Il secondo esempio è IMHO più leggibile, ma ancor più leggibile sarebbe quello di estrarre il loop in una funzione separata e quindi tornare quando si trova una corrispondenza. Ancora meglio sarebbe (in pseudocodice, non conosco la sintassi esatta):

if (!in_array($ip, $ips)) throw new Exception('Not allowed');

Quindi cosa c'è di male in GOTO ? La programmazione strutturata utilizza funzioni e strutture di controllo per organizzare il codice in modo che la struttura sintattica rifletta la struttura logica. Se qualcosa viene eseguito solo condizionatamente, apparirà in un blocco di istruzioni condizionale. Se qualcosa viene eseguito in un ciclo, apparirà in un blocco di loop. GOTO consente di aggirare la struttura sintattica saltando in giro arbitrariamente, rendendo quindi il codice molto più difficile da seguire.

Ovviamente se non hai altra scelta, usa GOTO , ma se è possibile ottenere lo stesso effetto con le funzioni e le strutture di controllo, è preferibile.

    
risposta data 25.08.2016 - 14:18
fonte
17

Come altri hanno già detto, il problema non è con goto stesso; il problema è con il modo in cui le persone usano goto e come può rendere più difficile la comprensione e la manutenzione del codice.

Assumi il seguente snippet di codice:

       i = 4;
label: printf( "%d\n", i );

Quale valore viene stampato per i ? Quando viene stampato? Fino a quando non conti l'istanza ogni di goto label nella tua funzione, non puoi sapere. La semplice presenza di quell'etichetta distrugge la tua capacità di eseguire il debug del codice con una semplice ispezione. Per piccole funzioni con uno o due rami, non è un grosso problema. Per le funzioni non piccole ...

All'inizio degli anni '90 ci fu data una pila di codice C che guidava un display grafico 3d e gli diceva di farlo funzionare più velocemente. Si trattava solo di circa 5000 righe di codice, ma tutte di esso era in main , e l'autore usava circa 15 branching di goto s in entrambe le direzioni. Questo è stato il codice cattivo per cominciare, ma la presenza di quei goto s lo ha reso molto peggio. Ho impiegato il mio collega circa 2 settimane per chiarire il flusso del controllo. Ancora meglio, quei goto s hanno portato a un codice così strettamente associato a se stesso che noi non potremmo apportare modifiche senza rompere qualcosa.

Abbiamo provato la compilazione con l'ottimizzazione di livello 1, e il compilatore ha mangiato tutta la RAM disponibile, quindi tutto lo scambio disponibile, e poi ha fatto il panico del sistema (che probabilmente non aveva nulla a che fare con il goto s, ma mi piace buttarlo aneddoto là fuori).

Alla fine, abbiamo dato al cliente due opzioni: riscriviamo il tutto da zero o acquistiamo hardware più veloce.

Hanno comprato hardware più veloce.

Regole di Bode per l'utilizzo di goto :

  1. Solo inoltro;
  2. Non bypassare le strutture di controllo (ad esempio, non diramarti nel corpo di un'istruzione if o for o while );
  3. Non utilizzare goto al posto di una struttura di controllo

Ci sono casi in cui un goto è la risposta giusta, ma sono rari (uscire da un ciclo profondamente annidato riguarda l'unico posto in cui lo userei).

Modifica

Espansione dell'ultima istruzione, ecco uno dei pochi casi di utilizzo validi per goto . Supponiamo di avere la seguente funzione:

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  for ( i = 0; i < N; i ++ )
  {
    arr[i] = malloc( sizeof *arr[i] * M );
    for ( j = 0; j < M; j++ )
    {
      arr[i][j] = malloc( sizeof *arr[i][j] * P );
      for ( k = 0; k < P; k++ )
        arr[i][j][k] = initial_value();
    }
  }
  return arr;
}

Ora abbiamo un problema: cosa succede se una delle chiamate malloc fallisce a metà? Per quanto improbabile possa essere un evento, non vogliamo restituire un array parzialmente assegnato, né vogliamo semplicemente salvare la funzione con un errore; vogliamo ripulire noi stessi e deallocare qualsiasi memoria parzialmente allocata. In una lingua che genera un'eccezione su un allocazione errata, è abbastanza semplice: basta scrivere un gestore di eccezioni per liberare ciò che è già stato assegnato.

In C, non hai una gestione strutturata delle eccezioni; devi controllare il valore di ritorno di ciascuna chiamata malloc e prendere l'azione appropriata.

T ***myalloc( size_t N, size_t M, size_t P )
{
  size_t i, j, k;

  T ***arr = malloc( sizeof *arr * N );
  if ( arr )
  {
    for ( i = 0; i < N; i ++ )
    {
      if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
        goto cleanup_1;

      for ( j = 0; j < M; j++ )
      {
        if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
          goto cleanup_2;

        for ( k = 0; k < P; k++ )
          arr[i][j][k] = initial_value();
      }
    }
  }
  goto done;

  cleanup_2:
    // We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
    while ( j-- )
      free( arr[i][j] );
    free( arr[i] );
    // fall through

  cleanup_1:
    // We failed while allocating arr[i]; free up all previously allocated arr[i][j]
    while ( i-- )
    {
      for ( j = 0; j < M; j++ )
        free( arr[i][j] );
      free( arr[i] );
    }

    free( arr );
    arr = NULL;

  done:
    return arr;
}

Possiamo farlo senza usare goto ? Certo che possiamo - richiede solo un po 'di contabilità in più (e, in pratica, questa è la strada che prenderò). Ma, se stai cercando luoghi in cui l'utilizzo di goto non è immediatamente un segno di cattiva pratica o progettazione, questo è uno dei pochi.

    
risposta data 25.08.2016 - 23:37
fonte
12

return , break , continue e throw / catch sono tutti essenzialmente gotos - trasferiscono il controllo a un altro pezzo di codice e potrebbero essere tutti implementati con gotos - infatti, l'ho fatto così una volta in un progetto scolastico, un istruttore PASCAL stava dicendo quanto Pascal fosse migliore del semplice a causa della struttura ... quindi dovevo essere contrario ...

La cosa più importante di Ingegneria del software (utilizzerò questo termine su Coding per fare riferimento a una situazione in cui vieni pagato da qualcuno per creare una base di codici insieme ad altri ingegneri che richiedono miglioramenti e manutenzione costanti) sta facendo codice leggibile - farlo fare qualcosa è quasi secondario. Il tuo codice verrà scritto una sola volta ma, nella maggior parte dei casi, le persone trascorreranno giorni e settimane a rivisitare / riapprendere, migliorare e risolvere il problema - e ogni volta che (o tu) dovrà ricominciare da zero e cercare di ricordare / capire il tuo codice.

La maggior parte delle funzionalità che sono state aggiunte alle lingue nel corso degli anni sono per rendere il software più gestibile, non più facile da scrivere (sebbene alcune lingue vadano in quella direzione - spesso causano problemi a lungo termine ...).

Rispetto a dichiarazioni di controllo di flusso simili, GOTO può essere quasi altrettanto facile da seguire al meglio (un singolo goto utilizzato in un caso come suggerisci tu), e un incubo in caso di abuso - e sono molto facilmente abusate ...

Quindi, dopo aver avuto a che fare con gli incubi degli spaghetti per alcuni anni, abbiamo appena detto "No", come comunità non accetteremo questa cosa - troppe persone lo incasinano se gli viene dato un po 'di libertà - questo è davvero l'unico problema con loro. Potresti usarli ... ma anche se è il caso perfetto, il prossimo assumerà che sei un programmatore terribile perché non capisci la storia della comunità.

Molte altre strutture sono state sviluppate solo per rendere più comprensibile il tuo codice: Funzioni, Oggetti, Scoping, Incapsulamento, Commenti (!) ... così come i pattern / processi più importanti come "DRY" (impedendo la duplicazione) e "YAGNI" (Riducendo la generalizzazione / complicazione del codice) - tutto davvero importa solo per il ragazzo NEXT per leggere il tuo codice (Chi sarà probabilmente te stesso - dopo aver dimenticato la maggior parte di quello che hai fatto in primo luogo! )

    
risposta data 25.08.2016 - 21:42
fonte
7

GOTO è uno strumento. Può essere usato per il bene o per il male.

Nei vecchi tempi, con FORTRAN e BASIC, era quasi lo strumento solo .

Quando guardi il codice di quei giorni, quando vedi GOTO devi capire perché è lì. Può far parte di un idioma standard che puoi capire rapidamente ... o può far parte di una struttura di controllo da incubo che non avrebbe mai dovuto essere. Non lo sai finché non hai guardato, ed è facile sbagliarsi.

Le persone volevano qualcosa di meglio e sono state inventate strutture di controllo più avanzate. Questi hanno coperto la maggior parte dei casi d'uso, e le persone che sono state bruciate da GOTO s hanno voluto vietarle completamente.

Ironia della sorte, GOTO non è così male quando è raro. Quando ne vedi uno, conosci c'è qualcosa di speciale in corso, ed è facile trovare l'etichetta corrispondente poiché è l'unica etichetta vicina.

Avanti veloce a oggi. Sei un docente che insegna programmazione. potresti dire "Nella maggior parte dei casi dovresti usare i nuovi costrutti avanzati, ma in alcuni casi un semplice GOTO può essere più leggibile." Gli studenti non lo capiranno. Stanno per abusare di GOTO per rendere il codice illeggibile.

Invece dici " GOTO bad. GOTO evil. GOTO esame fallito." Gli studenti capiranno che !

    
risposta data 26.08.2016 - 10:43
fonte
4

Ad eccezione di goto , tutti i costrutti di flusso in PHP (e nella maggior parte delle lingue) sono definiti gerarchicamente.

Immagina un codice esaminato attraverso gli occhi socchiusi:

a;
foo {
    b;
}
c;

Indipendentemente dal valore di controllo foo ( if , while , ecc.), ci sono solo determinati ordini consentiti per a , b e c .

Potresti avere a - b - c , o a - c , o anche a - b - b - b - c . Ma potresti mai avere b - c o a - b - a - c .

... a meno che tu non abbia goto .

$a = 1;
first:
echo 'a';
if ($a === 1) {
    echo 'b';
    $a = 2;
    goto first;
}
echo 'c'; 

goto (in particolare indietro all'indietro goto ) può essere abbastanza fastidioso che è meglio lasciarlo da solo e utilizzare costrutti di flusso gerarchici e bloccati.

goto s ha un posto, ma principalmente come micro-ottimizzazioni nei linguaggi di basso livello. IMO, non c'è posto per questo in PHP.

FYI, il codice di esempio può essere scritto anche meglio di entrambi i tuoi suggerimenti.

if(!in_array($ip, $ips, true)) {
    throw new Exception('Not allowed');
}
    
risposta data 25.08.2016 - 23:05
fonte
2

Nei linguaggi di basso livello GOTO è inevitabile. Ma ad alto livello dovrebbe essere evitato (nel caso in cui il linguaggio lo supporti) perché rende i programmi più difficili da leggere .

Tutto si riduce a rendere il codice più difficile da leggere. Le lingue di alto livello sono superate o rendono il codice più facile da leggere rispetto ai linguaggi di basso livello come, per esempio, assemblatore o C.

GOTO non causa il riscaldamento globale né provoca povertà nel terzo mondo. Rende il codice più difficile da leggere.

La maggior parte dei linguaggi moderni ha strutture di controllo che rendono GOTO inutile. Alcuni come Java non ce l'hanno nemmeno.

In effetti, il termine codice spaguetti deriva da cause di codice complicate e difficili da seguire da parte di strutture ramificate non strutturate.

    
risposta data 25.08.2016 - 21:35
fonte
1

Niente di sbagliato con le dichiarazioni di goto . I torti sono con alcune delle persone che usano impropriamente la dichiarazione.

Oltre a ciò che JacquesB ha detto (gestione degli errori in C), stai usando goto per uscire da un ciclo non annidato, cosa che puoi fare usando break . In questo caso è meglio usare break .

Ma se avessi uno scenario del ciclo nidificato, usare goto sarebbe più elegante / più semplice. Ad esempio:

$allowed = false;

foreach ($ips as $i) {
    foreach ($ips2 as $i2) {
        if ($ip === $i && $ip === $i2) {
            $allowed = true;
            goto out;
        }
    }
}

out:

if (!$allowed) {
    throw new Exception('Not allowed');
}

L'esempio sopra non ha senso nel tuo problema specifico, perché non hai bisogno di un ciclo annidato. Ma spero che tu veda solo la parte del ciclo nidificato.

Bonous point: se la tua lista di IP è piccola, il tuo metodo va bene. Ma se la lista cresce, sappi che il tuo approccio ha una peggiore complessità di run-time asintotica di O (n) . Man mano che la tua lista cresce, potresti voler utilizzare un metodo diverso che raggiunge O (log n) (come una struttura ad albero) o O (1) (una tabella hash senza collisioni).

    
risposta data 25.08.2016 - 15:23
fonte
0

With goto I can write faster code!

È vero. Non importa.

Goto exists in assembly! They just call it jmp.

È vero. Non importa.

Goto solves problems more simply.

È vero. Non importa.

In the hands of a disciplined developer code that uses goto can be easier to read.

È vero. Tuttavia, sono stato coder disciplinato. Ho visto cosa succede al codice nel tempo. Goto inizia bene. Poi l'urgenza di riutilizzare il codice entra in gioco. Presto mi ritrovo in una situazione di interruzione che non ha la minima idea di cosa sta succedendo anche dopo aver osservato lo stato del programma. Goto rende difficile ragionare sul codice. Abbiamo lavorato molto duramente creando while , do while , for , for each switch , subroutines , functions e altro ancora perché questa roba con if e goto è duro sul cervello.

Quindi no. Non vogliamo guardare goto . Certo è vivo e vegeto nel binario ma non abbiamo bisogno di vederlo alla fonte. Infatti, if sta iniziando a sembrare un po 'traballante.

    
risposta data 25.08.2016 - 22:15
fonte
-1

I linguaggi di assieme hanno in genere solo salti condizionali / incondizionati (l'equivalente di GOTO. Le implementazioni precedenti di FORTRAN e BASIC non avevano istruzioni di blocco di controllo oltre l'iterazione conteggiata (il ciclo DO), lasciando tutti gli altri flussi di controllo a IF e GOTO. Il ciclo DO in queste lingue è stato terminato da un'istruzione numerica. Di conseguenza, il codice scritto per queste lingue poteva essere, e spesso era, difficile da seguire e incline agli errori.

Per sottolineare il punto, c'è la frase " COME FROM " inventata scherzosamente.

Non è praticamente necessario utilizzare GOTO in linguaggi come C, C ++, C #, PASCAL, Java, ecc .; si possono usare costruzioni alternative che saranno quasi altrettanto efficienti e molto più manutenibili. È vero che un GOTO in un file sorgente non sarà un problema. Il problema è che non ci vogliono molte risorse per rendere l'unità di codice difficile da seguire e soggetta a errori da mantenere. Ecco perché la saggezza accettata è di evitare GOTO quando possibile.

Questo articolo di wikipedia sull'istruzione goto potrebbe essere utile

    
risposta data 27.08.2016 - 04:30
fonte

Leggi altre domande sui tag