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

Этот вопрос может показаться банальным, но я попал в сложную ситуацию.

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

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

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


person Abhinav Upadhyay    schedule 23.06.2010    source источник
comment
Обязательная ссылка: xkcd.com/292   -  person Kimmo Puputti    schedule 24.06.2010
comment
Мне интересно. Можете ли вы опубликовать снимок кода до / после перехода?   -  person    schedule 24.06.2010
comment
Мы не знаем вашего босса и его предрассудков, поэтому, если вы хотите выглядеть умным, используйте указатели на функции (см. Мой ответ на другой вопрос здесь: stackoverflow.com/questions/1371460/state-machines-tutorials/)   -  person qrdl    schedule 24.06.2010
comment
Конечные автоматы - это предпоследняя антитеза тому, чего информатика пыталась достичь с момента своего зарождения. Должен быть десяток лучших способов разобрать строку. Честно говоря, попробуй найти.   -  person Amardeep AC9MF    schedule 24.06.2010
comment
@Amardeep: Ба, тусовщик.   -  person Vinko Vrsalovic    schedule 24.06.2010
comment
@ Amardeep: Ты ведь понимаешь, что предпоследнее означает второе после последнего, верно?   -  person bta    schedule 24.06.2010
comment
@bta: Да, знаю. Конечным является конструкция if-then-elseif-elseif-elseif-elseif-elseif ...   -  person Amardeep AC9MF    schedule 24.06.2010
comment
@qrdl спасибо за ссылку. Это был очень полезный код и что-то вроде новой техники программирования на C для меня. :)   -  person Abhinav Upadhyay    schedule 24.06.2010
comment
@Moron Я не писал код на момент публикации этого вопроса. Как только я начал думать о том, как написать код, мне пришло в голову, что использование goto было более интуитивным, чем мы, естественно, думаем, создавая конечные автоматы на бумаге.   -  person Abhinav Upadhyay    schedule 24.06.2010
comment
@Abhinav: Понятно. Подумайте об этом так: скажем, вы разработали конечный автомат и имеете биграмму с 10 состояниями. Теперь, если бы я заставил вас выровнять состояния близко друг к другу по прямой линии, а затем нарисовать конечный автомат стрелками, движущимися вперед и назад, как вы думаете, будет ли он читаемым? По сути, это то, что вы будете делать с кодом, если используете gotos и прыгаете одним методом!   -  person    schedule 24.06.2010
comment
@Moron Спасибо, это было очень хорошее объяснение, в котором есть смысл :)   -  person Abhinav Upadhyay    schedule 24.06.2010
comment
@Kimmo Puputti. Разве вы не имеете в виду goto xkcd.com/292?   -  person bta    schedule 24.06.2010
comment
@bta Нет, я боялся использовать goto. :)   -  person Kimmo Puputti    schedule 15.07.2010
comment
Возможный дубликат Использовать GOTO или нет?.   -  person Peter Mortensen    schedule 16.05.2016


Ответы (9)


Использование 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;
      }
person Jerry Coffin    schedule 23.06.2010

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

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

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

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

person bta    schedule 23.06.2010
comment
есть мудрость следовать тому соглашению, которое уже используется - person Cervo; 24.06.2010
comment
Спасибо. Это очень полезный совет. Эта проблема с конечным автоматом только что возникла в моем собственном проекте, а код, предоставленный другими, действительно чистый, поэтому я воспользуюсь этим подходом. - person Abhinav Upadhyay; 24.06.2010

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

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

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

person Vinko Vrsalovic    schedule 23.06.2010
comment
Проблемы, с которыми я столкнулся при использовании нетривиальных генераторов кода, - это 1) отладка и 2) необходимость изучить новый инструмент ... Если инструмент дает мне что-то полезное, чего было бы трудно достичь без него, или если Я воспользуюсь этим инструментом достаточно, чтобы окупить время, потраченное на его изучение (обратите внимание на отлаживаемость как важный фактор), отлично, я сделаю это. Я бы не стал беспокоиться о тривиальном автомате, но мог бы подумать о более сложном. - person Aidan Cully; 24.06.2010
comment
Я бы не стал защищать GOTO, потому что это делают генераторы конечных автоматов. Есть разница между генератором кода (который предположительно был протестирован), создающим GOTO, и программистом, выполняющим его. Я не уверен, куда мне идти. Но я бы не позволил тому факту, что генераторы используют GOTO, повлиять на решение в любом случае (кроме, конечно, если я писал генератор для этого ...). - person Cervo; 24.06.2010
comment
@Aidan: Я в основном согласен. Хотя некоторые генераторы имеют средства отладки, которые устраняют проблему отладки, они не решают проблему «изучения нового инструмента». Для простых машин они, вероятно, излишни, если вы еще не знакомы с инструментом, который в любом случае может быть стимулом для его изучения :-) - person Vinko Vrsalovic; 24.06.2010
comment
@Cervo: Верно. Я не собирался защищать использование goto для кода, сгенерированного пользователем, хотя при перечитывании абзаца это действительно так. - person Vinko Vrsalovic; 24.06.2010
comment
@Aidan, @Cervo: Отредактировано с учетом ваших комментариев. - person Vinko Vrsalovic; 24.06.2010
comment
@Vinko Спасибо, это действительно очень полезный инструмент. Я обязательно его изучу. Прямо сейчас мне нужно построить небольшой конечный автомат, состоящий не более чем из 6 состояний, поэтому мне лучше использовать подход с переменной состояния и переключением вариантов. - person Abhinav Upadhyay; 24.06.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;
    }
}
person R Samuel Klatchko    schedule 23.06.2010

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

person Femaref    schedule 23.06.2010

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

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

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

person wallyk    schedule 23.06.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 = в предыдущем коде.

person Aidan Cully    schedule 23.06.2010
comment
... разве это не просто goto? - person amara; 24.06.2010
comment
Нет, потому что компилятор обрабатывает метки, прыжки и стек. Это все равно что предположить, что миллион строк выразительного языка, такого как Lisp или Python, по сути, может быть ассемблером. Конечно, они могут выполнять одну и ту же задачу после того, как все сказано и сделано, но удаленно они не одинаковы. - person Puppy; 24.06.2010

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

person jdehaan    schedule 23.06.2010

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

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

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

person Cervo    schedule 23.06.2010