Использование GOTO для FSM в C

Я создаю конечный конечный автомат в C. Я изучил FSM с аппаратной точки зрения (язык HDL). Поэтому я использую switch с одним case каждого состояния.

Я также хотел бы применить концепцию «Разделение проблем» при программировании. Я хочу сказать, что я хотел бы получить этот stream:

  1. Вычислить следующее состояние в зависимости от текущего состояния и входных флагов
  2. Подтвердите это следующее состояние (если пользователь запросит переход, который не разрешен)
  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 s таким образом:

 // // 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 части (вычислять, проверять и обрабатывать следующее состояние) в одном корпусе коммутатора. Но для чтения кода и просмотра кода я чувствую, что будет легче их разделить.

Наконец, мой вопрос: опасно ли использовать 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 (вот почему есть метки меток ).

    Вы в основном думаете не так. Для машины состояния вы должны сначала проверить ввод, затем вычислить следующее состояние, а затем выход. Существуют различные способы сделать это, но часто бывает полезно использовать два переключателя и, возможно, одну метку обработки ошибок или флаг ошибки:

     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_state и state вместо next_state и current_state . В первом switch вы устанавливаете output_state и state , второе – switch ( output_state ) ...

    Если один case s становится слишком длинным, вы должны использовать функции для определения next_state и / или output_state / выходов. Это очень зависит от FSM (количество входов, выходов, состояний, сложность (например, одноразовое или «закодированное»), если вы работаете с HDL, вы узнаете).

    Если вам нужна более сложная обработка ошибок (например, восстановление) внутри цикла, оставьте цикл как есть и добавьте внешний цикл, возможно, измените флаг ошибки на код ошибки и добавьте еще один переключатель для него во внешнем цикле. В зависимости от сложности упакуйте внутренний цикл в свою собственную функцию и т. Д.

    Sidenote: компилятор может очень хорошо оптимизировать структурированный подход (без goto ) к тому же / аналогичному коду, что и для goto

    Является ли это «опасным», вероятно, несколько вопросом. Обычная причина, по которой люди говорят, чтобы избежать GOTO, заключается в том, что она ведет к коду спагетти, который трудно поддаться. Это абсолютное правило? Наверное, нет, но я считаю, что определенно справедливо сказать, что это тренд. Во-вторых, большинство программистов на этом этапе обучены полагать, что GOTO плох, поэтому, даже если это не так, вы можете столкнуться с некоторой проблемой ремонтопригодности с другими людьми, входящими в проект позже.

    Сколько риска у вас в вашем случае, вероятно, зависит от того, насколько большой кусок кода вы собираетесь иметь под этими государственными надписями и насколько вы уверены, что он не сильно изменится. Дополнительный код (или потенциал для больших изменений) означает больший риск. Помимо простых вопросов читаемости, у вас будет больше шансов на присвоение переменных, мешающих событиям или зависящим от пути, который вы сделали, чтобы добраться до определенного состояния. Использование функций помогает в этом (во многих случаях), создавая локальную область для переменных.

    В общем, я бы рекомендовал избегать GOTO.

    Мое правило состоит в том, чтобы использовать GOTO только для перехода вперед в коде, но никогда не назад. В конечном итоге это сводится к использованию GOTO только для обработки исключений, чего в противном случае не существует в C.

    В вашем конкретном случае я бы абсолютно не рекомендовал использовать GOTO.

    Вам действительно не нужно использовать коммутационный футляр, он фактически будет оптимизирован компилятором в машинный код с таблицей перехода указателя функций. Выключатели для государственных машин, как правило, несколько трудно читать, особенно более сложные.

    Спагетти-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*); 

    с функциями как

     err_t do_state_s1 (state_t* state); 

    в этом случае вызывающий абонент получит:

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

    Оставьте всю обработку ошибок вызывающей стороне, как показано в приведенном выше примере.