Utiliser goto ou pas?

Cette question peut sembler clichée, mais je suis dans une situation ici.

J’essaie d’implémenter un automate à états finis pour parsingr une certaine chaîne en C. Lorsque j’ai commencé à écrire le code, je me suis rendu compte que le code pourrait être plus lisible si j’utilisais des étiquettes pour marquer les différents états et que je passais à l’état suivant. un autre selon les cas.

L’utilisation des variables de sauts et d’indicateurs standard est assez lourde dans ce cas et difficile à suivre.

Quelle approche est la meilleure? Plus que tout, je crains que cela ne laisse une mauvaise impression à mon supérieur hiérarchique, car je suis en stage.

Utiliser un goto pour implémenter une machine à états est souvent logique. Si vous êtes vraiment préoccupé par l’utilisation d’un goto, une alternative raisonnable consiste souvent à avoir une variable d’ state que vous modifiez et à une instruction switch basée sur celle-ci:

 typedef enum {s0,s1,s2,s3,s4,...,sn,sexit} state; state nextstate; int done = 0; nextstate = s0; /* set up to start with the first state */ while(!done) switch(nextstate) { case s0: nextstate = do_state_0(); break; case s1: nextstate = do_state_1(); break; case s2: nextstate = do_state_2(); break; case s3: . . . . case sn: nextstate = do_state_n(); break; case sexit: done = TRUE; break; default: /* some sort of unknown state */ break; } 

Il n’y a rien de fondamentalement faux avec goto . Ils sont souvent considérés comme “tabous” par la manière dont certains programmeurs (souvent issus du monde de l’assemblage) les utilisent pour créer un code “spaghetti” presque impossible à comprendre. Si vous pouvez utiliser les goto tout en maintenant votre code propre, lisible et sans bug, vous avez plus de pouvoir.

Utiliser des goto et une section de code pour chaque état est certainement un moyen d’écrire une machine à états. L’autre méthode consiste à créer une variable qui conservera l’état actuel et à utiliser une instruction switch (ou similaire) pour sélectionner le bloc de code à exécuter en fonction de la valeur de la variable d’état. Voir la réponse d’Aidan Cully pour un bon modèle utilisant cette deuxième méthode.

En réalité, les deux méthodes sont très similaires. Si vous écrivez une machine à états à l’aide de la méthode de variable d’état et la comstackz, l’assembly généré peut très bien ressembler à du code écrit à l’aide de la méthode goto (selon le niveau d’optimisation de votre compilateur). La méthode goto peut être vue comme une optimisation de la variable supplémentaire et de la boucle de la méthode de la variable d’état. La méthode que vous utilisez est une question de choix personnel, et tant que vous produisez un code lisible et fonctionnel, j’espère que votre patron ne pensera pas différemment de vous en utilisant une méthode par rapport à l’autre.

Si vous ajoutez ce code à une base de code existante contenant déjà des machines d’état, nous vous recommandons de suivre la convention déjà utilisée.

J’utiliserais un générateur FSM, comme Ragel , si je voulais laisser une bonne impression à mon patron.

Le principal avantage de cette approche est que vous êtes en mesure de décrire votre machine d’état à un niveau d’abstraction plus élevé et que vous n’avez pas à vous préoccuper de l’utilisation de goto ou d’un commutateur. Sans parler du cas particulier de Ragel, vous pouvez obtenir automatiquement de jolis diagrammes de votre FSM, insérer des actions à tout moment, minimiser automatiquement le nombre d’états et divers autres avantages. Ai-je mentionné que les FSM générés sont également très rapides?

Les inconvénients sont qu’ils sont plus difficiles à déboguer (la visualisation automatique y consortingbue beaucoup) et que vous devez apprendre un nouvel outil (ce qui ne vaut probablement pas la peine si vous avez une machine simple et que vous n’êtes pas susceptible d’écrire fréquemment. )

Je voudrais utiliser une variable qui suit l’état dans lequel vous vous trouvez et un commutateur pour les gérer:

 fsm_ctx_t ctx = ...; state_t state = INITIAL_STATE; while (state != DONE) { switch (state) { case INITIAL_STATE: case SOME_STATE: state = handle_some_state(ctx) break; case OTHER_STATE: state = handle_other_state(ctx); break; } } 

Goto n’est pas un mal nécessaire, et je dois être en total désaccord avec Denis. Oui, goo pourrait être une mauvaise idée dans la plupart des cas, mais il y a des utilisations. La plus grande crainte avec goto est ce que l’on appelle “code spagetti”, des chemins de code introuvables. Si vous pouvez éviter cela et si le comportement du code rest toujours clair et si vous ne sortez pas de la fonction avec un goto, rien ne s’oppose à goto. Utilisez-le simplement avec prudence et si vous êtes tenté de l’utiliser, évaluez vraiment la situation et trouvez une meilleure solution. Si vous ne pouvez pas faire cela, goto peut être utilisé.

Évitez de vous goto moins que la complexité ajoutée (à éviter) ne soit plus source de confusion.

