web-dev-qa-db-fra.com

Qu'est-ce qui est si mauvais avec goto quand il est utilisé pour ces cas évidents et pertinents?

J'ai toujours su que goto est quelque chose de mauvais, enfermé dans un sous-sol quelque part pour ne jamais être vu pour de bon, mais j'ai rencontré aujourd'hui un exemple de code qui est parfaitement logique d'utiliser goto.

J'ai une adresse IP où je dois vérifier si elle se trouve dans une liste d'adresses IP, puis continuer avec le code, sinon lever une exception.

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

...

Si je n'utilise pas goto alors je dois utiliser une variable comme

$allowed = false;

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

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

Ma question est ce qui est si mauvais avec goto quand il est utilisé pour des cas aussi évidents et pertinents?

40
php_nub_qq

GOTO lui-même n'est pas un problème immédiat, ce sont les machines à états implicites que les gens ont tendance à implémenter avec. Dans votre cas, vous voulez un code qui vérifie si l'adresse IP est dans la liste des adresses autorisées, d'où

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

donc votre code veut vérifier une condition. L'algorithme pour implémenter cette vérification ne devrait pas être un problème ici, dans l'espace mental de votre programme principal, la vérification est atomique. Voilà comment il devrait être.

Mais si vous mettez le code qui effectue la vérification dans votre programme principal, vous le perdez. Vous introduisez un état mutable, soit explicitement:

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

$list_contains_ip est votre seule variable d'état, ou implicitement:

                             # 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

Comme vous le voyez, il existe une variable d'état non déclarée dans la construction GOTO. Ce n'est pas un problème en soi, mais ces variables d'état sont comme des cailloux: en porter un n'est pas difficile, en porter un sac plein vous fera transpirer. Votre code ne restera pas le même: le mois prochain, il vous sera demandé de faire la différence entre les adresses privées et publiques. Le mois suivant, votre code devra prendre en charge les plages IP. L'année prochaine, quelqu'un vous demandera de prendre en charge les adresses IPv6. En un rien de temps, votre code ressemblera à ceci:

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.

Et quiconque doit déboguer ce code vous maudira, vous et vos enfants.

Dijkstra le dit comme ceci:

L'utilisation débridée de l'instruction go to a pour conséquence immédiate qu'il devient terriblement difficile de trouver un ensemble significatif de coordonnées pour décrire la progression du processus.

Et c'est pourquoi GOTO est considéré comme nocif.

120
wallenborn

Il existe des cas d'utilisation légitimes pour GOTO. Par exemple pour la gestion des erreurs et le nettoyage en C ou pour l'implémentation de certaines formes de machines d'état. Mais ce n'est pas un de ces cas. Le deuxième exemple est à mon humble avis plus lisible, mais encore plus lisible serait d'extraire la boucle vers une fonction distincte, puis de revenir lorsque vous trouverez une correspondance. Encore mieux serait (en pseudocode, je ne connais pas la syntaxe exacte):

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

Alors, qu'est-ce qui est si mauvais avec les GOTO? La programmation structurée utilise des fonctions et des structures de contrôle pour organiser le code afin que la structure syntaxique reflète la structure logique. Si quelque chose n'est exécuté que de manière conditionnelle, il apparaîtra dans un bloc d'instructions conditionnelles. Si quelque chose est exécuté dans une boucle, il apparaîtra dans un bloc de boucle. GOTO vous permet de contourner la structure syntaxique en sautant arbitrairement, ce qui rend le code beaucoup plus difficile à suivre.

Bien sûr, si vous n'avez pas d'autre choix, vous utilisez GOTO, mais si le même effet peut être obtenu avec des fonctions et des structures de contrôle, c'est préférable.

40
JacquesB

Comme d'autres l'ont dit, le problème n'est pas avec le goto lui-même; le problème est de savoir comment les gens utilisent goto, et comment cela peut rendre le code plus difficile à comprendre et à maintenir.

Supposons l'extrait de code suivant:

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

Quelle valeur est imprimée pour i? Quand est-il imprimé? Jusqu'à ce que vous preniez en compte chaque instance de goto label dans votre fonction, vous ne pouvez pas savoir. La simple présence de cette étiquette détruit votre capacité à déboguer du code par simple inspection. Pour les petites fonctions à une ou deux branches, pas trop de problème. Pour les petites fonctions ...

Au début des années 90, on nous a donné une pile de code C qui a piloté un affichage graphique 3D et a dit de le faire fonctionner plus rapidement. Il ne s'agissait que d'environ 5000 lignes de code, mais tout se trouvait dans main, et l'auteur en a utilisé une quinzaine gotos ramification dans les deux sens. C'était un mauvais code pour commencer, mais la présence de ces gotos l'a rendu bien pire. Il a fallu environ 2 semaines à mon collègue pour comprendre le flux de contrôle. Encore mieux, ces gotos ont abouti à un code si étroitement lié à lui-même que nous ne pouvions apporter aucune modification sans casser quelque chose.

Nous avons essayé de compiler avec l'optimisation de niveau 1, et le compilateur a mangé toute la RAM disponible, puis tout le swap disponible, puis a paniqué le système (ce qui n'avait probablement rien à voir avec les gotos eux-mêmes, mais j'aime jeter cette anecdote là-bas).

