这个问题可能听起来有些陈词滥调,但我现在处于这种状况。
我正在尝试实现一个有限状态自动机来解析C中的某个字符串。当我开始编写代码时,我意识到如果我使用标签来标记不同的状态并使用goto跳转,那么代码可能更具可读性案件来临时,一个国家与另一个国家。
在这种情况下使用标准的break和flag变量非常麻烦,很难跟踪状态。
哪种方法更好?最重要的是,我担心这会给我的老板留下不好的印象,因为我正在实习。
答案 0 :(得分:36)
goto
没有任何内在错误。它们通常被认为是“禁忌”的原因是因为一些程序员(通常来自汇编世界)使用它们来创建几乎无法理解的“意大利面条”代码。如果您可以在保持代码清洁,可读和无错误的同时使用goto
语句,那么您将获得更多权力。
使用goto
语句和每个状态的一段代码绝对是编写状态机的一种方法。另一种方法是创建一个变量,该变量将保持当前状态并使用switch语句(或类似)来根据状态变量的值选择要执行的代码块。请参阅Aidan Cully使用第二种方法获得良好模板的答案。
实际上,这两种方法非常相似。如果使用状态变量方法编写状态机并对其进行编译,则生成的程序集可能非常类似于使用goto
方法编写的代码(取决于编译器的优化级别)。可以将goto
方法视为优化额外变量并从状态变量方法循环。你使用哪种方法是个人选择的问题,只要你正在制作工作的,可读的代码,我希望你的老板不会认为你使用一种方法而不是另一种方法。
如果要将此代码添加到已包含状态机的现有代码库中,我建议您遵循已使用的任何约定。
答案 1 :(得分:19)
使用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;
}
答案 2 :(得分:15)
如果我想给老板留下好印象,我会使用像Ragel这样的FSM生成器。
这种方法的主要好处是,您可以在更高的抽象级别描述您的状态机,而无需关心是使用goto还是交换机。更不用说在Ragel的特定情况下,您可以自动获得FSM的漂亮图表,在任何点插入操作,自动最小化状态量和各种其他好处。我是否提到生成的FSM也非常快?
缺点是它们更难调试(自动可视化在这里有很多帮助)并且你需要学习一个新工具(如果你有一台简单的机器并且你不太可能写,那可能是不值得的机器频繁。)
答案 3 :(得分:10)
我会使用一个跟踪你所处状态的变量和一个处理它们的开关:
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;
}
}
答案 4 :(得分:8)
Goto不是必要的邪恶,我不得不强烈反对Denis,是的,在大多数情况下goto可能是一个坏主意,但有一些用途。 goto最大的恐惧是所谓的“spagetti-code”,无法追踪的代码路径。如果你可以避免这种情况,如果它总是很清楚代码的行为方式,并且你没有用goto跳出函数,那就没有什么可以反对goto了。请谨慎使用它,如果您想要使用它,请真正评估情况并找到更好的解决方案。如果你不能这样做,可以使用goto。
答案 5 :(得分:8)
避免使用goto
,除非添加的复杂性(避免)更加混乱。
在实际的工程问题中,goto的使用空间很小。学者和非工程师不必要地使用goto
。也就是说,如果你把自己描绘成一个实现角落,其中很多goto
是唯一的出路,重新考虑解决方案。
正确工作的解决方案通常是主要目标。使其正确和可维护(通过最小化复杂性)具有许多生命周期优势。让它先工作,然后逐渐清理,最好是简化和去除丑陋。
答案 6 :(得分: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 =
。
答案 7 :(得分:1)
我会向你推荐Aho,Sethi和Ullman的“Dragon book”:Compilers, Principles-Techniques-Tools。 (这是相当昂贵的购买,但你肯定会在图书馆找到它)。在那里,您将找到解析字符串和构建有限自动机所需的任何内容。我找不到goto
的地方。通常状态是数据表,转换是像accept_space()
答案 8 :(得分:1)
我看不出goto和switch之间的区别。我可能更喜欢switch / while,因为它为您提供了一个保证在切换后执行的位置(您可以在其中投入日志记录并了解您的程序)。使用GOTO,你只需要从标签跳到标签,所以要记录你必须把它放在每个标签上。
但除此之外不应该有太大的区别。无论哪种方式,如果你没有将它分解为函数而不是每个状态都使用/初始化所有局部变量,你可能会得到一堆乱七八糟的意大利面条代码而不知道哪些状态改变了哪些变量并使调试/推理变得非常困难约。
顺便说一下,你可以使用正则表达式解析字符串吗?大多数编程语言都有允许使用它们的库。正则表达式通常会创建FSM作为其实现的一部分。通常,正则表达式适用于非任意嵌套的项目,而其他所有项目都有解析器生成器(ANTLR / YACC / LEX)。维护语法/正则表达式通常比底层状态机容易得多。你也说你正在实习,通常他们可能比高级开发人员给你更容易的工作,所以正则表达式可能会对字符串起作用。大学也不会强调正则表达式,因此请尝试使用谷歌阅读它们。