使用goto语句是否合适?

21

这个问题可能听起来老套,但我遇到了困境。

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

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

哪种方法更好?最重要的是,我担心这可能会给我的老板留下不好的印象,因为我是一名实习生。


19
必须链接:http://xkcd.com/292/ - Kimmo Puputti
3
我很好奇。您能否发布代码的goto之前和goto之后的快照? - Aryabhatta
3
我们不了解你的老板以及他有哪些偏见,因此如果你想表现聪明,就使用函数指针(请参阅我的其他问题的答案:https://dev59.com/bHM_5IYBdhLWcg3wdy1g#1371654)。 - qrdl
1
有限状态机是计算机科学自诞生以来一直试图实现的终极对立面。解析字符串肯定有十几种更好的方法。说实话,你可以试着找找看。 - Amardeep AC9MF
3
你的意思是“前往http://xkcd.com/292”吗? - bta
显示剩余8条评论
9个回答

38

goto本身并没有问题。它通常被视为“禁忌”的原因是,一些程序员(通常来自汇编语言领域)使用它们创建“意大利面条”式的代码,这几乎是不可能理解的。如果您可以在保持代码干净、可读和无错误的情况下使用goto语句,那么您就有更多的能力。

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

实际上,这两种方法非常相似。如果您使用状态变量方法编写状态机并编译它,则生成的汇编代码很可能类似于使用goto方法编写的代码(取决于编译器的优化级别)。goto方法可以看作是通过从状态变量方法中优化掉额外的变量和循环来进行优化。您使用哪种方法是个人选择的问题,只要您制作出有效、易读的代码,我希望您的老板不会因为使用一种方法而对您有所不同的看法。

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


3
遵循已经存在的惯例是明智的,这其中蕴含着智慧。 - Cervo
谢谢。这是非常有用的建议。这个状态机问题刚好出现在我的项目中,而其他人提供的代码非常干净,所以我会采取那种方法。 - Abhinav Upadhyay

20

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

15

如果我想给老板留下好印象,我会使用类似Ragel的FSM生成器。

这种方法的主要优点是,您可以在更高的抽象级别上描述状态机,并且不需要考虑是使用goto还是switch。特别是在Ragel的情况下,您可以自动获得漂亮的FSM图表,在任何时候插入操作,自动最小化状态数量以及其他各种好处。我有没有提到生成的FSM也非常快速?

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


3
使用复杂代码生成器遇到的问题有两点:1)调试,2)需要学习新工具。如果该工具提供了一些难以不用它实现的有用功能或者我会经常使用该工具来弥补学习成本(注意可调试性是一个重要因素),那么太好了,我会使用它。对于简单的状态机,我不会费心思去学习,但对于更复杂的状态机可能会学习使用该工具。 - Aidan Cully
3
我不会提倡使用GOTO语句,因为有限状态机生成器会这样做。在代码生成器(应该已经过测试)创建GOTO语句和程序员自己创建之间是有区别的。我不确定该怎么做才好。但无论如何,我不会让生成器使用GOTO语句对决策产生影响(除非当然我正在编写一个生成器...)。 - Cervo
@Aidan:我基本上同意。虽然一些生成器具有调试工具,可以缓解调试问题,但它们并不能缓解“学习新工具”的问题。对于简单的机器来说,它们可能过于复杂,除非你已经了解该工具,这可能是学习它的动力 :-) - Vinko Vrsalovic
1
@Cervo: 没错。我的意图并不是为用户生成的代码提倡使用goto,尽管重新阅读段落后似乎确实是这样。 - Vinko Vrsalovic
@Aidan,@Cervo:已根据您的评论进行了编辑。 - Vinko Vrsalovic
@Vinko 谢谢,这真的是一个非常有用的工具。我一定会去探索它。现在我只需要构建一个由不超过6个状态组成的小状态机,所以我最好使用状态变量和switch case方法。 - Abhinav Upadhyay

10

我建议使用一个变量来跟踪当前所处的状态,并使用switch语句处理它们:

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

8

Goto并不是必须的恶,我强烈反对Denis的观点,是的,在大多数情况下goto可能是个坏主意,但有时也会有其用途。最大的担忧是所谓的“spagetti-code”,即不可追踪的代码路径。如果您能避免这种情况,并且代码行为始终清晰可见,并且不会使用goto跳出函数,则没有任何理由反对使用goto。只需小心使用,如果您想使用它,请确实评估情况并找到更好的解决方案。如果无法做到这一点,则可以使用goto。


8
避免使用goto,除非增加的复杂性更加混乱。
在实际工程问题中,可以非常少量地使用goto。学术界和非工程师不必过分担心使用goto。尽管如此,如果你将自己限制在一个实现角落里,而大量使用goto是唯一的出路,请重新考虑解决方案。
通常,正确工作的解决方案是主要目标。通过最小化复杂性使其正确且易于维护具有许多生命周期优势。先让它正常工作,然后逐渐清理,最好是通过简化和消除丑陋的代码。

4
我不知道你具体的代码,但是是否有这样的原因:
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 =


这不就是实际上的goto吗? - amara
1
不行,因为编译器处理标签、跳转和堆栈。这就像暗示百万行表达性语言(如Lisp或Python)可以实际上是汇编语言一样。当所有事情都说完了,它们可能会完成相同的任务,但它们根本不一样。 - Puppy

1
我建议你阅读由Aho、Sethi和Ullman所著的 "龙书": 编译原理-技术与工具。(虽然购买价格相对较高,但你肯定可以在图书馆找到它)。在那里,你将找到任何你需要解析字符串和构建有限自动机的东西。我没有找到任何使用 goto 的地方。通常状态是数据表,转换是像 accept_space() 这样的函数。

1

我看不出goto和switch之间有太大的区别。我可能更喜欢使用switch/while,因为它可以给你一个保证在switch之后执行的地方(你可以在这里添加日志并推理你的程序)。使用GOTO时,你只是从标签跳到标签,所以要添加日志,你必须在每个标签处放置它。

但除此之外,应该没有太大的区别。无论哪种方式,如果你没有将其分解成函数,并且不是每个状态都使用/初始化所有本地变量,你可能会得到一堆几乎像意大利面条代码一样的混乱,不知道哪些状态改变了哪些变量,使得调试/推理非常困难。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接