首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在C中为FSM使用GOTO

在C中为FSM使用GOTO
EN

Stack Overflow用户
提问于 2015-08-24 12:54:39
回答 4查看 2K关注 0票数 3

我在C中创建了一个有限状态机,我从硬件的角度(HDL语言)学习了FSM。所以我使用的是一个switch,每个州只有一个case

我也喜欢在编程时应用分离关注的概念。我的意思是,我想得到这个流量:

  1. 根据当前状态和输入标志计算下一个状态
  2. 验证此下一个状态(如果用户请求一个不允许的转换)
  3. 当允许下一个状态时处理它

首先,我实现了3个功能:静态e_InternalFsmStates fsm_GetNextState();静态bool_t fsm_NextStateIsAllowed(e_InternalFsmStates nextState);静态空洞fsm_ExecuteNewState(e_InternalFsmStates);

目前,它们都包含一个大的开关箱,这是相同的:

代码语言:javascript
复制
switch (FSM_currentState) {
case FSM_State1:
    [...]
    break;
case FSM_State2:
    [...]
    break;
default:
    [...]
    break;
}

现在它已经工作了,我想改进代码。

我知道,在这3个函数中,我将执行交换机的同一个分支。因此,我想以这种方式使用gotos:

代码语言:javascript
复制
//
// Compute next state
//
switch (FSM_currentState) {
case FSM_State1:
    next_state = THE_NEXT_STATE
    goto VALIDATE_FSM_State1_NEXT_STATE;
case FSM_State2:
    next_state = THE_NEXT_STATE
    goto VALIDATE_FSM_State2_NEXT_STATE;
    [...]
default:
    [...]
    goto ERROR;
}

//
// Validate next state
//
VALIDATE_FSM_State1_NEXT_STATE:
    // Some code to Set stateIsValid to TRUE/FALSE;
    if (stateIsValid == TRUE)
        goto EXECUTE_STATE1;
    else
        goto ERROR;

VALIDATE_FSM_State2_NEXT_STATE:
    // Some code to Set stateIsValid to TRUE/FALSE;
    if (stateIsValid == TRUE)
        goto EXECUTE_STATE2;
    else
        goto ERROR;

//
// Execute next state
//
EXECUTE_STATE1:
    // Do what I need for state1
    goto END;

EXECUTE_STATE2:
    // Do what I need for state2
    goto END;

//
// Error
//
ERROR:
    // Error handling
    goto END;

END:
    return; // End of function

当然,我可以在一个开关的情况下完成三个部分(计算、验证和处理下一个状态)。但是对于代码可读性和代码评审,我觉得将它们分开会更容易。

最后,我的问题是,用这种方式使用GOTOs是危险的吗?在使用FSM时,你会有什么建议吗?

谢谢您的评论!

在阅读了下面的答案和评论之后,下面是我要尝试的:

代码语言:javascript
复制
e_FSM_InternalStates nextState = FSM_currentState;
bool_t isValidNextState;

//
// Compute and validate next state
//
switch (FSM_currentState) {
case FSM_State1:
    if (FSM_inputFlags.flag1 == TRUE)
    {
        nextState = FSM_State2;
    }
    [...]

    isValidNextState = fsm_validateState1Transition(nextState);

case FSM_State2:
    if (FSM_inputFlags.flag2 == TRUE)
    {
        nextState = FSM_State3;
    }
    [...]
    isValidNextState = fsm_validateState2Transition(nextState);
}


//
// If nextState is invalid go to Error
//
if (isValidNextState == FALSE) {
    nextState = FSM_StateError;
}


//
// Execute next state
//
switch (nextState) {
case FSM_State1:
    // Execute State1
    [...]

case FSM_State2:
    // Execute State1
    [...]

case FSM_StateError:
    // Execute Error
    [...]
}

FSM_currentState = nextState;
EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2015-08-24 14:05:39

虽然goto在C中有它的优点,但它的使用应该非常谨慎。你想要的是没有推荐的用例。

您的代码将更不容易维护,更容易混淆。switch/case实际上是某种“经过计算的”goto (这就是为什么有case 标签)。

你的想法是错误的。对于状态机,您应该首先验证输入,然后计算下一个状态,然后是输出。这样做有多种方法,但使用两个开关(可能是一个错误处理标签或错误标志)往往是个好主意:

代码语言:javascript
复制
bool error_flag = false;

