Структурное программирование — это парадигма программирования, направленная на улучшение ясности, качества и времени разработки компьютерной программы путем широкого использования структурированных конструкций потока управления выбором ( if/then/else ) и повторением ( while и for ), блочных структур и подпрограмм .
Он появился в конце 1950-х годов с появлением языков программирования ALGOL 58 и ALGOL 60 , [1] причем последний включал поддержку блочных структур. Факторы, способствующие его популярности и широкому признанию, сначала в академических кругах, а затем среди практиков, включают открытие того, что сейчас известно как теорема о структурированной программе в 1966 году, [2] и публикацию влиятельного открытого письма " Go To Statement Considered Harmful " в 1968 году голландским ученым-компьютерщиком Эдсгером В. Дейкстрой , который ввел термин "структурированное программирование". [3]
Структурное программирование чаще всего используется с отклонениями, которые позволяют создавать более понятные программы в некоторых конкретных случаях, например, когда необходимо выполнять обработку исключений .
Следуя теореме о структурированной программе , все программы рассматриваются как состоящие из трех управляющих структур :
if..then..else..endif
. Условный оператор должен иметь по крайней мере одно истинное условие, и каждое условие должно иметь одну точку выхода максимум.while
, repeat
, forили do..until
. Часто рекомендуется, чтобы каждый цикл имел только одну точку входа (а в исходном структурном программировании также только одну точку выхода, и несколько языков требуют этого).Подпрограммы ; вызываемые единицы, такие как процедуры, функции, методы или подпрограммы, используются для того, чтобы можно было ссылаться на последовательность с помощью одного оператора.
Блоки используются для того, чтобы группы операторов можно было обрабатывать так, как если бы они были одним оператором. В языках с блочной структурой есть синтаксис для включения структур в некоторые формальные формы, например, оператор if, заключенный в скобки, if..fi
как в ALGOL 68 , или раздел кода, заключенный в скобки BEGIN..END
, как в PL/I и Pascal , отступы с пробелами , как в Python , или фигурные скобки {...}
в C и многих более поздних языках .
Структурное программирование возможно на любом языке программирования, хотя предпочтительнее использовать что-то вроде процедурного языка программирования . [4] [5] Некоторые из языков, изначально использовавшихся для структурного программирования, включают: ALGOL , Pascal , PL/I , Ada и RPL, но большинство новых процедурных языков программирования с того времени включали функции, поощряющие структурное программирование, а иногда намеренно исключали некоторые функции, в частности GOTO , чтобы затруднить неструктурированное программирование .
Структурное программирование (иногда называемое модульным программированием [4] ) навязывает логическую структуру разрабатываемой программе, делая ее более эффективной и простой для понимания и изменения.
Теорема о структурированной программе обеспечивает теоретическую основу структурного программирования. Она утверждает, что три способа объединения программ — последовательность, выбор и итерация — достаточны для выражения любой вычислимой функции . Это наблюдение не возникло в движении структурного программирования; эти структуры достаточны для описания цикла инструкций центрального процессора , а также работы машины Тьюринга . Следовательно, процессор всегда выполняет «структурированную программу» в этом смысле, даже если инструкции, которые он считывает из памяти, не являются частью структурированной программы. Однако авторы обычно приписывают результат статье 1966 года Бёма и Якопини, возможно, потому что Дейкстра сам цитировал эту статью. [6] Теорема о структурированной программе не рассматривает, как писать и анализировать полезно структурированную программу. Эти вопросы рассматривались в конце 1960-х и начале 1970-х годов, при этом значительный вклад внесли Дейкстра , Роберт У. Флойд , Тони Хоар , Оле-Йохан Даль и Дэвид Грис .
П. Дж. Плогер , один из первых принявших структурное программирование, так описал свою реакцию на теорему о структурированной программе:
Мы, новообращенные, размахивали этой интересной новостью перед носом у неперестроенных программистов на языке ассемблера, которые продолжали выдавать замысловатые логические построения и говорить: «Спорим, вы не сможете это структурировать». Ни доказательство Бёма и Якопини, ни наши неоднократные успехи в написании структурированного кода не заставили их измениться на день раньше, чем они были готовы убедить себя. [7]
Дональд Кнут принял принцип, что программы должны быть написаны с учетом доказуемости, но он не согласился с отменой оператора GOTO и по состоянию на 2018 год [обновлять]продолжал использовать его в своих программах. [8] В своей статье 1974 года «Структурное программирование с операторами Goto» [9] он привел примеры, в которых, по его мнению, прямой переход приводит к более ясному и эффективному коду без ущерба для доказуемости. Кнут предложил более слабое структурное ограничение: должно быть возможно нарисовать блок-схему программы со всеми прямыми ветвями слева, всеми обратными ветвями справа и без пересекающихся ветвей. Многие из тех, кто разбирается в компиляторах и теории графов , выступали за то, чтобы разрешить только сокращаемые потоковые графы . [ когда определяется как? ] [ кто? ]
Теоретики структурного программирования обрели крупного союзника в 1970-х годах после того, как исследователь IBM Харлан Миллс применил свою интерпретацию теории структурного программирования к разработке системы индексации для исследовательского файла The New York Times . Проект имел большой инженерный успех, и менеджеры других компаний ссылались на него в поддержку принятия структурного программирования, хотя Дейкстра критиковал то, как интерпретация Миллса отличалась от опубликованной работы. [10]
Еще в 1987 году все еще можно было поднять вопрос о структурном программировании в журнале по компьютерной науке. Фрэнк Рубин сделал это в том году в открытом письме под названием «GOTO считается вредным». [11] Последовали многочисленные возражения, включая ответ Дейкстры, который резко критиковал как Рубина, так и уступки, сделанные другими авторами в ответах на него.
К концу 20-го века почти все специалисты по информатике были убеждены, что полезно изучать и применять концепции структурного программирования. Высокоуровневые языки программирования, изначально не имевшие структур программирования, такие как FORTRAN , COBOL и BASIC , теперь их имеют.
Хотя goto теперь в значительной степени заменен структурированными конструкциями выбора (if/then/else) и повторения (while и for), лишь немногие языки являются чисто структурированными. Наиболее распространенным отклонением, встречающимся во многих языках, является использование оператора return для раннего выхода из подпрограммы. Это приводит к появлению нескольких точек выхода вместо одной, требуемой структурным программированием. Существуют и другие конструкции для обработки случаев, которые неудобны в чисто структурном программировании.
Наиболее распространенным отклонением от структурного программирования является ранний выход из функции или цикла. На уровне функций это return
оператор. На уровне циклов это break
оператор (завершить цикл) или continue
оператор (завершить текущую итерацию, перейти к следующей итерации). В структурном программировании это можно повторить, добавив дополнительные ветви или тесты, но для возвратов из вложенного кода это может значительно усложнить задачу. C является ранним и ярким примером таких конструкций. Некоторые новые языки также имеют «помеченные прерывания», которые позволяют выходить не только из самого внутреннего цикла. Исключения также допускают ранний выход, но имеют дополнительные последствия и, таким образом, рассматриваются ниже.
Множественные выходы могут возникать по разным причинам, чаще всего это происходит из-за того, что подпрограмме больше нечего делать (если возвращается значение, она завершила вычисления), или из-за «исключительных» обстоятельств, которые не позволяют ей продолжить работу, и поэтому требуется обработка исключений.
Наиболее распространенной проблемой при раннем выходе является то, что очистка или финальные операторы не выполняются — например, выделенная память не освобождается или открытые файлы не закрываются, что приводит к утечкам памяти или ресурсам . Это должно быть сделано в каждом месте возврата, что является хрупким и может легко привести к ошибкам. Например, при более поздней разработке оператор возврата может быть упущен разработчиком, и действие, которое должно быть выполнено в конце подпрограммы (например, оператор трассировки ), может быть выполнено не во всех случаях. Языки без оператора возврата, такие как стандартный Pascal и Seed7 , не имеют этой проблемы.
Большинство современных языков предоставляют поддержку на уровне языка для предотвращения таких утечек; [12] см. подробное обсуждение в разделе управление ресурсами . Чаще всего это делается с помощью защиты от раскручивания, которая гарантирует, что определенный код будет гарантированно запущен при выходе из блока; это структурированная альтернатива наличию блока очистки и goto
. Чаще всего это известно как try...finally,
и считается частью обработки исключений . В случае нескольких return
операторов введение try...finally,
без исключений может выглядеть странно. Существуют различные методы инкапсуляции управления ресурсами. Альтернативный подход, встречающийся в основном в C++, — это получение ресурсов как инициализация , который использует обычную раскрутку стека (освобождение переменных) при выходе из функции для вызова деструкторов локальных переменных для освобождения ресурсов.
Кент Бек , Мартин Фаулер и соавторы в своих книгах по рефакторингу утверждают , что вложенные условные операторы могут быть сложнее для понимания, чем определенный тип более плоской структуры, использующей несколько выходов, предицированных охранными предложениями . Их книга 2009 года категорически утверждает, что «одна точка выхода на самом деле не является полезным правилом. Ясность — это ключевой принцип: если метод понятнее с одной точкой выхода, используйте одну точку выхода; в противном случае не используйте». Они предлагают решение из кулинарной книги для преобразования функции, состоящей только из вложенных условных операторов, в последовательность защищенных операторов return (или throw), за которыми следует один незащищенный блок, который должен содержать код для общего случая, в то время как защищенные операторы должны иметь дело с менее распространенными (или с ошибками). [13] Херб Саттер и Андрей Александреску также утверждают в своей книге советов по C++ 2004 года, что единственная точка выхода — устаревшее требование. [14]
В своем учебнике 2004 года Дэвид Уотт пишет, что «однозаходные многовыходные потоки управления часто желательны». Используя фреймворковое понятие секвенсора Теннента , Уотт единообразно описывает конструкции потока управления, встречающиеся в современных языках программирования, и пытается объяснить, почему определенные типы секвенсоров предпочтительнее других в контексте многовыходных потоков управления. Уотт пишет, что неограниченные goto (секвенсоры перехода) плохи, потому что место назначения перехода не является самоочевидным для читателя программы, пока читатель не найдет и не изучит фактическую метку или адрес, который является целью перехода. Напротив, Уотт утверждает, что концептуальное намерение секвенсора возврата ясно из его собственного контекста, без необходимости исследовать его место назначения. Уотт пишет, что класс секвенсоров, известных как escape-секвенсоры , определяемый как «секвенсор, который завершает выполнение текстуально охватывающей команды или процедуры», охватывает как разрывы циклов (включая многоуровневые разрывы), так и операторы возврата. Уотт также отмечает, что хотя в таких языках, как C, секвенсоры переходов (goto) были несколько ограничены, где цель должна быть внутри локального блока или охватывающего внешнего блока, одного этого ограничения недостаточно, чтобы сделать намерение goto в C самоописываемым, и поэтому они все еще могут производить « спагетти-код ». Уотт также исследует, чем секвенсоры исключений отличаются от секвенсоров выхода и перехода; это объясняется в следующем разделе этой статьи. [15]
В отличие от вышесказанного, Бертран Мейер написал в своем учебнике 2009 года, что инструкции типа break
и continue
«просто старье goto
в овечьей шкуре» и настоятельно рекомендовал не использовать их. [16]
Основываясь на ошибке кодирования из катастрофы Ariane 501 , разработчик программного обеспечения Джим Бонанг утверждает, что любые исключения, выдаваемые функцией, нарушают парадигму единственного выхода, и предлагает запретить все межпроцедурные исключения. Бонанг предлагает, чтобы все C++, соответствующие единственному выходу, были написаны следующим образом:
bool MyCheck1 () throw () { bool success = false ; try { // Сделать что-то, что может вызвать исключения. if ( ! MyCheck2 ()) { throw SomeInternalException (); } // Другой код, аналогичный приведенному выше. success = true ; } catch (...) { // Все исключения перехвачены и зарегистрированы. } return success ; }
Питер Ритчи также отмечает, что, в принципе, даже одиночное throw
право перед return
в функции является нарушением принципа единственного выхода, но утверждает, что правила Дейкстры были написаны во времена, когда обработка исключений не стала парадигмой в языках программирования, поэтому он предлагает разрешить любое количество точек выброса в дополнение к единственной точке возврата. Он отмечает, что решения, которые оборачивали исключения ради создания единственного выхода, имеют большую глубину вложенности и, таким образом, более сложны для понимания, и даже обвиняет тех, кто предлагает применять такие решения к языкам программирования, поддерживающим исключения, в вовлечении в мышление карго-культа . [17]
Дэвид Уотт также анализирует обработку исключений в рамках секвенсоров (представленных в этой статье в предыдущем разделе о ранних выходах). Уотт отмечает, что ненормальная ситуация (обычно иллюстрируемая арифметическими переполнениями или сбоями ввода/вывода, такими как файл не найден) — это своего рода ошибка, которая «обнаруживается в некотором программном модуле низкого уровня, но [для которой] обработчик более естественно размещается в программном модуле высокого уровня». Например, программа может содержать несколько вызовов для чтения файлов, но действие, которое необходимо выполнить, когда файл не найден, зависит от значения (цели) рассматриваемого файла для программы, и, таким образом, процедура обработки для этой ненормальной ситуации не может быть размещена в системном коде низкого уровня. Уоттс далее отмечает, что введение тестирования флагов состояния в вызывающем объекте, как это повлечет за собой структурное программирование с одним выходом или даже секвенсоры возврата (с несколькими выходами), приводит к ситуации, когда «код приложения имеет тенденцию загромождаться тестами флагов состояния» и что «программист может по забывчивости или лениво пропустить тестирование флага состояния. Фактически, ненормальные ситуации, представленные флагами состояния, по умолчанию игнорируются!» Он отмечает, что в отличие от тестирования флагов состояния исключения имеют противоположное поведение по умолчанию , заставляя программу завершаться, если программист явно не обрабатывает исключение каким-либо образом, возможно, добавляя код для его намеренного игнорирования. Основываясь на этих аргументах, Уотт приходит к выводу, что секвенсоры переходов или секвенсоры выхода (обсуждавшиеся в предыдущем разделе) не так подходят, как выделенный секвенсор исключений с семантикой, обсуждавшейся выше. [18]
В учебнике Лаудена и Ламберта подчеркивается, что обработка исключений отличается от структурных программных конструкций, таких как while
циклы, поскольку передача управления «устанавливается в другой точке программы, нежели та, где происходит фактическая передача. В точке, где фактически происходит передача, может не быть синтаксического указания на то, что управление будет фактически передано». [19] Профессор компьютерных наук Арвинд Кумар Бансал также отмечает, что в языках, реализующих обработку исключений, даже управляющие структуры, такие как for
, которые имеют свойство единственного выхода при отсутствии исключений, больше не имеют его при наличии исключений, потому что исключение может преждевременно вызвать ранний выход в любой части управляющей структуры; например, если init()
выдает исключение в for (init(); check(); increm())
, то обычная точка выхода после check() не достигается. [20] Ссылаясь на многочисленные предыдущие исследования других авторов (1999–2004) и свои собственные результаты, Уэстли Ваймер и Джордж Некула написали, что существенной проблемой с исключениями является то, что они «создают скрытые пути потока управления, о которых программистам трудно рассуждать». [21]
Необходимость ограничения кода точками единственного выхода появляется в некоторых современных средах программирования, ориентированных на параллельные вычисления , таких как OpenMP . Различные параллельные конструкции из OpenMP, такие как parallel do
, не допускают ранних выходов изнутри наружу параллельной конструкции; это ограничение включает в себя все виды выходов, от break
исключений до C++, но все они разрешены внутри параллельной конструкции, если цель перехода также находится внутри нее. [22]
Реже подпрограммы допускают множественный вход. Чаще всего это только повторный вход в сопрограмму (или генератор /полусопрограмму), где подпрограмма возвращает управление (и, возможно, значение), но затем может быть возобновлена с того места, где она остановилась. Существует ряд распространенных применений такого программирования, в частности, для потоков (в частности, ввода/вывода), конечных автоматов и параллелизма. С точки зрения выполнения кода, выход из сопрограммы ближе к структурному программированию, чем возврат из подпрограммы, поскольку подпрограмма фактически не завершается и продолжится при повторном вызове — это не ранний выход. Однако сопрограммы означают, что несколько подпрограмм имеют состояние выполнения — а не один стек вызовов подпрограмм — и, таким образом, вводят другую форму сложности.
Подпрограммы очень редко допускают вход в произвольную позицию в подпрограмме, поскольку в этом случае состояние программы (например, значения переменных) неинициализировано или неоднозначно, и это очень похоже на goto.
Некоторые программы, в частности парсеры и протоколы связи , имеют ряд состояний , которые следуют друг за другом таким образом, что их нелегко свести к базовым структурам, и некоторые программисты реализуют изменения состояний с помощью перехода к новому состоянию. Этот тип переключения состояний часто используется в ядре Linux. [ необходима цитата ]
Однако можно структурировать эти системы, сделав каждое изменение состояния отдельной подпрограммой и используя переменную для указания активного состояния (см. trampoline ). В качестве альтернативы, их можно реализовать с помощью сопрограмм, которые обходятся без trampoline.
{{cite book}}
: CS1 maint: несколько имен: список авторов ( ссылка )Пример 4: Один вход, один выход («SESE»). Исторически некоторые стандарты кодирования требовали, чтобы каждая функция имела ровно один выход, то есть один оператор return. Такое требование устарело в языках, которые поддерживают исключения и деструкторы, где функции обычно имеют множество неявных выходов.