Введение
Парадигма программирования, основанная на потоке управления. Структурированное программирование – это парадигма программирования, направленная на повышение ясности, качества и сокращение времени разработки компьютерной программы за счет широкого использования структурированных конструкций управления потоком, таких как выбор (if/then/else) и повторение (while и for), блочных структур и подпрограмм. Она возникла в конце 1950-х годов с появлением языков программирования ALGOL 58 и ALGOL 60, причем последний поддерживал блочные структуры. Факторами, способствовавшими его популярности и широкому распространению, сначала в академической среде, а затем среди практиков, стали открытие теоремы о структурированных программах в 1966 году и публикация в 1968 году влиятельного открытого письма «Go To Statement Considered Harmful» нидерландского ученого-компьютерщика Эдсгера Вибе Дикстры, который и ввел термин «структурированное программирование». Структурированное программирование часто используется с некоторыми отклонениями, допускающими создание более понятных программ в определенных случаях, например, при обработке исключений.
Подпрограммы
Подпрограммы – это вызываемые единицы, такие как процедуры, функции, методы или подпрограммы, используемые для обозначения последовательности операторов одним оператором.
Блоки
Блоки используются для того, чтобы группы операторов рассматривались как единый оператор. Блочно-структурированные языки имеют синтаксис для оформления структур каким-либо формальным способом, например, оператор if, заключенный в конструкции if fi, как в ALGOL 68, или раздел кода, заключенный в BEGIN END, как в PL/I и Pascal, отступы в коде, как в Python, или фигурные скобки { } в C и многих последующих языках.
Исход
К концу XX века почти все ученые-компьютерщики были убеждены в полезности изучения и применения концепций структурированного программирования. Высокоуровневые языки программирования, которым изначально не хватало структур программирования, такие как FORTRAN, COBOL и BASIC, теперь ими обладают.
Общие отклонения
Хотя оператор goto в настоящее время в значительной степени заменен структурированными конструкциями выбора (if/then/else) и повторения (while и for), немногие языки программирования являются полностью структурированными. Наиболее распространенным отклонением, встречающимся во многих языках, является использование оператора return для преждевременного выхода из подпрограммы. Это приводит к наличию множественных точек выхода, вместо единственной точки выхода, требуемой принципами структурированного программирования. Существуют и другие конструкции для обработки ситуаций, которые неудобно реализовать в рамках чисто структурированного программирования.
Ранний выход
Наиболее распространенным отклонением от структурированного программирования является ранний выход из функции или цикла. На уровне функций это оператор `return`. На уровне циклов это оператор `break` (завершение цикла) или оператор `continue` (завершение текущей итерации, переход к следующей итерации). В структурированном программировании их можно воспроизвести, добавляя дополнительные ветви или проверки, но для возврата из вложенного кода это может привести к значительной сложности. Язык C является ранним и заметным примером этих конструкций. Некоторые новые языки также имеют "помеченные операторы `break`", которые позволяют выйти не только из самого внутреннего цикла. Исключения также обеспечивают ранний выход, но имеют дополнительные последствия и поэтому рассматриваются ниже. Множественные точки выхода могут возникать по разным причинам, чаще всего потому, что подпрограмма больше не имеет работы (если возвращает значение, то завершила вычисление) или столкнулась с "исключительными" обстоятельствами, которые препятствуют продолжению, что требует обработки исключений. Наиболее распространенная проблема при раннем выходе заключается в том, что очистка или заключительные операторы не выполняются – например, выделенная память не освобождается, или открытые файлы не закрываются, что приводит к утечкам памяти или ресурсов. Это необходимо делать в каждой точке выхода, что делает код хрупким и может легко привести к ошибкам. Например, в процессе дальнейшей разработки разработчик может упустить оператор `return`, и действие, которое должно быть выполнено в конце подпрограммы (например, оператор трассировки), может не выполняться во всех случаях. Языки, не имеющие оператора `return`, такие как стандартный Pascal и Seed7, не имеют этой проблемы. Большинство современных языков предоставляют поддержку на уровне языка для предотвращения таких утечек; подробное обсуждение см. в разделе "Управление ресурсами". Чаще всего это делается с помощью защиты от раскрутки стека, которая гарантирует, что определенный код будет гарантированно выполнен при выходе из блока; это структурированная альтернатива блоку очистки и оператору `goto`. Это чаще всего известно как `try...finally` и считается частью обработки исключений. Использование нескольких операторов `return` с `try...finally` без исключений может показаться странным. Существуют различные методы инкапсуляции управления ресурсами. Альтернативный подход, в основном используемый в C++, – это "Инициализация – это приобретение ресурса" (Resource Acquisition Is Initialization), который использует нормальную раскрутку стека (освобождение переменных) при выходе из функции для вызова деструкторов локальных переменных с целью освобождения ресурсов. Кент Бек, Мартин Фаулер и их соавторы в своих книгах по рефакторингу утверждали, что вложенные условные операторы могут быть сложнее для понимания, чем определенная плоская структура с использованием нескольких точек выхода, обусловленных защитными условиями. В их книге 2009 года прямо говорится, что "одна точка выхода – не полезное правило. Ясность – ключевой принцип: если метод яснее с одной точкой выхода, используйте ее; в противном случае – нет". Они предлагают готовое решение для преобразования функции, состоящей только из вложенных условных операторов, в последовательность защищенных операторов `return` (или `throw`), за которой следует один незащищенный блок, предназначенный для кода общего случая, а защищенные операторы должны обрабатывать менее распространенные случаи (или ошибки). Герб Саттер и Андрей Александреску также утверждают в своей книге советов по C++ 2004 года, что требование единой точки выхода устарело. В своем учебнике 2004 года Дэвид Уотт пишет, что "многоточечные потоки управления с несколькими точками выхода часто желательны". Используя концепцию последовательности Теннента, Уотт единообразно описывает конструкции потока управления, встречающиеся в современных языках программирования, и пытается объяснить, почему некоторые типы последовательностей предпочтительнее других в контексте многоточечных потоков управления. Уотт пишет, что неограниченные операторы `goto` (последовательности переходов) плохи, потому что пункт назначения перехода не является самоочевидным для читателя программы, пока он не найдет и не изучит фактическую метку или адрес, являющийся целью перехода. В отличие от этого, Уотт утверждает, что концептуальное намерение последовательности `return` ясно из ее собственного контекста, без необходимости изучения ее назначения. Уотт пишет, что класс последовательностей, известных как "последовательности выхода" (escape sequencers), определяемые как "последовательность, которая завершает выполнение текстово заключающего оператора или процедуры", охватывает как выходы из циклов (включая многоуровневые выходы), так и операторы `return`. Уотт также отмечает, что, хотя операторы `goto` были несколько ограничены в языках, таких как C, где цель должна находиться внутри локального блока или охватывающего внешнего блока, этого ограничения недостаточно, чтобы сделать намерение операторов `goto` в C самоописываемым, и поэтому они все еще могут создавать "спагетти-код". Уотт также рассматривает, чем последовательности исключений отличаются от последовательностей выхода и переходов; это объясняется в следующем разделе этой статьи. В отличие от вышесказанного, Бертранд Мейер написал в своем учебнике 2009 года, что инструкции, такие как `break` и `continue`, "это просто старый `goto` в овечьей шкуре" и настоятельно не рекомендует их использование.
Многократная запись
Реже подпрограммы допускают множественные точки входа. Чаще всего это повторный вход в корутину (или генератор/полукорутину), где подпрограмма передает управление (и, возможно, значение), но затем может быть возобновлена с того места, где она остановилась. Такая модель программирования широко используется, в частности, для работы с потоками (особенно ввода-вывода), конечными автоматами и для обеспечения параллельности. С точки зрения выполнения кода, передача управления из корутины ближе к структурированному программированию, чем возврат из подпрограммы, поскольку подпрограмма фактически не завершается, а продолжит работу при повторном вызове – это не преждевременный выход. Однако корутины означают, что несколько подпрограмм имеют собственное состояние выполнения, а не единый стек вызовов подпрограмм, что вносит иную сложность. Крайне редко подпрограммы допускают вход в произвольную точку внутри себя, поскольку в этом случае состояние программы (например, значения переменных) может быть не инициализировано или неоднозначно, что очень похоже на использование оператора goto.
Государственные машины
Некоторые программы, особенно парсеры и коммуникационные протоколы, имеют ряд состояний, следующих друг за другом не так просто, как свести к базовым структурам, и некоторые программисты реализуют переходы между состояниями с помощью перехода к новому состоянию. Этот тип переключения состояний часто используется в ядре Linux. Однако эти системы можно структурировать, оформив каждое изменение состояния в виде отдельной подпрограммы и используя переменную для указания текущего состояния (см. батут). Альтернативно, их можно реализовать с помощью сопрограмм, которые обходятся без батута.