在C中使用GOTO作为FSM

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

我还想在编程时应用Separation of Concerns概念。 我的意思是我想得到这个流程:

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

作为开始,我实现了3个函数:static e_InternalFsmStates fsm_GetNextState(); static bool_t fsm_NextStateIsAllowed(e_InternalFsmStates nextState); static void fsm_ExecuteNewState(e_InternalFsmStates);

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

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

既然它有效,我想改进代码。

我知道在3个函数中我将执行交换机的相同分支。 所以我想以这种方式使用goto

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

当然,我可以在一个开关盒中完成3个部分(计算,validation和处理下一个状态)。 但是对于代码可读性和代码审查,我觉得将它们分开会更容易。

最后我的问题是,以这种方式使用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; 

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

您的代码将难以维护且更加混乱。 switch / case实际上是某种“计算”goto(这就是为什么有案例标签 )。

你是基本思考错误的方式。 对于状态机,首先应validation输入,然后计算下一个状态,然后计算输出。 有多种方法可以做到这一点,但使用两个开关和 – 可能 – 一个error handling标签或错误标志通常是个好主意:

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

这样您就可以立即转换和validation输入。 这使得senase,因为你必须评估输入以设置下一个状态,因此无效输入只是自然地落在测试中。

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

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

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

旁注:编译器可以很好地优化结构化方法(不使用goto )与goto相同/相似的代码

它是否“危险”可能有点意见。 人们说避免使用GOTO的常见原因是它往往会导致难以理解的意大利面条代码。 这是绝对的规则吗? 可能不是,但我认为说这是趋势是绝对公平的。 其次,此时大多数程序员都接受过相信GOTO不好的培训,因此,即使不是某些情况,您可能会遇到与其他人稍后进入项目的某种程度的可维护性问题。

在你的情况下你有多大的风险,可能取决于你将在这些州标签下有多大的代码块,以及你有多确定它不会有太大变化。 更多代码(或大型修订的潜力)意味着更多风险。 除了直接的可读性问题之外,您还可以增加分配变量的机会,这些变量会干扰案例或依赖于您达到特定状态的路径。 使用函数通过为变量创建局部范围来帮助解决这个问题(在很多情况下)。

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

我的经验法则是只使用GOTO在代码中向前跳,但从向后跳。 最后,归结为使用GOTO 用于exception处理,否则在C中不存在。

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

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

spaghetti-gotos是不可接受的和糟糕的编程习惯: goto有一些有效用途,这不是其中之一。

相反,考虑使用一个单行状态机,如下所示:

 state = STATE_MACHINE[state](); 

以下是我的答案 (取自电气工程网站,它几乎普遍适用),它基于函数指针查找表。

 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*); 

function为

 err_t do_state_s1 (state_t* state); 

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

 error = STATE_MACHINE[state](&state); if(error != NO_ERROR) { // handle errors here } 

将所有error handling留给调用者,如上例所示。