Использовать goto или нет? - PullRequest
20 голосов
/ 24 июня 2010

Возможно, этот вопрос звучит банально, но я нахожусь в такой ситуации.

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

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

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

Ответы [ 9 ]

36 голосов
/ 24 июня 2010

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

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

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

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

19 голосов
/ 24 июня 2010

Использование 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;
      }
15 голосов
/ 24 июня 2010

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

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

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

10 голосов
/ 24 июня 2010

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

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;
    }
}
8 голосов
/ 24 июня 2010

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

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

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

8 голосов
/ 24 июня 2010

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

4 голосов
/ 24 июня 2010

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

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

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

1 голос
/ 24 июня 2010

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

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

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

1 голос
/ 24 июня 2010

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

...