Введение
Многие программисты считают, что операторы goto
— лучший способ обработки ошибок в языке программирования C. Я даже видел, как это чувство выражается в виде мема. Я категорически не согласен. Почему меня это должно волновать, если я нахожусь в отпуске в пределах видимости пляжа? Я полагаю, что за эти годы у меня был этот спор с таким количеством программистов, что я надеюсь, что изложение своих мыслей будет каким-то катарсисом.
Я осознаю, что, придерживаясь этой точки зрения, я могу показаться второкурсником. Наоборот, я считаю себя закаленным ветераном. Хорошо изучите правила, чтобы знать, когда их нарушать и правила созданы для того, чтобы их нарушать — всегда популярные принципы. В них есть доля правды, но я также думаю, что они популярны отчасти потому, что льстят эго. Мы все должны остерегаться ошибки обратного. (Я нарушаю правила, поэтому я профессионал.)
Программирование — это не только наука, но и искусство, а большая часть искусства подчиняется правилам. Пуантилизм — прекрасный тому пример. Есть заметные движения в компьютерных науках, а также в мире искусства. Одним из таких направлений является структурированное программирование. Даже если вы продолжите свободно использовать goto
в своих программах, я думаю, есть смысл изучить альтернативы.
Моя цель — показать, что C обеспечивает достаточную поддержку обработки ошибок без обращения к goto
.
Фон
Еще в 1968 году выдающийся голландский ученый-компьютерщик Эдсгер Дийкстра позорно заметил:
В течение ряда лет я был знаком с наблюдением, что качество программистов является убывающей функцией плотности
go to
операторов в создаваемых ими программах.
Язык программирования C (1978, 1988), возможно, самая важная книга о C, написанная его изобретателями: Керниганом и Ритчи. Они не использовали goto
в своей книге и прямо не советуют его использовать:
За некоторыми исключениями, подобными приведенным здесь, код, основанный на операторах
goto
, как правило, труднее понять и поддерживать, чем код без операторов goto. Хотя мы не догматичны в этом вопросе, кажется, что операторыgoto
следует использовать редко, если вообще использовать.
Одни только эти два источника должны убедить любого, кто восприимчив к аргументам авторитетных источников, тем не менее, использование goto
, похоже, широко распространено, как никогда, несмотря на широко разрекламированную ошибку перехода к ошибке в коде Apple SSL. (Ошибка, вызванная отсутствием фигурных скобок, но Барнум ошибался, говоря, что плохой рекламы не бывает.)
мой собственный взгляд
За последние двадцать лет я писал код во многих стилях. У одного работодателя было правило не более одного ярлыка на функцию; у другого был стандарт кодирования, основанный на MISRA C, который запрещал не только goto
, но и любой вид раннего выхода (хотя почти все его игнорировали). Я также прошел через ошибочную фазу тщательного изучения всего объектного кода, сгенерированного старомодным компилятором C, чтобы убедиться, что он похож на язык ассемблера, который я написал бы сам. Такой уровень брезгливости повлек за собой довольно много goto
утверждений. В настоящее время, как и у Лапласа, в моем коде нет необходимости goto
.
Многие стандарты кодирования не рекомендуют goto
; Я никогда не видел ни одного мандата на это. Следовательно, для большинства программистов использование goto
является активным выбором. Где-то по ходу они видят код, изобилующий операторами goto
, а затем решают скопировать этот стиль. Некоторые, возможно, были полностью самоучками и никогда не могли придумать ничего лучшего. Некоторые учатся не использовать GOTO
в BASIC, а затем "забывают" этот урок, когда начинают писать C. Другие, возможно, все еще пишут код в стиле, который они приняли, когда компиляторы C были намного хуже, чем сегодня. Независимо от того, как эта привычка была приобретена, она естественным образом подкрепляется эффектом простого воздействия.
Два исключения, процитированные K&R в абзаце, который я процитировал выше, таковы:
- Чтобы отказаться от обработки в какой-то глубоко вложенной структуре, например, выйти из двух или более циклов одновременно. Они комментируют: «Эта организация удобна, если код обработки ошибок нетривиален и если ошибки могут возникать в нескольких местах».
- Отказаться от обработки, когда установлено, что два массива имеют общий элемент (т. е. разрыв двух циклов в случае успеха, а не в случае неудачи).
Возможно, вдохновленные пунктом 1, многие программисты не только используют goto
исключительно для обработки ошибок, но и обрабатывают ошибки исключительно с помощью goto
! К сожалению, проверка ошибок может составлять 90% изменений потока управления во многих программах на языке C, в зависимости от того, что программист произвольно обозначил как «ошибку». Это соглашение, в соответствии с которым каждая проверка на ошибку является условной goto
, настолько далеко от "редко, если вообще", что было бы смешно, если бы не было так трагично.
Каждая функция, которую я нахожу со сломанной обработкой ошибок, представляет собой крысиное гнездо goto. Я не говорю, что невозможно написать правильный код, используя goto
; Я думаю, что мое анекдотическое наблюдение, скорее всего, является отражением того факта, что программисты, которые думают, что goto
упрощает их код, не проявляют достаточной осторожности, чтобы убедиться, что он правильный. Я склонен так думать, потому что вижу множество аргументов в пользу goto
, которые сводятся к «я не хочу думать». Откровенно говоря, если вы не хотите думать, вам не следует писать программы на C.
Функция, в которой goto
используется слишком часто, имеет тот же поток управления, что и эквивалентная функция, содержащая вложенные блоки if
(так называемый «антишаблон стрелки»), за исключением того, что реальная структура функции была сглажена и запутана с помощью произвольно придуманные (и, возможно, вводящие в заблуждение) названия ярлыков:
void test(void) { FILE *fmin = fopen("testmin", "rb"); if (!fmin) { fputs("Open testmin failed\n", stderr); goto fmin_fail; } FILE *fsub = fopen("testsub", "rb"); if (!fsub) { fputs("Open testsub failed\n", stderr); goto fsub_fail; } FILE *fdiff = fopen("testdiff", "wb"); if (!fdiff) { fputs("Open testdiff failed\n", stderr); goto fdiff_fail; } int const minuend = fgetc(fmin), subtrahend = fgetc(fsub); if (minuend == EOF || subtrahend == EOF) { fputs("Read failed\n", stderr); } else if (fputc(minuend - subtrahend, fdiff) == EOF) { fputs("Write failed\n", stderr); } if (fclose(fdiff)) { fputs("Close testdiff failed\n", stderr); } fdiff_fail: fclose(fsub); fsub_fail: fclose(fmin); fmin_fail: return; }
Я искренне возмущаюсь проверять порядок и наименование таких меток при просмотре кода. Вложенные блоки if
тоже не идеальны, но, по крайней мере, они честны и визуально выровнены:
void test(void) { FILE *fmin = fopen("testmin", "rb"); if (!fmin) { fputs("Open testmin failed\n", stderr); } else { FILE *fsub = fopen("testsub", "rb"); if (!fsub) { fputs("Open testsub failed\n", stderr); } else { FILE *fdiff = fopen("testdiff", "wb"); if (!fdiff) { fputs("Open testdiff failed\n", stderr); } else { int const minuend = fgetc(fmin), subtrahend = fgetc(fsub); if (minuend == EOF || subtrahend == EOF) { fputs("Read failed\n", stderr); } else if (fputc(minuend - subtrahend, fdiff) == EOF) { fputs("Write failed\n", stderr); } if (fclose(fdiff)) { fputs("Close testdiff failed\n", stderr); } } fclose(fsub); } fclose(fmin); } }
(Позже я предложу третий вариант, который фактически упрощает эту функцию.)
Некоторые программисты ненавидят отступы: они вызывают у них беспокойство. В какой-то степени это рационально, так как количество отступов является одним из показателей сложности функции. С другой стороны, мониторы больше не ограничены 80 столбцами, и сокрытие структуры функции не упрощает ее. Если вы хотите сделать отступ в коде Python, вам следует проявить такую же любезность к C; если нет, вам, вероятно, следует подумать о карьере, написанной на ассемблере.
Я пришел к выводу, что реальная проблема с запретом goto
заключается в том, что он нравится программистам, а не в том, что он им нужен или что он делает программы лучше. Как и темная сторона Силы, она быстрее, проще и соблазнительнее. Это как пиво для программистов. Конечно, кто-то однажды пытался это запретить. Одной из основных причин отмены запрета в США было то, что налоговые поступления можно было увеличить за счет налогообложения пива. Если бы использование goto
облагалось налогом, программисты могли бы дважды подумать, прежде чем засорять им свой код.
Когда талибы с гордостью объявили миру, что «права женщин будут уважаться — в рамках ислама», у меня возникло искушение объявить о моем собственном реформированном, прагматичном и сострадательном подходе к программированию на языке C, который уважает право каждого на использование goto
— в рамках пределы K&R. (То есть редко, если вообще). На самом деле я склонен согласиться со Страуструпом в том, что убеждение более желательно и эффективно, чем запрет.
В следующих примерах используются функции <stdio.h>
стандартной библиотеки C, поскольку все должны быть с ними знакомы.
Отложенная обработка ошибок
Мое первое предпочтение шаблону обработки ошибок — вообще не ветвление при ошибке, учитывая, что эффективность сбойной программы не имеет значения. Эту стратегию легко реализовать при написании кода сериализации, поскольку объект FILE
хранит состояние ошибки потока:
typedef struct { uint32_t count; unsigned char data[100]; } foo_t; static unsigned char const magic[] = {'S', 'O', 'U', 'L'}; static bool save_file(foo_t const *const obj, char const *const filename) { FILE *const f = fopen(filename, "wb"); if (NULL == f) { fprintf(stderr, "Open %s failed\n", filename); return false; } fwrite(magic, sizeof(magic), 1, f); for (size_t i = 0; i < sizeof(obj->count); ++i) { fputc((obj->count >> (CHAR_BIT * i)) & UCHAR_MAX, f); } fwrite(obj->data, obj->count, 1, f); bool err = ferror(f); if (fclose(f)) { err = true; } if (err) { fprintf(stderr, "Write to %s failed\n", filename); } return !err; }
Этот шаблон использования легко поддерживать и при разработке собственных интерфейсов. Другой пример — функция OpenGL glGetError.
Обработка ошибок при раннем выходе из функции
Второе, что я предпочитаю для обработки ошибок, — следовать шаблону allocate-call-free. Основная причина, по которой программисты goto
ставят метку ближе к концу функции, состоит в том, чтобы избежать утечки ресурсов. Если бы те же ресурсы были выделены в вызывающей функции, то вызываемый объект мог бы вернуться напрямую без утечки. Следовательно, я пришел к выводу, что ранний выход (т. е. return
) является самым мощным механизмом структурирования программы на языке C, хотя это и отклонение от структурного программирования в чистом виде:
typedef enum { ERROR_NONE, ERROR_READ_FAIL, ERROR_BAD_MAGIC, ERROR_TOO_BIG, } error_t; static error_t deserialize(foo_t *const obj, FILE *const f) { unsigned char hdr[sizeof(magic)]; if (fread(hdr, sizeof(hdr), 1, f) != 1) { return ERROR_READ_FAIL; } if (memcmp(hdr, magic, sizeof(magic))) { return ERROR_BAD_MAGIC; } obj->count = 0; for (size_t i = 0; i < sizeof(obj->count); ++i) { int const c = fgetc(f); if (c == EOF) { return ERROR_READ_FAIL; } obj->count |= (uint32_t)c << (CHAR_BIT * i); } if (obj->count > sizeof(obj->data)) { return ERROR_TOO_BIG; } if (fread(obj->data, obj->count, 1, f) != 1) { return ERROR_READ_FAIL; } return ERROR_NONE; } static bool load_file(foo_t *const obj, char const *const filename) { FILE *const f = fopen(filename, "rb"); if (!f) { fprintf(stderr, "Open %s failed\n", filename); return false; } error_t const err = deserialize(obj, f); switch (err) { case ERROR_READ_FAIL: fprintf(stderr, "Read from %s failed\n", filename); break; case ERROR_BAD_MAGIC: fprintf(stderr, "Bad magic values in %s\n", filename); break; case ERROR_TOO_BIG: fprintf(stderr, "Too much data in %s\n", filename); break; } fclose(f); return err == ERROR_NONE; }
Обратите внимание, что функция deserialize
даже не знает имени файла, из которого она читает. Это хорошо, потому что это может быть вовсе не файл — это может быть stdin
.
Отделение выделения ресурсов от обработки также может повысить производительность, поскольку поощряет повторное использование ресурсов. Следующий отрывок взят из конвертера формата сетки 3D-объектов. Функция process_object
не выделяет ничего, что не связано с varray
или groups
, а также не открывает и не закрывает файлы models
или out
. Следовательно, любая память, выделенная для каждого объекта, используется для следующего, и ни памяти, ни файловых дескрипторов не происходит утечка:
Group groups[Group_Count]; for (int g = 0; g < Group_Count; ++g) { group_init(groups + g); } VertexArray varray; vertex_array_init(&varray); int object_count; for (object_count = 0; !stop && success; ++object_count) { success = process_object(models, out, object_name, object_count, &varray, &groups, &vtotal, &list_title, thick, data_start, flags); } for (int g = 0; g < Group_Count; ++g) { group_free(groups + g); } vertex_array_free(&varray);
Эта идея присоединения ресурсов к объекту, переданному вызывающей функцией, широко применима и делает тестирование на наличие утечек очень скучным, потому что их никогда не бывает. Кто-то однажды заметил, что я думал, что мой код базирован. Вы тоже можете писать основанный код, просто лучше используя функции.
Обработка ошибок с помощью фиктивного цикла
При искушении записать несколько операторов goto
в одну метку очистки рассмотрите альтернативу цикла do...while
с одной итерацией:
bool subtractor(char const *const filename) { FILE *const f = fopen(filename, "rb"); if (!f) { fprintf(stderr, "Open %s failed\n", filename); return false; } bool success = false; do { unsigned char minuends[32]; if (1 != fread(minuends, sizeof(minuends), 1, f)) { continue; } unsigned char subtrahends[sizeof(minuends)]; if (1 != fread(subtrahends, sizeof(subtrahends), 1, f)) { continue; } for (size_t i = 0; i < sizeof(minuends); ++i) { printf("%d - %d = %d\n", minuends[i], subtrahends[i], minuends[i] - subtrahends[i]); } success = true; } while(0); if (!success) { fprintf(stderr, "Read from %s failed\n", filename); } fclose(f); return success; }
Эта идиома может быть полезна в тех случаях, когда вы не хотите выделять ресурсы в вызывающей функции (возможно, из-за необходимости передачи чрезмерного количества аргументов). По сравнению с goto
он имеет то преимущество, что вам не нужно придумывать имя метки и набирать его повторно, кроме того, он делает структуру кода очевидной с первого взгляда, поскольку блок, в котором может произойти досрочный выход (эквивалентно блок try
) имеет отступ.
Хороший способ запомнить эту идиому — известное высказывание Йоды: «Делай или не делай. Нет никакой попытки».
Часто не имеет значения, используется ли break
для раннего выхода из цикла или continue
для перехода к концу тела цикла. Как правило, continue
может быть предпочтительнее, поскольку его также можно использовать в любых операторах switch
, вложенных в тело цикла.
В любом случае, эта идиома не допускает ранний выход из вложенных циклов, как это требуется в приведенной выше функции deserialize
. В таких случаях лучше провести рефакторинг в отдельные функции и использовать return
вместо break
или continue
.
Обработка ошибок при раннем выходе из цикла распределения
Если функция выделяет несколько ресурсов одного и того же типа, может быть лучше выполнить итерацию по массиву этого типа вместо использования вложенных блоков if
. Перечисление — хороший способ назвать индексы массива:
void test(void) { enum { FILE_MIN, FILE_SUB, FILE_DIFF, FILE_COUNT }; static struct { char const *name, *mode; } const files[FILE_COUNT] = { [FILE_MIN] = {"testmin", "rb"}, [FILE_SUB] = {"testsub", "rb"}, [FILE_DIFF] = {"testdiff", "wb"} }; FILE *f[FILE_COUNT]; size_t nopen; for (nopen = 0; nopen < FILE_COUNT; ++nopen) { f[nopen] = fopen(files[nopen].name, files[nopen].mode); if (!f[nopen]) { fprintf(stderr, "Open %s failed\n", files[nopen].name); break; } } if (nopen == FILE_COUNT) { int const minuend = fgetc(f[FILE_MIN]), subtrahend = fgetc(f[FILE_SUB]); if (minuend == EOF || subtrahend == EOF) { fputs("Read failed\n", stderr); } else if (fputc(minuend - subtrahend, f[FILE_DIFF]) == EOF) { fputs("Write failed\n", stderr); } } while (nopen-- > 0) { if (fclose(f[nopen])) { fprintf(stderr, "Close %s failed\n", files[nopen].name); } } }
Часто нет необходимости называть индексы массива:
#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) static struct { int event_code; WimpEventHandler *handler; } const wimp_handlers[] = { { Wimp_ERedrawWindow, redraw_window }, { Wimp_EOpenWindow, open_window }, { Wimp_ECloseWindow, close_window }, { Wimp_EMouseClick, mouse_click }, }; static void deregister_wimp_handlers(EditWin *const edit_win, size_t i) { while (i-- > 0) { event_deregister_wimp_handler(edit_win->window_id, wimp_handlers[i].event_code, wimp_handlers[i].handler, edit_win); } } static bool register_wimp_handlers(EditWin *const edit_win) { for (size_t i = 0; i < ARRAY_SIZE(wimp_handlers); i++) { if (E(event_register_wimp_handler(edit_win->window_id, wimp_handlers[i].event_code, wimp_handlers[i].handler, edit_win))) { deregister_wimp_handlers(edit_win, i); return false; } } return true; }
Обработка ошибок с помощью конечного автомата
Часто разные типы ресурсов выделяются одной функцией. В таких случаях нельзя использовать простой цикл распределения.
Обычно я использую комбинацию уже описанных методов, чтобы ограничить глубину вложенности условных блоков в пределах одной функции. В редких случаях использование конечного автомата для управления потоком управления может быть оправдано. Это расширяет концепцию цикла распределения: вместо того, чтобы использовать перечислители в качестве индексов массива, они используются как состояния в операторе switch
. Один и тот же код можно использовать как для обработки ошибок, так и для обычного уничтожения объекта:
typedef struct { void *buffer; FILE *file; } object_t; typedef enum { INIT_STATE_FIRST, INIT_STATE_BUFFER = INIT_STATE_FIRST, INIT_STATE_FILE, INIT_STATE_LAST } init_state_t; static object_t *partial_destructor(object_t *const o, init_state_t state) { /* state is the failed initialization step, so start destruction at the previous step. */ while (state-- > INIT_STATE_FIRST) { switch (state) { case INIT_STATE_BUFFER: free(o->buffer); break; case INIT_STATE_FILE: fclose(o->file); break; } } free(o); return NULL; } void destructor(object_t *const o) { partial_destructor(o, INIT_STATE_LAST); } object_t *constructor(char const *const filename, size_t const buf_size) { object_t *const o = malloc(sizeof(*o)); if (!o) { fputs("Memory allocation failed\n", stderr); return NULL; } for (init_state_t state = INIT_STATE_FIRST; state < INIT_STATE_LAST; ++state) { switch (state) { case INIT_STATE_BUFFER: o->buffer = malloc(buf_size); if (!o->buffer) { fprintf(stderr, "Memory allocation of %zu failed\n", buf_size); return partial_destructor(o, state); } break; case INIT_STATE_FILE: o->file = fopen(filename, "rb"); if (!o->file) { fprintf(stderr, "Open %s failed\n", filename); return partial_destructor(o, state); } break; } } return o; }
Обычный вариант этой идиомы состоит в том, чтобы исключить цикл в деструкторе и вместо этого полагаться на переход между операторами case
:
static object_t *partial_destructor(object_t *const o, init_state_t state) { if (state-- > INIT_STATE_FIRST) { switch (state) { case INIT_STATE_FILE: fclose(o->file); // fallthrough case INIT_STATE_BUFFER: free(o->buffer); } } free(o); return NULL; }
Я не одобряю этот вариант, потому что он делает деструктор хрупким: изменение порядка перечисления больше не меняет порядок освобождения ресурсов, что может привести к утечкам или попыткам освободить ресурсы, которые никогда не выделялись. Другими словами, он так же подвержен ошибкам, как и эквивалентный код, написанный с использованием набора меток вместо операторов case
.
Отсутствие операторов break
даже не дает преимущества краткости, потому что линтеры требуют явного аннотирования сквозных ошибок. Эффективность уничтожения объекта редко бывает значительной, и компилятор все равно может развернуть цикл деструктора.
возражения
Я чувствую, что рефакторинг load_file на две функции был сделан несколько произвольно.
Он использует средства, которые C предоставляет для создания структуры программы. Это правда, что структура отражает ограничения программирования на языке, в котором отсутствуют автоматические деструкторы, но в этом разница между хорошо написанной программой на C и программой на C, написанной в том же стиле, что и на другом языке с меньшим количеством ограничений. Вот почему программирование на C — это ремесло, а не просто написание кода.
Мне нравятся метки с именами out1, out2 и out3, чтобы мой код обработки ошибок выглядел как стек.
Но в C есть стек… и вложенность… и вызовы функций.
Но что, если я хочу сделать уборку перед возвращением?
Тогда вы, вероятно, неправильно структурировали свою программу, особенно если вы, вероятно, вернетесь во многих местах и захотите выполнить ту же очистку. В равной степени возможно, что вы не всегда хотите выполнять очистку (например, в примере, который я привел, где FILE *
может быть stdin
), поэтому программы должны состоять из функций, а не goto
утверждения и ярлыки.
Рефакторинг кода в другую функцию на самом деле не решает проблему сложности, он просто переносится куда-то еще.
«Удар по банке» — это суть поэтапного уточнения, абстракции и всего хорошего в программной инженерии.
Версия goto в C не противоречит структурному программированию, поскольку разрешена только внутри функций.
Я не думаю, что с этим согласуется какая-либо правдоподобная интерпретация структурного программирования. Во всяком случае, формальное определение структурного программирования строже, чем то, что я отстаиваю.
Goto — это просто слегка смягченная версия break, continue и return.
Это правда, что break
, continue
и return
— это отклонения от структурного программирования в его самой строгой форме, но убеждение людей — это баланс кнута и пряника. Я бы не назвал ограничение ветвлений до конца цикла, конца функции или оператора сразу после окончания текущего цикла незначительным ограничением, поэтому я не назвал бы снятие этих ограничений ограничением. также небольшое расслабление.
Обычно мы читаем функции сверху вниз. Читая функцию, мы можем отслеживать все прочитанные нами метки.
Незнание того, куда переходит выполнение программы, без чтения всей программы практически является определением неструктурированной программы. Функция — это просто подпрограмма.
Можно ли использовать goto исключительно для прямых ветвей?
Некоторые стандарты кодирования допускают это, но человек, читающий код, не обязательно знает, какой стандарт действовал, когда он был написан, и придерживался ли автор этого стандарта. Напротив, нет никакой двусмысленности в том, будут ли break
и continue
переходить вперед (потому что они всегда делают это).
Goto — лучшее, на что мы можем надеяться в отсутствие автоматических деструкторов.
Тот факт, что C++ позволяет вам не думать о последствиях приобретения ресурса, не означает, что вы должны придерживаться тех же привычек, что и в C. Структурированная программа на C не выглядит так, как программа на C++ с нагрузкой goto
и добавлены метки. (Или вызовы longjmp
, если вы также пытаетесь эмулировать исключения C++.)
Использование do…while для обработки ошибок вводит в заблуждение, поскольку предполагает итерацию.
Это уже распространенная идиома для определений макросов; единственная разница здесь в том, что цикл не скрыт препроцессором. После того, как я привык к этому, использование do
вместо try
показалось мне естественным. В любом случае неверно предполагать, что любой цикл имеет более одной итерации (или более нуля итераций в случае while
или for
циклов).
Что, если бы мне пришлось инициализировать мьютекс между двумя другими типами инициализации?
Очевидный ответ: «Не пишите такой код». Почти всегда лучше сгруппировать распределение ресурсов по типу. В противном случае используйте конечный автомат.
Я предпочитаю метки, потому что не знаю, куда вы переходите, не читая остальную часть функции.
Этот аргумент не применяется к циклам do...while
, где конец цикла очевиден. В других случаях начало и конец содержащего цикла (или switch
) должны быть визуально выровнены.
По крайней мере, я могу искать ярлыки по названию.
Это интересная критика синтаксиса C (и большинства современных языков), которая не имеет ничего общего с обработкой ошибок. Доведенный до абсурда вывод, всегда следует использовать goto
и никогда не использовать else
, break
или continue
. Я не думаю, что возможность поиска перевешивает преимущество знания того, что поток управления программой отклоняется от ее очевидной структуры ограниченным и предсказуемым образом.
Государственная машина кажется массивной сверхинженерией.
Это это чрезвычайно сложное решение проблемы с игрушками, которую я представил. Однако когда вы видели функции, содержащие сотни меток, перемешанных с логикой препроцессора, вы могли чувствовать себя по-другому. Полезно иметь в своем арсенале инструмент, который масштабируется до неограниченного количества инициализаций (в отличие от goto
, который требует строгого обратного порядка завершения без предоставления каких-либо средств проверки этого, кроме того, что вызывает утомление глаз и головную боль).
Я бы не стал доверять себе, чтобы правильно реализовать обратный цикл while.
Хотя использование оператора постдекремента в операторе a while
может сначала показаться запутанным, это распространенная идиома, так что вы можете выучить ее (точно так же, как когда-то научились писать идиоматический цикл for
).