Структуры управления
7.7. Низкий уровень: операторы перехода
Комбинация наших трех фундаментальных механизмов — последовательности, цикла и условного оператора — дает необходимую основу (будучи дополненной подпрограммами) для построения управляющих структур, необходимых для написания программ.
Эти механизмы языка программирования имеют двойников в машинном коде, который для большинства компьютерных архитектур имеет много рудиментов. Обычно нам не приходится встречаться с машинным кодом, поскольку компиляторы ответственны за перевод программного текста с одного уровня на другой. Однако полезно понимать, как механизмы, доступные на уровне аппаратуры, способны управлять потоком выполнения нашей программы.
Условное и безусловное ветвление (переходы)
Управляющие механизмы машинных языков (язык ассемблера, система команд компьютера) обычно включают:
- Безусловный переход: команду, которая передает управление команде с указанным адресом расположения в памяти. В ниже приведенном примере такая команда появляется как BR Address, где Address — адрес памяти целевой команды;
- Условный переход: передает управление по заданному адресу при выполнении условия или следующей команде, если условие не выполняется. В нашем примере проверяется условие равенства двух значений: BEQ Value1 Value2 Address. Мнемоника команды "Branch если EQual".
Для языка ассемблер (примеры приводятся на этом уровне) адреса являются условными. Фактические адреса памяти появляются в процессе загрузки программы.
Понятие команды с указанным адресом расположения в памяти следует из принципа хранимой в памяти программы, справедливого для всех компьютеров, которые хранят в своей памяти и данные, и программы. Команда перехода, которая ошибочно задает адрес памяти, не являющийся командой, служит причиной ошибочной работы, приводящей, как правило, к завершению работы программы.
Рассмотрим составной оператор:
if a = b then Составной_1 else Составной_2 End
В машинном коде это может выглядеть так
100 BEQ loc_a loc_b 104 101 …Код для составной_2 102 … 103 BR 106 104 …Код для составной_1… 105 … 106 …Код для продолжения программы…
Здесь loc_a и loc_b задают адреса памяти, хранящие значения. Числа слева задают адреса команд, начиная с адреса 100 (просто в качестве примера). Определение точного расположения в памяти машинного кода, связанного с каждым программным элементом, является непростой задачей. Ее решением занимаются разработчики компиляторов, а не прикладные программисты, специализирующиеся на разработке приложений.
По примеру кода для условного оператора вам придется, выполняя упражнение, построить структуру кода, который компилятор строит для цикла.
Оператор goto
Команды перехода, условные и безусловные, отражают базисные операции, которые может выполнять компьютер: проверять выполнение некоторых условий, таких как равенство двух значений, хранящихся в памяти, передавать управление команде, хранящейся в памяти по определенному адресу. Все языки программирования имели в свое время, а некоторые и до сих пор предлагают оператор goto, чье имя образовано слиянием двух английских слов "go to" (перейти к). В таких языках можно приписать каждому оператору программы метку:
some_label: some_instruction
Здесь some_label — это имя, которое вы даете оператору. Общепринято метку отделять от оператора символом двоеточие, хотя возможны и другие соглашения. При компиляции компилятор отображает метки в условные адреса (100. 101, ...), появляющиеся в нашем примере машинного кода. Языки с goto включают оператор безусловного перехода в форме:
goto label
Эффект выполнения этого оператора состоит в том, что управление передается оператору с меткой, заданной оператором перехода.
Вместо красивого условного оператора if condition then Compound_1 else Compound_2 end старые языки программирования имели более примитивный оператор выбора test condition simple_instruction, выполняющий simple_instruction, если condition истинно, в противном случае выполнялся оператор, следующий за тестом.
Такой оператор легко отображается в команды машинных языков, такие как BEQ. При использовании goto эквивалентным представлением оператора test будет следующий код:
test condition goto else_part Compound_2 goto continue else_part: Compound_1 continue:... Продолжение программы...
Это менее ясно, чем условный оператор с его симметричной иерархической структурой, в особенности с учетом вложенности. Игнорируя часть from, цикл можно было бы представить как:
start: test exit_condition goto continue Body goto start сontinue: ...Продолжение программы...
Поток управления включает два оператора перехода — две ветви, идущие в разных направлениях.
Блок-схемы
На последнем рисунке поток управления показан в виде так называемой блок-схемы программы или просто блок-схемы. Форма элементов блок-схемы стандартизована: ромбы для условий с двумя выходящими ветвями для True и False; прямоугольник для обрабатывающих блоков, здесь для Body. Можно проверить свое понимание блок-схем, изобразив схему для условного оператора.
В свое время блок-схемы были весьма популярны для выражения структуры управления программы. И сегодня их можно встретить в описании процессов, не связанных с программированием. В программировании они потеряли репутацию (некоторые авторы называют их теперь не "flowchart", а "flaw chart" — порочными схемами). Причины этого понятны. Если язык программирования предоставляет вам безусловный оператор перехода goto и условный оператор ветвления, такой как test condition goto label, то блок-схемы представляют способ отображения потока управления в период выполнения более понятный, чем программный текст, использующий goto. Теперь же такое представление устарело по двум причинам.
- Наши программы делают все более сложные вещи. Большие блок-схемы с вложенностью внутри циклов и условных операторов быстро становятся запутанными.
- Механизмы этой лекции — составной оператор, цикл, условный — обеспечивают высокий уровень выражения структур управления. Аккуратно отформатированный программный текст с отступами, отражающими уровень вложенности, дает лучшее представление о порядке выполнения операторов.
Переход от блок-схем к тщательно отобранным структурам управления противоречит клише "рисунок лучше тысячи слов". В ПО нам необходимы многие тысячи, фактически — миллионы слов, но критически важно, чтобы это были "правильные" слова. Из-за проблем с точностью картинки теряют свою привлекательность.
Корректность программ может зависеть от маленьких деталей, таких как использование условия i <= n вместо i < n; лучшие рисунки в мире полностью бесполезны, когда приходится иметь дело с правильным отображением таких аспектов.
7.8. Исключение GOTO и структурное программирование
Блок-схемы — не единственное, что вышло из употребления в программной инженерии с превращением ее в более солидную дисциплину: операторы goto также потеряли свою былую славу.
Goto вредны?
Причины потери доверия к goto практически те же, что и для блок-схем. Механизмы, которые мы уже изучили, предлагают лучшее управление. Приведу два аргумента.
- Конструкции цикла и условного оператора лучше читаются, чем goto — особенно для сложных структур с большой степенью вложенности. Особых доказательств не требуется — достаточно визуально сравнить оригинальные структуры и их goto-аналоги.
- Однако это еще не вся история. Остановившись на трех перечисленных механизмах, мы ограничиваем себя в сравнении с программистом, который может использовать произвольно оператор goto или, что эквивалентно, произвольные блок-схемы со стрелками, выходящими из любого блока и входящими в любой блок. Прозвищем таких переплетающихся структур является "блюдо спагетти". Образец такого "блюда" для довольно простого варианта с небольшим числом блоков показан на рисунке. Очевидно, что "наши" структуры управления понятнее и лучше читаются, чем блюдо спагетти. Но, с другой стороны, это ведь только методологический аргумент. Возможно ли, что, ограничив себя тремя структурами и исключив goto, мы потеряли нечто важное? Другими словами, существуют ли алгоритмы, не выразимые без полной мощи goto?
На этот вопрос ответ получен — нет! Теорема, доказанная в 1966 году двумя итальянскими учеными Коррадо Бёмом и Джузеппе Джакопини, говорит, что каждая блок-схема в теории вычислений имеет эквивалентное представление, использующее только последовательности и циклы (нет необходимости даже в условных операторах).
Corrado Bohm, Giuseppe Jacopini: Flow diagrams, Turing machines and languages with only two formation rules. Comm. of the ACM, vol. 9, no. 5, pages 366-371, May 1966.
Общие правила преобразования произвольных блок-схем в программы без goto, приведенные в этой статье, могут быть сложными. Для случаев, встречающихся на практике, часто возможно исключить эти схемы неформальным, но простым и понятным способом. Поскольку это более специальная тема (и она использует присваивание, формально еще не пройденное), ее обсуждение и специальные примеры приведены в приложении. В одном из упражнений, которое предстоит выполнить после чтения приложения, предлагается построить программу без goto для еще одного примера.
Жизнь без goto
Задача исключения, рассматриваемая в приложении, — это не та задача, которую придется решать в повседневной жизни. Нет необходимости писать программу с goto, а потом последовательно исключать этот оператор из программы. Следует ясным способом строить нашу программу, непосредственно применяя высокоуровневые структуры управления, которые доказали свою адекватность при построении алгоритмов, простых и сложных.
Почувствуй историю:
Сегодня "Go to" - почти бранное слово в программировании. Но так было не всегда. В свое время операторы перехода входили в состав основных структур. И вот в марте 1968 года в журнале Communications of the ACM была опубликована статья Эдсгера Дейкстры "О вреде оператора Goto". Чтобы избежать задержки с ее публикацией, Никлас Вирт, бывший тогда редактором журнала, напечатал ее в виде "Письма к редактору". Тщательно проведя обоснование, Дейкстра показал, что неограниченные операторы перехода пагубно влияют на качество программ.
Эта статья вызвала массу полемики. Тогда, как и теперь, программистам не нравится, когда их привычки ставятся под сомнение, - что все же временами случается. Но никто так и не смог привести веских аргументов в пользу неограниченного применения goto.
В короткой статье Дейкстры, которую следует прочитать каждому программисту, объяснялись те вызовы, с которыми приходится сталкиваться при проектировании программ.
Почувствуй мастера:
Наши интеллектуальные способности довольно хорошо приспособлены для управления статическими отношениями, но наши способности визуализации процессов, протекающих во времени, относительно слабы. По этой причине нам следует (как мудрым программистам, осознающим наши ограничения) сделать все возможное, чтобы сократить концептуальный разрыв между статической программой и динамическим процессом, установить столь простое соответствие между программой (разворачивающейся в пространстве текста) и процессом (разворачивающимся во времени), насколько это возможно.
Edsger W. Dijkstra, 1968
Никто, тогда или позже, не выразил эту мысль лучше. Программа, даже простая, представляет статический взгляд на широкую область возможных динамических вычислений, определенных на широкой области возможных входов. Эта область настолько широка, а во многих случаях потенциально бесконечна, что ее и изобразить невозможно. Тем не менее, чтобы быть уверенными в корректности наших программ, мы должны уметь выводить ее динамические свойства из статического текста.
Структурное программирование
Революция во взглядах на программирование, начатая Дейкстрой, привела к движению, известному как структурное программирование, которое предложило систематический, рациональный подход к конструированию программ. Структурное программирование стало основой всего того, что сделано в методологии программирования, включая и объектное программирование
Уже в первой книге по этой теме было показано, что структурное программирование — это не просто управляющие структуры и программирование без goto. Принципиальным посылом было рассмотрение программирования как научной дисциплины, базирующейся на строгих математических выводах (Дейкстра пошел дальше, описав программирование как "одну из наиболее трудных ветвей прикладной математики").
В массовом сознании остались, в первую очередь, две идеи — исключение goto и ограничение управляющих структур тремя видами, рассмотренными в этой лекции: последовательностью, циклом и альтернативой, часто называемых "управляющими структурами структурного программирования"
В отличие от этого, произвольные структуры управления (смотри блоки в блюде спагетти) могут иметь произвольное число входов и выходов. Ограничив себя строительными блоками с одним входом и одним выходом, мы получаем возможность построения произвольных алгоритмов любой сложности с помощью трех простых и надежных механизмов.
- Последовательное соединение: используйте выход одного элемента как вход к другому, подобно тому, как электрики соединяют выход сопротивления с входом конденсатора.
- Вложенность: используйте элемент как один из блоков внутри другого элемента.
- Функциональная абстракция: преобразуйте элемент, возможно, с внутренними элементами, в подпрограмму, также характеризуемую одним входом, одним выходом в потоке управления.
Теорема Бёма — Джакопини говорит нам, что мы не потеряли никакой выразительной силы, ограничив себя этими механизмами. Мы получаем существенные преимущества в простоте программы и ее читабельности, а, следовательно, в гарантировании ее корректности, возможности расширения и повторного использования.