Подтвердить что ты не робот

Использовать goto или нет?

Этот вопрос может звучать клише, но я здесь в ситуации.

Я пытаюсь реализовать автомат с конечным состоянием для синтаксического анализа определенной строки в C. Когда я начал писать код, я понял, что код может быть более читаемым, если я использовал метки для обозначения разных состояний и использовал goto для перехода от в зависимости от обстоятельств, одно государство в другое.

Использование стандартных разрывов и переменных переменных в этом случае довольно громоздко и трудно отслеживать состояние.

Какой подход лучше? Больше всего на свете я беспокоюсь, что это может оставить плохое впечатление на моего босса, поскольку я нахожусь на стажировке.

4b9b3361

Ответ 1

Использование goto для реализации конечного автомата часто имеет смысл. Если вы действительно обеспокоены использованием goto, разумной альтернативой часто является переменная state, которую вы изменяете, и оператор switch на основе этого:

typedef enum {s0,s1,s2,s3,s4,...,sn,sexit} state;

state nextstate;
int done = 0;

nextstate = s0;  /* set up to start with the first state */
while(!done)
   switch(nextstate)
      {
         case s0:
            nextstate = do_state_0();
            break;
         case s1:
            nextstate = do_state_1();
            break;
         case s2:
            nextstate = do_state_2();
            break;
         case s3:
             .
             .
             .
             .
         case sn:
            nextstate = do_state_n();
            break;
         case sexit:
            done = TRUE;
            break;
         default:
            /*  some sort of unknown state */
            break;
      }

Ответ 2

В goto нет ничего неправильного. Причина, по которой они часто считаются "табу", объясняется тем, как некоторые программисты (часто выходящие из мира сборки) используют их для создания кода "спагетти", который почти невозможно понять. Если вы можете использовать операторы goto, сохраняя свой код чистым, читаемым и без ошибок, тогда вам больше силы.

Использование операторов goto и раздела кода для каждого состояния, безусловно, является одним из способов записи конечного автомата. Другой метод - создать переменную, которая будет удерживать текущее состояние и использовать оператор switch (или аналогичный), чтобы выбрать, какой блок кода выполнить на основе значения переменной состояния. См. Aidan Cully для получения хорошего шаблона с использованием этого второго метода.

В действительности эти два метода очень похожи. Если вы записываете конечный автомат с использованием метода переменных состояния и скомпилируете его, сгенерированная сборка может очень похож на код, написанный с использованием метода goto (в зависимости от уровня оптимизации вашего компилятора). Метод goto можно рассматривать как оптимизацию дополнительной переменной и цикла из метода переменных состояния. Какой метод вы используете, это вопрос личного выбора, и пока вы создаете рабочий, читаемый код, я надеюсь, что ваш босс не будет думать о вас по-другому, если вы используете один метод над другим.

Если вы добавляете этот код в существующую базу кода, которая уже содержит конечные машины, я бы рекомендовал вам следовать тому, что уже используется.

Ответ 3

Я бы использовал генератор FSM, например Ragel, если бы хотел оставить хорошее впечатление на моем боссе.

Основное преимущество этого подхода заключается в том, что вы можете описать свою машину состояния на более высоком уровне абстракции и не нужно беспокоиться о том, следует ли использовать goto или коммутатор. Не говоря уже о конкретном случае Ragel, что вы можете автоматически получить красивые диаграммы вашего FSM, вставить действия в любой момент, автоматически минимизировать количество состояний и различные другие преимущества. Я упоминал, что сгенерированные FSM тоже очень быстрые?

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

Ответ 4

Я бы использовал переменную, которая отслеживает, в каком состоянии вы находитесь, и переключатель для их обработки:

fsm_ctx_t ctx = ...;
state_t state = INITIAL_STATE;

while (state != DONE)
{
    switch (state)
    {
    case INITIAL_STATE:
    case SOME_STATE:
        state = handle_some_state(ctx)
        break;

    case OTHER_STATE:
        state = handle_other_state(ctx);
        break;
    }
}

Ответ 5

Гото не нужно зло, и я должен категорически не соглашаться с Денисом, да, goto может быть плохой идеей в большинстве случаев, но есть применения. Самый большой страх с goto - это так называемый "спагетти-код", неразделенные пути кода. Если вы можете этого избежать, и если будет всегда ясно, как работает код, и вы не выходите из функции с goto, нет ничего против goto. Просто используйте его с осторожностью, и если вы соблазняетесь его использовать, действительно оцените ситуацию и найдите лучшее решение. Если вы не можете этого сделать, можно использовать goto.

