这个问题可能听起来老套,但我遇到了困境。
我正在尝试在C语言中实现一个有限状态自动机来解析某个字符串。当我开始编写代码时,我意识到如果我使用标签来标记不同的状态并使用goto从一个状态跳到另一个状态,代码会更易读。
在这种情况下,使用标准的break和flag变量相当麻烦,并且很难跟踪状态。
哪种方法更好?最重要的是,我担心这可能会给我的老板留下不好的印象,因为我是一名实习生。
这个问题可能听起来老套,但我遇到了困境。
我正在尝试在C语言中实现一个有限状态自动机来解析某个字符串。当我开始编写代码时,我意识到如果我使用标签来标记不同的状态并使用goto从一个状态跳到另一个状态,代码会更易读。
在这种情况下,使用标准的break和flag变量相当麻烦,并且很难跟踪状态。
哪种方法更好?最重要的是,我担心这可能会给我的老板留下不好的印象,因为我是一名实习生。
goto
本身并没有问题。它通常被视为“禁忌”的原因是,一些程序员(通常来自汇编语言领域)使用它们创建“意大利面条”式的代码,这几乎是不可能理解的。如果您可以在保持代码干净、可读和无错误的情况下使用goto
语句,那么您就有更多的能力。
使用goto
语句和每个状态的代码块来编写状态机绝对是一种方法。另一种方法是创建一个变量来保存当前状态,并使用switch语句(或类似语句)根据状态变量的值选择要执行的代码块。请参见Aidan Cully的答案,其中提供了使用第二种方法的良好模板。
实际上,这两种方法非常相似。如果您使用状态变量方法编写状态机并编译它,则生成的汇编代码很可能类似于使用goto
方法编写的代码(取决于编译器的优化级别)。goto
方法可以看作是通过从状态变量方法中优化掉额外的变量和循环来进行优化。您使用哪种方法是个人选择的问题,只要您制作出有效、易读的代码,我希望您的老板不会因为使用一种方法而对您有所不同的看法。
如果您要将此代码添加到已经包含状态机的现有代码库中,我建议您跟随已经使用的约定。
使用 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;
}
如果我想给老板留下好印象,我会使用类似Ragel的FSM生成器。
这种方法的主要优点是,您可以在更高的抽象级别上描述状态机,并且不需要考虑是使用goto还是switch。特别是在Ragel的情况下,您可以自动获得漂亮的FSM图表,在任何时候插入操作,自动最小化状态数量以及其他各种好处。我有没有提到生成的FSM也非常快速?
缺点是它们更难调试(自动可视化在这里非常有帮助),并且您需要学习一个新工具(如果您有一个简单的机器并且不太可能频繁编写机器,则可能不值得)。
我建议使用一个变量来跟踪当前所处的状态,并使用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;
}
}
Goto并不是必须的恶,我强烈反对Denis的观点,是的,在大多数情况下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 =
。
goto
的地方。通常状态是数据表,转换是像 accept_space()
这样的函数。我看不出goto和switch之间有太大的区别。我可能更喜欢使用switch/while,因为它可以给你一个保证在switch之后执行的地方(你可以在这里添加日志并推理你的程序)。使用GOTO时,你只是从标签跳到标签,所以要添加日志,你必须在每个标签处放置它。
但除此之外,应该没有太大的区别。无论哪种方式,如果你没有将其分解成函数,并且不是每个状态都使用/初始化所有本地变量,你可能会得到一堆几乎像意大利面条代码一样的混乱,不知道哪些状态改变了哪些变量,使得调试/推理非常困难。
另外,你能否使用正则表达式解析字符串?大多数编程语言都有允许使用它们的库。正则表达式通常作为它们实现的一部分创建FSM。通常正则表达式适用于非任意嵌套项,对于其他所有内容,都有解析器生成器(ANTLR/YACC/LEX)。维护语法/正则表达式通常比维护底层状态机容易得多。此外,你说你正在实习,通常他们可能会给你比高级开发人员更简单的工作,所以有很大的机会正则表达式可以在字符串上工作。此外,正则表达式通常不是大学强调的内容,所以尝试使用Google来了解它们。