Dans les problèmes d’ingénierie pratiques, il y a de la place pour une utilisation très modérée. Les universitaires et les non-ingénieurs se tordent les doigts inutilement pour l’utilisation de goto . Cela dit, si vous vous mettez dans un coin de la mise en œuvre où beaucoup de goto est la seule solution, repensez la solution.

Une solution qui fonctionne correctement est généralement l’objective principal. Le rendre correct et maintenable (en minimisant la complexité) présente de nombreux avantages sur le cycle de vie. Faites-le d’abord fonctionner, puis nettoyez-le progressivement, de préférence en simplifiant et en supprimant la laideur.

Je ne connais pas votre code spécifique, mais existe-t-il une raison qui ressemble à ceci:

 typedef enum { STATE1, STATE2, STATE3 } myState_e; void myFsm(void) { myState_e State = STATE1; while(1) { switch(State) { case STATE1: State = STATE2; break; case STATE2: State = STATE3; break; case STATE3: State = STATE1; break; } } } 

ne travaillerait pas pour vous? Il n’utilise pas de goto et est relativement facile à suivre.

Edit: Tous ces fragments State = violent Violent DRY, je pourrais donc faire quelque chose comme:

 typedef int (*myStateFn_t)(int OldState); int myStateFn_Reset(int OldState, void *ObjP); int myStateFn_Start(int OldState, void *ObjP); int myStateFn_Process(int OldState, void *ObjP); myStateFn_t myStateFns[] = { #define MY_STATE_RESET 0 myStateFn_Reset, #define MY_STATE_START 1 myStateFn_Start, #define MY_STATE_PROCESS 2 myStateFn_Process } int myStateFn_Reset(int OldState, void *ObjP) { return shouldStart(ObjP) ? MY_STATE_START : MY_STATE_RESET; } int myStateFn_Start(int OldState, void *ObjP) { resetState(ObjP); return MY_STATE_PROCESS; } int myStateFn_Process(int OldState, void *ObjP) { return (process(ObjP) == DONE) ? MY_STATE_RESET : MY_STATE_PROCESS; } int stateValid(int StateFnSize, int State) { return (State >= 0 && State < StateFnSize); } int stateFnRunOne(myStateFn_t StateFns, int StateFnSize, int State, void *ObjP) { return StateFns[OldState])(State, ObjP); } void stateFnRun(myStateFn_t StateFns, int StateFnSize, int CurState, void *ObjP) { int NextState; while(stateValid(CurState)) { NextState = stateFnRunOne(StateFns, StateFnSize, CurState, ObjP); if(! stateValid(NextState)) LOG_THIS(CurState, NextState); CurState = NextState; } } 

ce qui est, bien sûr, beaucoup plus long que la première tentative (chose amusante à propos de DRY). Mais il est également plus robuste: l'échec du renvoi de l'état à partir de l'une de ses fonctions entraînera un avertissement du compilateur, plutôt que d'ignorer en silence un State = manquant State = dans le code précédent.

Je vous recommanderais le ” livre Dragon “: Compilateurs, Principes-Techniques-Outils de Aho, Sethi et Ullman. (L’achat est plutôt cher, mais vous le trouverez certainement dans une bibliothèque). Vous y trouverez tout ce dont vous aurez besoin pour parsingr les chaînes et construire des automates finis. Il n’y a aucun endroit que je pourrais trouver avec un goto . Habituellement, les états sont une table de données et les transitions sont des fonctions comme accept_space()

Je ne vois pas beaucoup de différence entre goto et switch. Je préférerais peut-être switch / while car il vous donne une place garantie pour l’exécution après le basculement (où vous pouvez vous connecter à la journalisation et raisonner sur votre programme). Avec GOTO, vous ne faites que sauter d’une étiquette à l’autre. Par conséquent, pour enregistrer, vous devez le placer sur chaque étiquette.

Mais à part ça, il ne devrait pas y avoir beaucoup de différence. Quoi qu’il en soit, si vous ne divisez pas les fonctions et que tous les états n’utilisent / initialisent pas toutes les variables locales, vous risquez de vous retrouver avec un code presque spaghetti sans savoir quels états ont changé quelles variables et il est donc très difficile de déboguer / raisonner. sur.

En passant, pouvez-vous peut-être parsingr la chaîne en utilisant une expression régulière? La plupart des langages de programmation ont des bibliothèques qui permettent de les utiliser. Les expressions régulières créent souvent un FSM dans le cadre de leur implémentation. Généralement, les expressions rationnelles fonctionnent pour des éléments nesteds de manière non arbitraire et pour tout le rest, il existe un générateur d’parsingur (ANTLR / YACC / LEX). Il est généralement beaucoup plus facile de gérer une grammaire / regex que la machine à états sous-jacente. Vous avez également indiqué que vous effectuiez un stage et que, dans l’ensemble, ils vous faciliteraient la tâche par rapport à un développeur senior. Il ya donc de fortes chances pour qu’une expression régulière fonctionne avec la chaîne. De plus, les expressions régulières ne sont généralement pas mises en avant à l’université, alors essayez d’utiliser Google pour les lire.