Наука — Школе |
С оператором GOTO к настоящему времени достигнута полная ясность: в языке программирования для серьезной работы оператора GOTO быть не должно. История его изгнания весьма поучительна.
Наличие
GOTO в машинном языке понятно: там важна экономия
средств, а с помощью одного этого
оператора можно смоделировать любые структуры управления (операторы IF,
FOR, WHILE и т.д.). По той же причине в 50-е
годы
GOTO был автоматически включен в первые языки
программирования (например, фортран): ведь еще не было ясно, ни что такое
хорошие программы, ни как их строить.
К концу 60-х гг. анализ уже накопленного опыта выявил (см. знаменитое письмо
выдающегося теоретика систематического программирования Э.Дейкстры
Go To
Statement Considered Harmful), что бесконтрольное использование
GOTO приводит к «спагетти» — программам, хаотически напичканным операторами GOTO, в
которых отследить все возможные состояния вычислительного процесса — а
следовательно, и убедиться в корректности программы — становится практически невозможно.
Вслед за работами Дейкстры и др. возникла методология
структурного программирования.
(Здесь часто говорят о «программировании без GOTO», хотя
это слишком грубое упрощение.)
В структурном программировании был найден минимальный набор управляющих структур для систематического построения
эффективных программ (IF, WHILE и т.п.; все они представлены в
Обероне/Компонентном Паскале).
В 70-е гг. «проблема GOTO» еще активно дискутировалась, но к настоящему времени
вопрос исчерпан: GOTO не нужен и вреден, и защищать его
не возьмется ни один серьезный специалист. Наличие GOTO в старых языках
— следствие необходимости обеспечить совместимость с
«программным
наследием»; а его наличие в новых — признак некомпетентности
проектировщика.
Но почему оператор GOTO следует вообще исключать из языка программирования, ведь, казалось бы, им можно просто не пользоваться, оставив лишь на всякий пожарный случай? Ответ заключается в следующем.
В эволюции больших программных проектах наблюдается феномен насыщения степеней свободы языка программирования, т.е. тендеция к использованию всех возможных средств языка, причем проконтролировать это трудно. Насыщение происходит по ряду причин: участие многих программистов, значительная часть которых относится к категории самоучек; использование программ, написанных вне данного проекта; соблазн поставить быструю «заплатку» — соблазн зачастую непреодолимый под давлением жесткого графика.
Но такие заплатки как ржавчина: стоит ржавчине появиться в одной точке, и она начинает расти, разъедая конструкцию.
Проблема усугубляется в случае программистов-самоучек — а
таких большинство, т.к. цифровая революция развивается слишком быстро, требуя бОльшего
числа специалистов, нежели может поставлять система образования, сама в
значительной степени построенная из самоучек: нередко «образование» сводится к семестровому курсу, в котором объясняется
синтаксис фортрана или C под сопровождением благоглупостей
вроде «программирование — просто раздел прикладной математики» или «настоящему
программисту все равно, на каком языке писать».
Программисты-самоучки, особенно начинающие, нередко убеждены, что «крутизна» программирования в том, чтобы как можно хитроумней использовать как можно больше
средств языка в каждой программе: ведь им пришлось учить про них к экзамену!
Из документации, где просто
перечислены все средства — как и из большинства руководств, сляпанных по
принципу разжевывания документации — невозможно узнать, что то или иное средство
было просто сделанной когда-то ошибкой дизайна.
Поэтому наилучшее решение проблемы GOTO — радикальное: просто исключить его из языка.
Пример. Пусть дан массив из n целочисленных элементов a[0] ... a[n-1]. Описание на Обероне/Компонентном Паскале:
VAR a: ARRAY n OF INTEGER;
Пусть про этот массив известно, что один из элементов равен x. Требуется найти этот элемент и записать его позицию в целую переменную i.
Типичный начинающий программист (и многие «профессионалы») сразу напишет FOR c вложенной проверкой и начнет искать способ выйти из цикла:
FOR i := 0 TO n-1 DO
IF a[i] = x
THEN ... END
END
Но в Обероне/Компонентном Паскале из цикла FOR выйти просто так нельзя. Если не отказываться от цикла FOR (см. ниже решение этой задачи с циклом WHILE), то единственный способ это сделать — заключить цикл в процедуру и использовать оператор RETURN, позволяющий выйти из процедуры в произвольной точке:
PROCEDURE Найти
(VAR pos: INTEGER);
VAR i:
INTEGER;
BEGIN
FOR i := 0 TO n-1
DO
IF a[i] = x THEN pos := i; RETURN END
END
END Найти
Тогда в основной программе вместо цикла нужно вызвать эту процедуру:
Найти( i )
Еще можно заменить FOR на
оператор LOOP, в теле которого допускается выход на конец
цикла (оператор EXIT).
Но ни в
том, ни в другом случае нельзя «выпрыгнуть» вообще в произвольную точку вне цикла:
RETURN «прыгает» на конец процедуры, а EXIT
— на конец цикла. Таким образом программа остается достаточно
структурированной.
Кстати, вот классическое «правильное» решение в стиле Дейкстры, выраженное на Обероне/Компонентном Паскале:
i := 0;
WHILE (i < n) & (a[i] # x) DO
i := i + 1
END
Напомним, что вычисление логических выражений в Обероне/Компонентном Паскале производится по правилу «короткого замыкания»: если результат логической операции можно определить по первому операнду, то второй не вычисляется.
В данном случае по достижении i = n
первый операнд (i < n) даст FALSE,
так что результатом операции & (логическое И)
тоже будет FALSE, и выход из цикла произойдет сразу, без
вычисления второго операнда. Заметим, что вычисление второго операнда привело бы
к ошибке и аварийной остановке программы из-за выхода за верхнюю границу массива.
(О
проверках выхода за границы массивов в Обероне/Компонентном Паскале см.
здесь.)
Правило весьма удобно при систематической разработке алгоритмов и приводит к лаконичным программам; одно это правило исключает множество ситуаций, где наивный инстинкт требует использовать GOTO. Много примеров на этот счет — в том числе гораздо менее тривиальных, чем приведенный — дано в книгах Гриса и Дейкстры в нашем списке.
Заметим еще, что по приведенной стандартной схеме решается большое число задач, сводящихся к задаче поиска:
<инициализация цикла>
WHILE <условие ограничения
поиска> & ~(<условие
окончания поиска>) DO
i := i + 1
END
Логическое значение, выражающее успех поиска, равно значению выражения <условие ограничения поиска> после выхода из цикла.
Наука — Школе |