Циклы: for, while и loop с exit
Циклы в Ada: единая конструкция loop и её разновидности — for по диапазону, while с условием и базовый loop с явным exit. Почему счётчик цикла защищён от изменения и как именованные циклы помогают выходить из вложенности.
loop — базовая конструкция повторения в Ada;
forиwhile— это удобные формы того же цикла, отличающиеся способом задания условия продолжения.
Единая идея цикла
В Ada все циклы строятся вокруг одного слова — loop. Базовая форма — это просто бесконечное повторение тела между loop и end loop;, из которого выходят оператором exit. Поверх неё надстроены две удобные разновидности: for (перебор диапазона) и while (повтор по условию). Такое единство делает синтаксис цельным: освоив loop и exit, вы понимаете все формы. Завершается любой цикл явным end loop; — той же логикой читаемости, что и end if;.
Цикл for: перебор диапазона
Самая частая форма — перебор значений диапазона:
for I in 1 .. 5 loop
Put_Line ("Шаг номер" & Integer'Image (I));
end loop;
Вывод:
Шаг номер 1 Шаг номер 2 Шаг номер 3 Шаг номер 4 Шаг номер 5
Переменная цикла I здесь обладает несколькими замечательными свойствами, прямо вытекающими из философии безопасности. Во-первых, её не нужно объявлять заранее — она существует только внутри цикла и автоматически имеет тип, выведенный из диапазона. Во-вторых, и это важнее всего, счётчик цикла доступен только для чтения: внутри тела ему нельзя присвоить новое значение. Попытка I := 10; внутри цикла — ошибка компиляции. Зачем такой запрет? В языках, где счётчик цикла можно менять изнутри, это источник коварных багов и бесконечных циклов. Ada исключает целый класс ошибок, сделав счётчик неприкосновенным. Цикл for в Ada всегда завершается: диапазон фиксирован, счётчик меняться не может, бесконечность невозможна.
Обратный перебор и пустые диапазоны
Чтобы идти в обратную сторону, добавляют reverse:
for I in reverse 1 .. 3 loop
Put_Line (Integer'Image (I));
end loop;
Вывод:
3 2 1
Ещё одна продуманная деталь: если нижняя граница больше верхней, диапазон пуст, и цикл просто не выполнится ни разу — без ошибки. Например, for I in 5 .. 1 loop (без reverse) корректно выполнит ноль итераций. Это избавляет от граничных проверок «а вдруг диапазон вырожденный». Перебирать можно и по типу через атрибут 'Range или прямо по перечислению: for D in Day loop пройдёт по всем дням недели — очень идиоматично и устойчиво к изменению типа.
Цикл while: повтор по условию
Когда число итераций заранее неизвестно и зависит от условия, используют while:
N : Integer := 1;
while N <= 100 loop
N := N * 2;
end loop;
-- после цикла N = 128 (первое значение, превысившее 100)
Условие N <= 100 (в коде оператор «меньше или равно», в HTML — <=) проверяется перед каждой итерацией. Пока оно истинно, тело повторяется. В отличие от for, здесь вы сами отвечаете за то, чтобы условие когда-нибудь стало ложным, иначе цикл зациклится. Поэтому while применяют, когда условие выхода естественно выражается логически, а не диапазоном.
Базовый loop и exit
Самая гибкая форма — чистый loop с явным выходом. Она нужна, когда условие выхода удобнее проверять в середине тела, а не в начале:
Sum : Integer := 0;
Num : Integer;
loop
Get (Num); -- читаем число
exit when Num = 0; -- выходим, если ввели ноль
Sum := Sum + Num;
end loop;
Put_Line ("Сумма:" & Integer'Image (Sum));
Конструкция exit when Условие; — это сокращение для «если условие истинно, выйти из цикла». Здесь она стоит посреди тела: сначала читаем число, и только потом решаем, продолжать ли. Такую логику «прочитать, затем проверить» неуклюже выражать через while (пришлось бы дублировать чтение), а loop ... exit when выражает её естественно. Есть и безусловный exit; — обычно внутри if. Это аналог распространённой идиомы «цикл с выходом из середины», которую Ada поддерживает чисто и читаемо.
Именованные циклы: выход из вложенности
Особенно элегантно Ada решает проблему выхода из вложенных циклов. Циклу можно дать имя, и тогда exit может указать, из какого именно цикла выходить:
Search :
for Row in 1 .. 10 loop
for Col in 1 .. 10 loop
if Matrix (Row, Col) = Target then
Put_Line ("Нашли!");
exit Search; -- выходим сразу из ВНЕШНЕГО цикла
end if;
end loop;
end loop Search;
Имя Search : ставится перед внешним циклом, и оно же повторяется в end loop Search;. Оператор exit Search; выходит не из ближайшего (внутреннего) цикла, а именно из Search — то есть сразу из обоих уровней. В языках без этой возможности приходится заводить флаги-переменные или прибегать к нелюбимому многими goto, чтобы выскочить из вложенности. Именованные циклы решают задачу ясно и без трюков: вы прямо называете цель выхода. Это ещё один пример, как Ada предлагает структурированную, читаемую альтернативу опасным приёмам.
Как работает под капотом гарантия завершения
Сравним три формы по самому важному для надёжности свойству — гарантии завершения. Цикл for по диапазону гарантированно конечен: диапазон фиксирован при входе, счётчик нельзя менять, поэтому бесконечный for в Ada невозможен в принципе. Это сильная гарантия, и потому for предпочитают везде, где число итераций известно. Циклы while и loop ... exit такой гарантии не дают — ответственность за выход на программисте, и здесь возможны бесконечные циклы при ошибке в логике. Отсюда практическое правило: если можно выразить цикл через for, делайте это — вы получаете завершаемость бесплатно. while и loop оставляйте для случаев, где условие выхода действительно не сводится к перебору диапазона. Этот градиент гарантий — характерная черта Ada: язык подталкивает к конструкциям, чьи свойства легче доказать. В пределе, в SPARK, для циклов while пишут специальные инварианты и доказывают завершение математически — но даже в обычной Ada привычка предпочитать for уже снижает риск.
Циклы по элементам и почему goto почти не нужен
Современная Ada предлагает ещё более выразительную форму перебора — цикл по самим элементам контейнера или массива, а не по их индексам. Когда вам нужны значения, а не позиции, это устраняет лишний шаг индексации:
type Temps is array (1 .. 5) of Float;
Readings : Temps := (20.0, 21.5, 19.0, 22.0, 20.5);
Sum : Float := 0.0;
begin
for Value of Readings loop -- перебор ЗНАЧЕНИЙ, не индексов
Sum := Sum + Value;
end loop;
Форма for Value of Readings (с предлогом of, а не in) на каждой итерации даёт сам элемент массива в переменную Value. Сравните: for I in Readings'Range loop ... Readings (I) ... требует индекса и обращения по нему, а for Value of Readings сразу даёт значение. Это короче, читаемее и исключает ошибку индексации. Для контейнеров из Ada.Containers такой цикл работает единообразно, что делает код, перебирающий векторы и списки, особенно чистым. Если же элемент нужно менять, используют for Value of Readings loop Value := ...; — здесь Value ссылается на элемент, и присваивание меняет массив на месте.
Раз уж мы обсуждаем поток управления, стоит закрыть вопрос, который неизбежно возникает у программиста с опытом: а есть ли в Ada goto? Технически да — оператор goto и метки существуют. Но на практике он почти никогда не нужен, и вот почему. Все ситуации, в которых в старых языках тянулись к goto, в Ada покрыты структурными конструкциями: выход из цикла — exit (в том числе из вложенного — именованный exit Имя;), ранний возврат из подпрограммы — return, реакция на ошибку — исключения, выбор ветви — case. Эти конструкции выражают намерение явно и оставляют структуру кода видимой, тогда как goto разрывает её, создавая «спагетти», в котором трудно проследить ход выполнения.
Это иллюстрирует общий принцип дизайна Ada: для каждой осмысленной потребности есть структурная, читаемая конструкция, так что прибегать к разрушающим структуру средствам почти не приходится. Язык не запрещает goto догматически (бывают редкие случаи, например выход из глубокой вложенности в сгенерированном коде), но устроен так, что его отсутствие в обычной программе — норма, а присутствие — повод задуматься, нет ли более ясного способа. Структурное программирование здесь не лозунг, а встроенная в язык реальность, и циклы — for, while, loop ... exit, for ... of — её ядро. Богатство форм цикла как раз и позволяет почти всегда выразить повторение ясно и завершаемо, не сходя к низкоуровневым переходам.
Частые ошибки и заблуждения
- Пытаться изменить счётчик
for. Переменная цикла доступна только для чтения; присваивание ей — ошибка компиляции (и это защита от багов). - Объявлять переменную цикла заранее. Она объявляется неявно самим
forи живёт только внутри цикла; внешняя одноимённая переменная не используется. - Бояться «перевёрнутого» диапазона.
for I in 5 .. 1даёт пустой цикл (ноль итераций) без ошибки; для убывания нуженreverse. - Забывать обеспечить выход из
while/loop. В отличие отfor, эти формы могут зациклиться; следите, чтобы условие выхода достигалось. - Городить флаги для выхода из вложенных циклов. Используйте именованные циклы и
exit Имя;— это чище любых флагов и не требуетgoto.
Итоги
- Все циклы Ada строятся на
loop ... end loop;;forиwhile— удобные формы, различающиеся способом задания условия. - В
forсчётчик объявляется неявно, доступен только для чтения и не даёт зациклиться — цикл гарантированно конечен. whileповторяет по условию (проверка перед итерацией);loop ... exit whenпозволяет проверять выход в середине тела.- Именованные циклы и
exit Имя;дают чистый выход из вложенности без флагов иgoto. - Правило надёжности: предпочитайте
for, когда число итераций известно — завершаемость достаётся бесплатно.