GOTO
Материал из Википедии — свободной энциклопедии
GOTO (англ. go to — «перейти к») — в некоторых языках программирования — оператор безусловного перехода (перехода к определённой точке программы, обозначенной номером строки либо меткой). В более широком смысле, под «GOTO» подразумевают любой такой оператор, даже если в рассматриваемом языке он называется по-другому. В компилируемых языках GOTO можно рассматривать как основную операцию по передаче управления из одной части программы в другую, поскольку компилятор переводит все остальные операторы перехода в форму, аналогичную GOTO.
Содержание |
[править] Функциональность
В абсолютном большинстве языков программирования, поддерживающих его использование, оператор GOTO состоит из двух частей: собственно имени оператора и метки, маркирующей целевую точку перехода в программе, то есть имеет вид GOTO метка. Метка, в зависимости от правил языка, может быть либо числом (как, например, в классическом Бейсике), либо правильным идентификатором используемого языка программирования. Чтобы оператор перехода был корректным, необходимо наличие в тексте программы места, помеченного той же самой меткой, которая использована в данном операторе. Пометка может выглядеть по-разному, например, в языке Паскаль она имеет вид метка: (то есть имя метки, за которым следует двоеточие), возможны и другие соглашения.
Выполнение оператора перехода состоит в том, что следующим после него будет выполнен тот оператор программы, который стоит в тексте непосредственно за помеченным меткой местом (оператор, помеченный меткой), и далее будут последовательно выполняться операторы, расположенные после него (разумеется, до следующего оператора перехода, ветвления или цикла). В случае машинных языков (ассемблеров или непосредственно машинного кода) технический смысл команды перехода элементарен: она записывает в регистр процессора, хранящий адрес следующей выполняемой команды, адрес команды, помеченной меткой.
[править] Распространение
GOTO имеется в таких языках, как Фортран, Алгол, КОБОЛ, Бейсик, Си, C++, D, Паскаль, Perl, Ада, а также во многих других. GOTO присутствует также во всех языках ассемблера в форме JMP, JUMP или BRA (от англ. branch — ветвь) и используется там чрезвычайно активно. Свобода использования оператора GOTO в различных языках сильно различается. Если в ассемблерах или языках типа Фортрана он может применяться произвольно (допускается передача управления внутрь ветви условного оператора или внутрь тела цикла, а иногда и процедуры), то в более поздних языках высокого уровня его использование ограничено: как правило, с помощью GOTO запрещено передавать управление между различными процедурами и функциями, внутрь выделенного блока операторов, между ветвями условного оператора и оператора множественного выбора.
GOTO отсутствует в некоторых языках высокого уровня, например в Forth (но может быть реализовано средствами самого языка). В Паскаль GOTO первоначально включён не был, но недостаточность имеющихся языковых средств вынудила Никлауса Вирта его добавить. В более поздних своих языках Вирт всё же отказался от GOTO: этого оператора нет ни в Модуле-2, ни в Обероне и Компонентном Паскале. В Java есть зарезервированное слово goto, но оно не несёт никаких функций — оператора безусловного перехода в языке нет. При этом в языке сохранились метки — они могут применяться для выхода из вложенных циклов операторами break и continue.
[править] Критика
Оператор GOTO в языках высокого уровня является объектом критики, поскольку чрезмерное его применение нарушает иерархическую структуру программы и приводит к созданию нечитаемого «спагетти-кода». Впервые эта точка зрения была отражена в статье Эдсгера Дейкстры «Доводы против оператора GOTO»[1], который заметил, что качество программного кода обратно пропорционально количеству операторов GOTO в нём. Статья приобрела широкую известность как среди теоретиков, так и среди практиков программирования, в результате чего взгляды на использование оператора GOTO были существенно пересмотрены. В своей следующей работе Дейкстра обосновал тот факт, что для кода без GOTO намного легче проверить формальную корректность.
Код с GOTO трудно форматировать, так как он может нарушать иерархичность выполнения, и потому отступы, призванные отображать структуру программы, не всегда могут быть выставлены правильно. GOTO аннулирует многие возможности компилятора по оптимизации управляющих структур, из-за чего исполняемый код становится медленней и объёмней[2].
Доводы против оператора GOTO оказались столь серьёзны, что в структурном программировании его стали рассматривать как крайне нежелательный. Это нашло своё отражение при проектировании новых языков программирования. Например, GOTO был намеренно полностью запрещён в Java и Ruby. Вместе с тем, в Аде — одном из наиболее продуманных с точки зрения архитектуры языке за всю историю[3], GOTO всё же был оставлен.
Формально доказано, что применение GOTO не является обязательным (то есть не существует такой программы с GOTO, которую нельзя было бы переписать без этого оператора с полным сохранением функциональности). Одним из доводов против GOTO является то, что в некоторых случаях программисты ставят GOTO, чтобы быстрее достичь желаемого результата (написать фрагмент программы с определёнными свойствами), в то время как отсутствие GOTO заставило бы их провести более внимательный анализ задачи, который обычно приводит к получению более ясного, эффективного и лучше организованного кода.
[править] Оправданное применение
Тем не менее, в практическом программировании применение GOTO в некоторых случаях можно считать допустимым. Поскольку GOTO — «простейший», «атомарный» оператор перехода, а все остальные являются «составными», производными от него, то применение GOTO допустимо и оправданно, когда другие средства языка не реализуют или недостаточно эффективно реализуют нужную функциональность. К таким случаям можно отнести:
[править] Выход из нескольких вложенных циклов сразу
Обычно считается, что в языках, где операторы досрочного завершения цикла (такие, как break и continue в Си) могут относиться только к тому из вложенных циклов где в котором они расположены, использование goto допустимо, чтобы выйти из нескольких вложенных циклов сразу. Здесь GOTO значительно упрощает программу, избавляя от необходимости создания вспомогательных переменных-флагов и условных операторов.
Другие варианты решения этой проблемы — помещение вложенных циклов в отдельную процедуру и использование команды досрочного выхода из процедуры, а в языках с поддержкой исключений — генерацию исключения, обработчик которого располагается за пределами циклов. Однако подобные решения могут снижать производительность, в особенности если этот участок кода вызывается многократно (поскольку и вызовы процедур, и операторы работы с исключениями транслируются далеко не в одну машинную инструкцию).
Пример:
int matrix[n][m]; int value; ... for(int i=0; i<n; i++) for (int j=0; j<m; j++) if (matrix[i][j] == value) { printf("value %d found in cell (%d,%d)\n",value,i,j); //act if found goto end_loop; } printf("value %d not found\n",value); //act if not found end_loop: ;
Прямолинейный способ избавления от GOTO — создать дополнительную переменную-флаг, сигнализирующую, что надо выйти из внешнего цикла (после выхода из внутреннего по break) и обойти блок кода, выполняющийся, когда значение не найдено. Но вряд ли этот способ можно рекомендовать на практике, так как в результате код окажется загромождён проверками, станет длиннее и будет дольше работать.
Впрочем, в данном конкретном примере есть минимум два лучших решения. Во-первых, код в примере написан плохо, в нём смешан поиск значения в массиве и отображение результатов этого поиска. Можно легко вынести поиск в отдельную функцию, которая будет возвращать индексы найденного элемента и значение true/false в зависимости от того, найден элемент или нет. Замедления это не вызовет — вызов функции заведомо ничтожен по сравнению с обходом матрицы, а необходимость в goto исчезнет. Во-вторых, можно воспользоваться особенностями языка Си и обойти матрицу без вложенных циклов, с помощью одной переменной-указателя.
Без изменения структуры кода проблема решается, если команда break (или её аналог) позволяет выйти из нескольких вложенных блоков сразу, как в Java или Ada. Аналогичный код на Java никакого goto не требует:
int[][] matrix; int value; ... outer: { for(int i=0; i<n; i++) for (int j=0; j<m; j++) if (matrix[i][j] == value) { System.out.println("value " + value + " found in cell (" + i + "," + j + ")"); break outer; } System.out.println("value " + value + " not found"); }
Тем не менее, если специальной конструкции для выхода из вложенного цикла нет, в некоторых случаях из него удобнее выходить именно с помощью GOTO.
[править] Обработка ошибок
Этот случай применим к языкам, не содержащим конструкции try ... finally — например, к C без применения SEH, существующего только в Windows. В этом случае goto используется для перехода на код «очистки» — находящийся в конце функции и уничтожающий созданные ей объекты перед выходом из неё. Этот метод широко используется при написании драйверов.
Пример такой обработки ошибок (все имена и константы, кроме NULL, вымышлены и приведены лишь для примера):
int fn(int* presult) { int sts = 0; TYPE entity, another_entity = NULL; TYPE2 entity2 = NULL; if ((entity = create_entity()) == NULL) {sts = ERROR_CODE1; goto exit0;} if (!do_something(entity) ) {sts = ERROR_CODE2; goto exit1;} if ( condition ) { if ((entity2 = create_another_entity()) == NULL ) {sts = ERROR_CODE3; goto exit1;} if ((*presult = do_another_thing(entity2) == NEGATIVE ) {sts = ERROR_CODE4; goto exit2;} } else { if ((*presult = do_something_special(entity) == NEGATIVE) {sts = ERROR_CODE5; goto exit2;} } exit2: if (entity2) destroy_another_entity(entity2); exit1: destroy_entity(entity); exit0: return sts; }
Здесь без goto было бы совсем неудобно, поскольку ошибка может возникнуть в любом месте иерархии. Впрочем, и в данном случае необходимость в GOTO не безусловна. Например, при написании кода на C++ для избавления от GOTO достаточно поместить создаваемые фрагментом объекты внутрь класса, освобождающего ресурсы в деструкторе, а в функции fn просто создать локальные переменные соответствующих типов. Правильно написанный компилятор гарантирует вызов деструкторов и удаление локальных переменных при любом порядке завершения функции. Другое дело, что создатели драйверов часто пренебрегают этой возможностью, поскольку либо пишут код на чистом Си, либо не хотят дополнительных накладных расходов, которые возникают при использовании кода с классами (хотя они не так уж велики при правильной настройке компилятора), либо просто не владеют соответствующей техникой.
Главным критерием применимости goto во всех случаях, включая указанные, является ненарушение используемой парадигмы программирования. В приведенных примерах это — структурное программирование, то есть должны сохраняться иерархическая организация программы и таковая же логика её работы. Нарушение принципа иерархии (например: переходы внутрь цикла; обход операций инициализации — как явных, так и неявных; выход из кода, следующего за fork(), в код, предшествующий ему) чревато всевозможными побочными эффектами, возникающими из деталей трансляции программы в машинный код, и, как следствие, странными, труднообнаружимыми ошибками.

