在C中使用GOTO作为FSM
我在C中创建了一个有限状态机。我从硬件角度(HDL语言)学习了FSM。 所以我使用了一个每个状态一个case
的switch
。
我还想在编程时应用Separation of Concerns概念。 我的意思是我想得到这个流程:
- 根据当前状态和输入标志计算下一个状态
- validation下一个状态(如果用户请求不允许的转换)
- 允许时处理下一个状态
作为开始,我实现了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_state
和state
变量而不是next_state
和current_state
。 在第一个switch
设置output_state
和state
,第二个是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留给调用者,如上例所示。