是否使用goto?

这个问题可能听起来有些陈词滥调,但我处于这种情况。

我正在尝试实现一个有限状态自动机来解析C中的某个字符串。当我开始编写代码时,我意识到如果我使用标签来标记不同的状态并使用goto从一个状态跳转到另一个案件来了。

在这种情况下使用标准的break和flag变量非常麻烦,很难跟踪状态。

什么方法更好? 最重要的是我担心这会给我的老板留下不好的印象,因为我正在实习。

使用goto实现状态机通常很有意义。 如果您真的担心使用goto,那么合理的替代方法通常是使用您修改的state变量,以及基于此的switch语句:

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

goto没有任何内在错误。 它们通常被认为是“禁忌”的原因是因为一些程序员(通常来自汇编世界)使用它们来创建几乎不可能理解的“意大利面条”代码。 如果您可以使用goto语句,同时保持代码清洁,可读和无错误,那么您将获得更多权力。

对每个状态使用goto语句和一段代码绝对是编写状态机的一种方法。 另一种方法是创建一个变量,该变量将保持当前状态并使用switch语句(或类似)来根据状态变量的值选择要执行的代码块。 请参阅Aidan Cully使用第二种方法获得良好模板的答案。

实际上,这两种方法非常相似。 如果使用状态变量方法编写状态机并对其进行编译,则生成的程序集可能非常类似于使用goto方法编写的代码(取决于编译器的优化级别)。 goto方法可以看作是从状态变量方法优化额外变量和循环。 你使用哪种方法是个人选择的问题,只要你正在制作工作的,可读的代码,我希望你的老板不会认为你使用一种方法而不是另一种方法。

如果要将此代码添加到已包含状态机的现有代码库中,我建议您遵循已使用的任何约定。

如果我想给老板留下好印象,我会像Ragel一样使用FSM发生器。

这种方法的主要好处是,您能够在更高的抽象级别描述您的状态机,而不需要关心是否使用goto或switch。 更不用说在Ragel的特定情况下,您可以自动获得FSM的漂亮图表,在任何点插入操作,自动最小化状态量和各种其他好处。 我是否提到生成的FSM也非常快?

缺点是它们更难调试(自动可视化在这里有很多帮助)并且你需要学习一个新工具(如果你有一台简单的机器并且你不太可能经常编写机器,这可能是不值得的。 )

我会使用一个跟踪你所处状态的变量和一个处理它们的开关:

 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不是必要的邪恶,我不得不强烈反对Denis,是的,在大多数情况下goto可能是一个坏主意,但有用途。 goto最大的恐惧是所谓的“spagetti-code”,无法追踪的代码路径。 如果你可以避免这种情况,如果它总是很清楚代码的行为方式,并且你没有用goto跳出函数,那就没有什么可以反对goto了。 只需谨慎使用它,如果您想要使用它,请真正评估情况并找到更好的解决方案。 如果您不能这样做,可以使用goto。

除非添加的复杂性(避免)更加混乱,否则请避免使用goto

在实际的工程问题中,可以非常谨慎地使用goto。 学者和非工程师不必要地使用goto 。 也就是说,如果你把自己描绘成一个实现角落,其中很多goto是唯一的出路,重新考虑解决方案。

正确工作的解决方案通常是主要目标。 使其正确和可维护(通过最小化复杂性)具有许多生命周期益处。 首先使其工作,然后逐渐清理,最好通过简化和去除丑陋。

我不知道你的具体代码,但有这样的原因:

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

不会对你有用吗? 它不使用goto ,并且相对容易遵循。

编辑:所有那些State =片段都违反DRY,所以我可能会做类似的事情:

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

当然,这比第一次尝试要长得多(关于DRY的有趣事情)。 但它也更强大 - 未能从状态函数之一返回状态将导致编译器警告,而不是在早期代码中默默地忽略缺少的State =

我会向你推荐“ 龙书 ”:Aho,Sethi和Ullman的编译器,原理 – 技术 – 工具 。 (这是相当昂贵的购买,但你肯定会在图书馆找到它)。 在那里,您将找到解析字符串和构建有限自动机所需的任何内容。 我没有找到goto 。 通常状态是数据表,转换是像accept_space()这样的函数

我不能看到goto和switch之间有太大区别。 我可能更喜欢switch / while,因为它为您提供了一个保证在切换后执行的位置(您可以在其中输入日志并推理您的程序)。 使用GOTO,你只需要从标签跳到标签,所以要记录你必须把它放在每个标签上。

但除此之外,应该没有太大区别。 无论哪种方式,如果你没有将它分解为函数而不是每个状态都使用/初始化所有局部变量,你可能会得到一堆乱七八糟的意大利面条代码而不知道哪些状态改变了哪些变量并使得调试/推理非常困难关于。

顺便说一句,你可以使用正则表达式解析字符串吗? 大多数编程语言都有允许使用它们的库。 正则表达式通常会创建FSM作为其实现的一部分。 通常,正则表达式适用于非任意嵌套的项目,对于其他所有项目,都有一个解析器生成器(ANTLR / YACC / LEX)。 维护语法/正则表达式通常比底层状态机容易得多。 另外你说你正在实习,通常他们可能比高级开发人员更容易工作,所以正则表达式可能会对字符串起作用。 大学也不会强调正则表达式,因此请尝试使用谷歌阅读它们。