En fin de compte, nous avons donné au client deux options - réécrivons le tout à partir de zéro ou achetons du matériel plus rapide.

Ils ont acheté du matériel plus rapide.

Règles de Bode pour l'utilisation de goto:

  1. Branche en avant uniquement;
  2. Ne contournez pas les structures de contrôle (c'est-à-dire, ne branchez pas dans le corps d'une instruction if ou for ou while);
  3. N'utilisez pas goto à la place d'une structure de contrôle

Il y a des cas où un goto est la bonne réponse, mais ils sont rares (sortir d'une boucle profondément imbriquée est à peu près le seul endroit où je l'utiliserais).

MODIFIER

En développant cette dernière instruction, voici l'un des rares cas d'utilisation valides pour goto. Supposons que nous ayons la fonction suivante:

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

Maintenant, nous avons un problème - que se passe-t-il si l'un des appels malloc échoue à mi-chemin? Peu probable que ce soit un événement, nous ne voulons pas retourner un tableau partiellement alloué, ni ne voulons simplement sortir de la fonction avec une erreur; nous voulons nettoyer après nous-mêmes et désallouer toute mémoire partiellement allouée. Dans un langage qui lève une exception sur une mauvaise allocation, c'est assez simple - vous écrivez simplement un gestionnaire d'exceptions pour libérer ce qui a déjà été alloué.

En C, vous n'avez pas de gestion structurée des exceptions; vous devez vérifier la valeur de retour de chaque appel malloc et prendre l'action appropriée.

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