Ответ 6

Избегайте goto, если добавленная сложность (чтобы избежать) более запутанна.

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

Правильно работающее решение обычно является основной задачей. Обеспечение правильности и сопровождения (с минимальной сложностью) имеет много преимуществ по жизненному циклу. Заставьте его работать сначала, а затем очистите его постепенно, предпочтительно, путем упрощения и устранения уродства.

Ответ 7

Я не знаю вашего конкретного кода, но есть ли что-то вроде этого:

typedef enum {
    STATE1, STATE2, STATE3
} myState_e;

void myFsm(void)
{
    myState_e State = STATE1;

    while(1)
    {
        switch(State)
        {
            case STATE1:
                State = STATE2;
                break;
            case STATE2:
                State = STATE3;
                break;
            case STATE3:
                State = STATE1;
                break;
        }
    }
}

не будет работать для вас? Он не использует goto, и относительно легко следовать.

Изменить: Все эти фрагменты State = нарушают DRY, поэтому я могу вместо этого сделать что-то вроде:

typedef int (*myStateFn_t)(int OldState);

int myStateFn_Reset(int OldState, void *ObjP);
int myStateFn_Start(int OldState, void *ObjP);
int myStateFn_Process(int OldState, void *ObjP);

myStateFn_t myStateFns[] = {
#define MY_STATE_RESET 0
   myStateFn_Reset,
#define MY_STATE_START 1
   myStateFn_Start,
#define MY_STATE_PROCESS 2
   myStateFn_Process
}

int myStateFn_Reset(int OldState, void *ObjP)
{
    return shouldStart(ObjP) ? MY_STATE_START : MY_STATE_RESET;
}

int myStateFn_Start(int OldState, void *ObjP)
{
    resetState(ObjP);
    return MY_STATE_PROCESS;
}

int myStateFn_Process(int OldState, void *ObjP)
{
    return (process(ObjP) == DONE) ? MY_STATE_RESET : MY_STATE_PROCESS;
}

int stateValid(int StateFnSize, int State)
{
    return (State >= 0 && State < StateFnSize);
}

int stateFnRunOne(myStateFn_t StateFns, int StateFnSize, int State, void *ObjP)
{
    return StateFns[OldState])(State, ObjP);
}

void stateFnRun(myStateFn_t StateFns, int StateFnSize, int CurState, void *ObjP)
{
    int NextState;

    while(stateValid(CurState))
    {
        NextState = stateFnRunOne(StateFns, StateFnSize, CurState, ObjP);
        if(! stateValid(NextState))
            LOG_THIS(CurState, NextState);
        CurState = NextState;
    }
}

что, конечно, намного дольше, чем первая попытка (смешная вещь о СУХОЙ). Но он также более надежный - отказ вернуть состояние из одной из функций состояния приведет к предупреждению компилятора, вместо того, чтобы молча игнорировать отсутствующий State = в более раннем коде.

Ответ 8

Я бы порекомендовал вам Dragon book ": Компиляторы, Принципы-Методы-Инструменты от Ахо, Сети и Ульмана. (Это довольно дорого купить, но вы наверняка найдете его в библиотеке). Там вы найдете все, что вам нужно, чтобы разобрать строки и построить конечные автоматы. Нет места, где я мог бы найти с помощью goto. Обычно состояния представляют собой таблицу данных, а переходы - такие функции, как accept_space()

Ответ 9

Я не вижу большой разницы между goto и switch. Я бы предпочел включить/выключить, потому что он дает вам место, которое может быть выполнено после переключения (где вы можете бросить запись и причину вашей программы). С GOTO вы просто продолжаете прыгать с ярлыка на ярлык, поэтому для входа в журнал вы должны поместить его на каждую метку.

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

Как в стороне, можете ли вы проанализировать строку, используя регулярное выражение? Большинство языков программирования имеют библиотеки, которые позволяют их использовать. Регулярные выражения часто создают FSM как часть их реализации. Обычно регулярные выражения работают для не произвольно вложенных элементов, а для всего остального - генератор парсера (ANTLR/YACC/LEX). Вообще говоря, гораздо проще поддерживать грамматику/регулярное выражение, чем базовый автомат. Кроме того, вы сказали, что находитесь на стажировке, и обычно они могут дать вам более легкую работу, чем сказать старший разработчик, поэтому существует сильная вероятность того, что регулярное выражение может работать над строкой. Также регулярные выражения обычно не выделяются в колледже, поэтому попробуйте использовать Google, чтобы читать их.