while ( run_fsm ) {
    switch ( current_state ) {

        case STATE1:
             if ( input1 == 1 )
                 next_state = STATE2;
             ...
             else
                 goto error_handling;   // use goto 
                 error_flag = true;     // or the error-flag (often better)
             break;

        ...
    }

    if ( error_flag )
        break;

    switch ( next_state ) {

        case STATE1:
            output3 = 2;
            // if outputs depend on inputs, similar to the upper `switch`
            break;
        ...
    }

    current_state = next_state;
}

error_handling:
    ...

这样,您就可以立即转换和验证输入。这使得senase,因为您必须评估输入,以设置下一个状态,所以无效的输入只是自然下降的测试。

另一种选择是有一个output_statestate变量,而不是next_statecurrent_state。在第一个switch中,设置了output_statestate,第二个是switch ( output_state ) ...

如果单个case的长度太长,则应该使用函数来确定next_state和/或output_state/outputs。这在很大程度上取决于FSM (输入、输出、状态、复杂性的数量(例如,一个热点与“编码”)--如果您是HDL家族,您就会知道)。

如果您需要在循环中进行更复杂的错误处理(例如恢复),请保持循环的原样并添加一个外部循环,可能会将错误标志更改为错误代码,并在外部循环中为其添加另一个开关。根据复杂性,将内环封装到自己的功能中,等等。

Sidenote:编译器很可能很好地将结构化方法(没有goto)优化为与goto相同/类似的代码。

票数 4
EN

Stack Overflow用户

发布于 2015-08-24 13:09:11

这是否“危险”可能在某种程度上是一个意见问题。人们说避免GOTO的通常原因是它会导致很难理解的意大利面代码。这是绝对规则吗?也许不是,但我认为这绝对是公平的,这是一个趋势。其次,目前大多数程序员都被训练成相信GOTO是坏的,所以,即使在某些情况下,您可能会在其他人进入项目时遇到某种程度的可维护性问题。

在您的情况下,您有多大的风险,可能取决于您将在这些州标签下的代码块的多大,以及您如何确定它不会有太大的变化。更多的代码(或潜在的大规模修订),意味着更多的风险。除了直截了当的可读性问题外,你将有更多的机会分配给变量,干扰情况,或者依赖于达到某种状态的路径。通过为变量创建本地范围,使用函数(在许多情况下)有助于实现这一点。

总之,我建议你避开后藤。

票数 2
EN

Stack Overflow用户

发布于 2015-08-24 14:20:50

您实际上并不需要使用开关大小写,它实际上会被编译器优化成带有函数指针跳转表的机器代码。状态机的开关用例往往比较难读,特别是比较复杂的。

意大利面-gotos是不可接受的和糟糕的编程实践:有一些有效的使用goto,这不是其中之一。

相反,考虑有一个单行状态机,它看起来像:

代码语言:javascript
复制
state = STATE_MACHINE[state]();

下面是基于函数指针查找表的an answer of mine (取自电气工程站点,它几乎是通用的)。

代码语言:javascript
复制
typedef enum
{
  STATE_S1,
  STATE_S2,
  ...
  STATE_N // the number of states in this state machine
} state_t;

typedef state_t (*state_func_t)(void);


state_t do_state_s1 (void);
state_t do_state_s2 (void);

static const state_func_t STATE_MACHINE [STATE_N] =
{
  &do_state_s1,
  &do_state_s2,
  ...
};


void main()
{
  state_t state = STATE_S1;

  while (1)
  {
    state = STATE_MACHINE[state]();
  }
}

state_t do_state_s1 (void)
{
  state_t result = STATE_S1;
  // stuff
  if (...) 
    result = STATE_S2;
  return result;
}

state_t do_state_s2 (void)
{
  state_t result = STATE_S2;
  // other stuff
  if (...) 
    result = STATE_S1;
  return result;
}

您也可以轻松地修改函数签名以包含错误代码,例如:

代码语言:javascript
复制
typedef err_t (*state_func_t)(state_t*); 

具有如下功能

代码语言:javascript
复制
err_t do_state_s1 (state_t* state); 

在这种情况下,调用者将以以下形式结束:

代码语言:javascript
复制
error = STATE_MACHINE[state](&state);

if(error != NO_ERROR)
{
  // handle errors here
}      

将所有错误处理留给调用方,如上面的示例所示。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/32182876

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档