Циклы: 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, когда число итераций известно — завершаемость достаётся бесплатно.
Проверьте себя
1. Можно ли внутри цикла for изменить его счётчик?
AДа, как обычную переменную
BНет — счётчик доступен только для чтения, что исключает класс ошибок
CТолько через :=
DТолько в reverse
2. Почему for предпочтительнее while, когда число итераций известно?
Afor красивее
Bfor по диапазону гарантированно завершается, а while может зациклиться
Cwhile запрещён
Dfor быстрее на любом железе
3. Как чисто выйти сразу из нескольких вложенных циклов?
AЧерез goto
BЧерез флаги-переменные
CИменованным циклом и exit Имя;
DЭто невозможно