Pouvons-nous faire cela sans utiliser goto? Bien sûr, nous pouvons - cela nécessite juste un peu de comptabilité supplémentaire (et, en pratique, c'est la voie que je prendrais). Mais, si vous cherchez des endroits où l'utilisation d'un goto n'est pas immédiatement un signe de mauvaise pratique ou de conception, c'est l'un des rares.

17
John Bode

return, break, continue et throw/catch sont tous essentiellement des gotos - ils transfèrent tous le contrôle à un autre morceau de code et pourrait tous être mis en œuvre avec gotos - en fait, je l'ai fait une fois dans un projet d'école, un instructeur Pascal disait à quel point Pascal était meilleur que de base en raison de la structure ... alors je devais être contraire ...

La chose la plus importante à propos du génie logiciel (je vais utiliser ce terme sur le codage pour faire référence à une situation où vous êtes payé par quelqu'un pour créer une base de code avec d'autres ingénieurs qui nécessite une amélioration et une maintenance continues) est de rendre le code lisible - - lui faire faire quelque chose est presque secondaire. Votre code ne sera écrit qu'une seule fois mais, dans la plupart des cas, les gens passeront des jours et des semaines à revisiter/réapprendre, à l'améliorer et à le corriger - et chaque fois qu'ils (ou vous) devront recommencer à zéro et essayer de se souvenir/comprendre votre code.

La plupart des fonctionnalités qui ont été ajoutées aux langues au fil des ans visent à rendre les logiciels plus faciles à maintenir et non plus faciles à écrire (bien que certaines langues vont dans ce sens - elles causent souvent des problèmes à long terme ...).

Par rapport à des déclarations de contrôle de flux similaires, les GOTO peuvent être presque aussi faciles à suivre au mieux (un seul goto utilisé dans un cas comme vous le suggérez), et un cauchemar en cas d'abus - et sont très facilement abusés ...

Donc, après avoir fait face à des cauchemars spaghettis pendant quelques années, nous avons juste dit "Non", en tant que communauté, nous n'allons pas accepter cela - trop de gens gâchent si on leur donne une petite latitude - c'est vraiment le seul problème avec eux. Vous pouvez les utiliser ... mais même si c'est le cas parfait, le prochain gars supposera que vous êtes un mauvais programmeur parce que vous ne comprenez pas l'histoire de la communauté.

De nombreuses autres structures ont été développées juste pour rendre votre code plus compréhensible: fonctions, objets, portée, encapsulation, commentaires (!) ... ainsi que les modèles/processus les plus importants comme "DRY" (empêchant la duplication) et "YAGNI" (Réduction de la généralisation excessive/complication du code) - tout cela n'est vraiment important que pour le gars suivant pour lire votre code (qui sera probablement vous - après avoir oublié la plupart de ce que vous avez fait en premier lieu!)

12
Bill K

GOTO est un outil. Il peut être utilisé pour le bien ou pour le mal.

Dans le mauvais vieux temps, avec FORTRAN et BASIC, c'était presque l'outil uniquement .

Lorsque vous regardez le code de ces jours, lorsque vous voyez un GOTO, vous devez comprendre pourquoi il est là. Cela peut faire partie d'un idiome standard que vous pouvez comprendre rapidement ... ou cela peut faire partie d'une structure de contrôle cauchemardesque qui n'aurait jamais dû l'être. Vous ne savez pas jusqu'à ce que vous ayez regardé, et il est facile de se tromper.

Les gens voulaient quelque chose de mieux et des structures de contrôle plus avancées ont été inventées. Celles-ci couvraient la plupart des cas d'utilisation et les personnes brûlées par de mauvais GOTOs voulaient les interdire complètement.

Ironiquement, GOTO n'est pas si mal quand c'est rare. Quand vous en voyez un, vous savez il se passe quelque chose de spécial, et il est facile de trouver l'étiquette correspondante car c'est la seule étiquette à proximité.

Avance rapide jusqu'à aujourd'hui. Vous êtes conférencier enseignant la programmation. Vous pourriez dire "Dans la plupart des cas, vous devez utiliser les nouvelles constructions avancées, mais dans certains cas, un simple GOTO peut être plus lisible." Les étudiants ne comprendront pas cela. Ils vont abuser de GOTO pour rendre le code illisible.

Au lieu de cela, vous dites "GOTO mauvais. GOTO mal. GOTO échec à l'examen." Les élèves comprendront que!

7
Stig Hemmer

À l'exception de goto, toutes les constructions de flux dans PHP (et la plupart des langues) sont étendues hiérarchiquement.

Imaginez un code examiné à travers les yeux plissés:

a;
foo {
    b;
}
c;

Quelle que soit la construction du contrôle foo (if, while, etc.), il n'y a que certaines commandes autorisées pour a, b et c.

Vous pouvez avoir a-b-c, ou a-c, ou même a-b-b-b-c. Mais vous pourriez jamais avoir b-c ou a-b-a-c.

... sauf si vous avez goto.

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

goto (en particulier vers l'arrière goto) peut être suffisamment gênant pour qu'il soit préférable de le laisser seul et d'utiliser des constructions de flux hiérarchiques et bloquées.

gotos ont une place, mais surtout comme micro-optimisations dans les langages de bas niveau. OMI, il n'y a pas de bon endroit pour cela en PHP.


Pour info, l'exemple de code peut être écrit encore mieux que l'une ou l'autre de vos suggestions.

if(!in_array($ip, $ips, true)) {
    throw new Exception('Not allowed');
}
4
Paul Draper

Dans les langues de bas niveau, GOTO est inévitable. Mais à un niveau élevé, il doit être évité (dans le cas où le langage le prend en charge) car cela rend les programmes plus difficiles à lire.

Tout se résume à rendre le code plus difficile à lire. Les langages de haut niveau sont proposés pour rendre le code plus facile à lire que les langages de bas niveau comme, par exemple, l'assembleur ou le C.

GOTO ne cause pas le réchauffement climatique ni la pauvreté dans le tiers monde. Cela rend le code plus difficile à lire.

La plupart des langages modernes ont des structures de contrôle qui rendent GOTO inutile. Certains comme Java ne l’ont même pas.

En fait, le terme code spaguetti provient de causes de code compliquées et difficiles à suivre par des structures de ramification non structurées.

2
Tulains Córdova

Rien de mal avec les instructions goto elles-mêmes. Les torts sont dus à certaines des personnes qui utilisent de manière inappropriée la déclaration.

En plus de ce que JacquesB a dit (gestion des erreurs en C), vous utilisez goto pour quitter une boucle non imbriquée, ce que vous pouvez faire en utilisant break. Dans ce cas, il vaut mieux utiliser break.

Mais si vous aviez un scénario de boucle imbriquée, l'utilisation de goto serait plus élégante/plus simple. Par exemple:

$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'exemple ci-dessus n'a pas de sens dans votre problème spécifique, car vous n'avez pas besoin d'une boucle imbriquée. Mais j'espère que vous n'en voyez que la partie imbriquée.

Bonous point: si votre liste d'adresses IP est petite, votre méthode est très bien. Mais si la liste s'allonge, sachez que votre approche a une pire complexité d'exécution asymptotique de O (n). Au fur et à mesure que votre liste s'allonge, vous souhaiterez peut-être utiliser une méthode différente qui permet d'obtenir O (log n) (comme une structure arborescente) ou O (1) (une table de hachage sans collision).

1
caveman

Avec goto, je peux écrire du code plus rapidement!

Vrai. Ne t'en fais pas.

Goto existe dans Assembly! Ils l'appellent juste jmp.

Vrai. Ne t'en fais pas.

Goto résout les problèmes plus simplement.

Vrai. Ne t'en fais pas.

Entre les mains d'un code de développeur discipliné qui utilise goto peut être plus facile à lire.

Vrai. Cependant, j'ai été ce codeur discipliné. J'ai vu ce qui arrive au code au fil du temps. Goto commence bien. Puis l'envie de réutiliser le code s'installe. Très vite, je me retrouve à un point d'arrêt n'ayant aucune idée de ce qui se passe même après avoir regardé l'état du programme. Goto, il est difficile de raisonner sur le code. Nous avons travaillé très dur pour créer while, do while, for, for eachswitch, subroutines, functions, et plus encore parce que faire ce genre de choses avec if et goto est difficile pour le cerveau.

Donc non. Nous ne voulons pas regarder goto. Bien sûr, il est bel et bien vivant dans le binaire, mais nous n'avons pas besoin de le voir dans la source. En fait, if commence à paraître un peu fragile.

0
candied_orange