Структуры управления
Как goto прячется под маской
Хотя мало кто настаивает на возвращение оператора goto в прежнем виде, битва за простые структуры управления еще не окончена. В частности, многие языки программирования поддерживают форму цикла с оператором break, представляющим оператор выхода из цикла в любой его точке (этот же оператор выхода может применяться и для многовариантного оператора выбора, изучаемого ниже). Вот возможный пример цикла с оператором break:
from ... until условие выхода loop Некоторые операторы if другое условие then break end Другие операторы end
Предупреждение: это только иллюстрация, а не программа на Eiffel.
Если во время выполнения тела цикла станет истинным "другое условие", то цикл прекратит свою работу, не выполнив "Другие операторы", не проверив истинности условия выхода, не выполняя дальнейших итераций.
Другой конструкцией подобной природы является оператор, применяемый в теле цикла и позволяющий прервать выполнение итерации в любой точке и перейти к выполнению следующей итерации.
Такие операторы — это тот же старый goto в овечьей шкуре. Относитесь к ним так же, как и к goto.
Почувствуй методологию
Избегайте операторов break и любых подобных механизмов.
Достаточно просто, применив данные ранее советы, избавиться от скрытого goto, переписав пример следующим образом:
from ... until условие_выхода loop Некоторые операторы if not другое_условие then Другие операторы end end
Другие примеры могут потребовать больших усилий на избавление от goto, но и они не нарушают общего правила.
Основные возражения против скрытого goto остаются те же, что и против общего goto, — ясность и простота структур с одним входом и одним выходом. Существует и фундаментальный критерий: наша способность выводить семантику программу из ее текста — в терминах Э. Дейкстры "сократить концептуальный разрыв между статической программой и динамическим процессом". Для циклов, как ранее мы рассмотрели, ключевым приемом построения вывода является принцип постусловия цикла: для понимания того, что делает цикл, достаточно скомбинировать инвариант цикла (даже неформальный) с условием выхода. В приводимом ранее примере нахождения максимума множества чисел мы имели инвариант:
и условие выхода:
i = n
Достаточно просто проверить, что инициализация делает инвариант истинным, что тело сохраняет его истинность и что цикл завершается. Комбинация этих свойств незамедлительно влечет, что max является максимумом из . Если бы мы ввели оператор break или другую подобную конструкцию, то такой простой вывод стал бы невозможным, само понятие инварианта цикла перестало бы существовать, по крайней мере, в такой простой форме, как мы его изучали. Это еще одна причина не отказываться от схем с одним входом и одним выходом.
Риск ошибок при использовании подобных конструкций - не просто теоретический. Основная телефонная сеть США AT&T в 1990 году вышла из строя, отключив все телефоны. Причиной была ошибка в программном коде на языке С: оператор break прервал выполнение оператора switch, хотя предполагалось, что он прервет выполнение охватывающей конструкции.
7.9. Вариации базисных структур управления
Последовательность, цикл, альтернатива — триада "структурного программирования" — составляет базис структурного потока управления. Но есть некоторые интересные варианты, заслуживающие отдельного рассмотрения.
Согласно теореме Бёма — Джакопини триады достаточно для выражения всех имеющих смысл алгоритмов, ни одно из расширений не является теоретически необходимым — все они могут быть выражены как комбинация элементов триады. Но это не исключает их практической полезности для программистов, так как они в частных случаях предлагают более эффективный способ записи. Исходя из этого критерия, мы можем разделить их на две категории.
- Конструкции, обеспечивающие в сравнении с базисными серьезное улучшение для важных частных случаев.
- Механизмы, которые следует знать, так как они присутствуют в некоторых широко распространенных языках программирования, хотя и нет веских аргументов для их использования.
Разница в конструкциях, во многом, дело вкуса, так что у вас может быть свое собственное мнение.
Инициализация цикла
Предложение from в конструкции нашего цикла представляет способ задания начальных характеристик управления. Оно, конечно, избыточно, так как вместо такой конструкции:
from Операторы инициализации until условие loop Тело end
можно скомбинировать две конструкции — последовательность и цикл, записав:
Операторы инициализации from <— Здесь пусто until условие loop Тело end
Такая запись дает тот же самый эффект. Конструкции циклов в некоторых языках программирования действительно не включают инициализацию и начинаются с until или его эквивалента.
Причина включения предложения from в синтаксическую конструкцию цикла в том, что большинству циклических процессов, подобно процессам аппроксимации, нужна инициализация, чтобы цикл мог правильно работать. Инициализация — это не просто группа операторов, выполняемых перед началом цикла, — это неотъемлемая часть цикла. Правила корректности цикла отражают этот факт, приписывая инициализации важную роль — обеспечить начальную истинность инварианта цикла еще до того, как начнут выполняться итерации, заданные телом цикла, которые должны затем поддерживать инвариантность.
В языках, чьи циклы не имеют предложения from, приходится выполнять инициализацию как независимый составной оператор, чаще всего с комментарием, поясняющим его роль.
В Eiffel это обсуждение дает нам ответ на вопрос: если некоторые операции выполняются перед циклом, то они должны появляться в операторах, предшествующих циклу, или в предложении from? В зависимости от роли этих операций они могут появляться в том или в другом месте или могут быть расщеплены на две части.
Почувствуй методологию
Если оператор, выполняемый перед циклом, служит для инициализации циклического процесса, в частности, для формирования инварианта, то размещайте его в предложении from.
Если же часть из множества операций просто по алгоритму следует выполнить до начала цикла, то помещайте их перед циклом как независимые операторы.
Другие формы цикла
Многие языки программирования предлагают форму цикла с ключевым словом, задающим условие продолжения работы цикла, а не условие его окончания:
while условие_продолжения loop Тело end
Семантика понятна: вычисли "условие_продолжения" если оно истинно, то выполни итерацию и снова проверь условие, если условие ложно, то цикл завершает работу. Это эквивалентно записи в нашем стиле:
until not условие_продолжения
или until условие выхода, где условие выхода является отрицанием условие_продолжения. Разница в следующем.
- Форма while отражает динамику: во время выполнения цикл будет повторять тело до тех пор, пока истинно условие_продолжения.
- Форма until характеризует выводимость — корректность цикла и его эффект: она отражает тот факт, что постусловие цикла и его инвариант определяют условие_выхода.
Еще одну форму цикла не следует путать с формой from... until... loop ... end, хотя она тоже использует обычно ключевое слово until, но в конце конструкции, а не в начале. Ее типичный вид:
repeat Тело until условие_выхода end
Семантика такова: выполните тело, если после этого условие выхода истинно, то выйдите из цикла, в противном случае начните все сначала. В отличие от предыдущих вариантов (from ... until... и while), где тело цикла может ни разу не выполняться, в данном варианте тело цикла всегда будет выполняться, по крайней мере, один раз.
Нетрудно выразить этот вариант в нашей нотации:
from Тело until условие_выхода loop Тело end
Недостатком является повторение тела цикла, в то время как мы обычно стараемся избежать повторения кода. Можно избежать повторения, превратив тело, включающее несколько операторов, в подпрограмму.
Здесь мы достигли точки, где возможны разные мнения. Одни предпочитают иметь несколько конструкций с возможностью проверки условия выхода (условия продолжения) в начале или в конце цикла в зависимости от возникающей ситуации. Я предпочитаю иметь единственную конструкцию цикла с тщательно определенной семантикой и простым понятием инварианта (чей двойник для цикла в форме repeat является более сложным). Я готов заплатить за это в ряде редких случаев повторением строчки кода или добавлением подпрограммы.
Необходимость действительно редкая, так как циклы с повторением "ноль или более раз" встречаются значительно чаще, чем циклы, требующие "одно или более повторений". Еще одним общим видом является цикл типа for:
for i : 1 .. 10 loop Тело end
Семантика такова: выполни тело, чьи операторы обычно используют i, последовательно для всех значений i в заданном интервале (здесь 1, 2 и так далее до 10). Граничные значения 1 и 10 даны для примера, и обычно они задаются значениями переменных или выражений, а не константами.
В языке С и его последователях, таких как С++, форма такова:
for (i=1; i <= 10;i++){ Тело }
Первый элемент в круглых скобках задает инициализацию i. Второй — условие продолжения. Последний элемент представляет операцию увеличения, выполняемую после каждого выполнения тела. Нотация i++ означает увеличение i на единицу. Отметим видимую разницу в стиле синтаксиса: вместо ключевых слов используются символы, такие как скобки, точки с запятой, что характерно для этого стиля программирования.
Формы цикла, основанные на явном использовании индексов известны также, как циклы do, от соответствующего ключевого слова языка Фортран, который ввел подобный механизм, будучи первым языком программирования.
Конструкция from этой лекции выражает такие циклы следующим образом:
from i:= 1 until i > n loop Тело i:= i + 1 end
Здесь используется оператор присваивания a:= b (дать a текущее значение b), изучаемый в деталях в лекции 9.
На этом история со стилем for для цикла не заканчивается. Показанный его эквивалент from ... until ... loop не столь хорошо выполняет работу. Выполняя операции над индексом в разных местах: инициализации, теста, увеличения индекса — он скрывает основную идею итерирования некоторого интервала, 1 ... 10 в нашем примере. Поэтому появляются веские основания для формы цикла более высокого уровня, просто предписывающего: "Выполни эту операцию для всех элементов данного множества".
Цикл for — пример движения в этом направлении. Здесь данное множество представлено непрерывным интервалом целых чисел. Хотелось бы выполнять итерации на множествах более общего вида, например, на списках, таких как линия метро, представленная списком станций. Общий механизм должен позволять в терминах высокого уровня говорить нечто подобное: "Выполни эту операцию для всех станций этой линии".
Такой механизм имеет имя: итератор. При обсуждении структур данных мы увидим, что можно, не вводя новые структуры управления, определить мощные итераторы, применимые к широкому спектру структур данных.