在 C 中对 FSM 使用 GOTO

Using GOTO for a FSM in C

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

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

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

一开始我实现了 3 个功能: 静态 e_InternalFsmStates fsm_GetNextState(); static bool_t fsm_NextStateIsAllowed(e_InternalFsmStates nextState); 静态无效 fsm_ExecuteNewState(e_InternalFsmStates);

目前它们都包含一个相同的大开关盒:

switch (FSM_currentState) {
case FSM_State1:
    [...]
    break;
case FSM_State2:
    [...]
    break;
default:
    [...]
    break;
}

现在它可以工作了,我想改进代码。

我知道在 3 个函数中我将执行相同的开关分支。 所以我想以这种方式使用 gotos:

//
// 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

当然,我可以在一个 switch case 中完成这 3 个部分(计算、验证和处理下一个状态)。但是为了代码的可读性和代码审查,我觉得把它们分开会更容易。

最后我的问题是,以这种方式使用 GOTO 有危险吗? 在这样使用 FSM 时,您有什么建议吗?

感谢您的意见!


阅读下面的答案和评论后,这是我要尝试的方法:

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;

是不是"dangerous"大概见仁见智吧。人们说要避免 GOTO 的通常原因是它往往会导致难以理解的意大利面条代码。这是绝对的规则吗?可能不是,但我认为说这是趋势绝对公平。其次,在这一点上,大多数程序员都被训练成相信 GOTO 是不好的,所以,即使在某些情况下不是这样,你也可能 运行 进入某种程度的可维护性问题,其他人稍后会进入该项目。

你的情况有多大风险,可能取决于你将在这些状态标签下拥有的代码块有多大,以及你对它不会有太大变化的把握有多大。更多的代码(或可能进行大的修改)意味着更多的风险。除了直接的可读性问题之外,您还会有更多机会为变量赋值,这些变量会干扰案例或依赖于您到达特定状态所采取的路径。使用函数(在许多情况下)通过为变量创建局部范围来帮助解决这个问题。

总而言之,我建议避免使用 GOTO。

我的经验法则是只使用 GOTO 在代码中向前跳转,从不 向后跳转。最后这归结为使用 GOTO only 进行异常处理,否则在 C.

中不存在

在您的特定情况下,我绝对不建议使用 GOTO。

虽然 goto 在 C 中有其优势,但应谨慎使用并格外小心。您的意图不是值得推荐的用例。

您的代码将更难维护且更混乱。 switch/case 实际上是某种 "calculated" goto(这就是为什么有 case labels) .

你基本上是想错了。对于状态机,您应该首先验证输入,然后计算下一个状态,然后是输出。有多种方法可以做到这一点,但使用两个开关和 - 可能 - 一个错误处理标签或错误标志通常是个好主意:

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:
    ...

通过这种方式,您可以同时转换和验证输入。这很有道理,因为无论如何您都必须评估输入以设置下一个状态,因此无效输入自然会落在测试中。

另一种方法是使用 output_statestate 变量代替 next_statecurrent_state。在第一个 switch 你设置 output_statestate,第二个是 switch ( output_state ) ....

如果单个 case 变得太长,您应该使用函数来确定 next_state and/or output_state/输出。它在很大程度上取决于 FSM(输入、输出、状态、复杂性的数量(例如,one-hot 与 "encoded" - 如果你是 HDL 的家庭,你就会知道)。

如果您需要在循环内进行更复杂的错误处理(例如恢复),请保持循环不变并添加一个外部循环,可能会将错误标志更改为错误代码并为其添加另一个开关在外循环中。根据复杂度,将内层循环打包成自己的函数等

旁注:编译器可能会很好地优化 same/similar 代码的结构化方法(没有 goto),就像 goto

你真的不需要使用 switch-case,它实际上会被编译器优化成带有函数指针跳转的机器码 table。状态机的 Switch-case 往往有点难以阅读,尤其是更复杂的。

意大利面条式 goto 是不可接受的table 和糟糕的编程习惯:goto 有一些有效的用法,这是 不是 其中之一.

相反,考虑拥有一个如下所示的单线状态机:

state = STATE_MACHINE[state]();

这里是 an answer of mine(取自电气工程站点,它几乎普遍适用),它基于函数指针查找 table.

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

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

typedef err_t (*state_func_t)(state_t*); 

具有

的功能
err_t do_state_s1 (state_t* state); 

在这种情况下,调用者最终会是:

error = STATE_MACHINE[state](&state);

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

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