Ветвление: if и case с полным покрытием
Условный выбор в Ada: оператор if во всей красе и оператор case, который требует покрыть ВСЕ возможные варианты — почему это требование спасает от целого класса ошибок.
case в Ada — оператор множественного выбора, который обязан охватить каждое возможное значение проверяемого выражения; пропуск варианта — ошибка компиляции, а не молчаливое «ничего не делать».
Оператор if: ясный и закрытый
Условное ветвление — основа любой программы. В Ada оператор if устроен подчёркнуто читаемо и всегда явно закрывается:
if Temperature > 100 then
Put_Line ("Перегрев!");
elsif Temperature > 80 then
Put_Line ("Высокая температура");
else
Put_Line ("Норма");
end if;
Разберём детали. После условия идёт обязательное then. Ветви дополнительных условий вводятся словом elsif — это слитное написание (не else if), характерное для Ada и Algol-семейства. Завершается конструкция явным end if;. Оператор сравнения «больше» в коде записывается как > (в HTML экранируется в >), «меньше» — <, «больше или равно» — >=, «меньше или равно» — <=, «не равно» — /=. Обратите внимание: проверка равенства — это одиночный = (а не ==), потому что присваивание в Ada, как мы помним, обозначается :=, и путаницы не возникает.
Зачем явный end if;, ведь он многословнее закрывающей скобки? Ради читаемости в длинном коде. Когда между if и его концом сотня строк, фигурная скобка теряется, а end if; ясно говорит, что именно закрывается. Это снова принцип Steelman: язык оптимизирован под чтение.
Условия и логические операторы
Условия комбинируют логическими операторами and, or, not. Но в Ada есть тонкость, важная для надёжности — две формы конъюнкции и дизъюнкции:
and/or— вычисляют оба операнда.and then/or else— сокращённое вычисление: второй операнд вычисляется, только если результат ещё не ясен по первому.
if Divisor /= 0 and then (Value / Divisor > 10) then
Put_Line ("Большое частное");
end if;
Здесь and then критичен: если Divisor = 0, второй операнд (деление) не вычисляется, и деления на ноль не происходит. С простым and оба операнда считались бы всегда, и при нулевом делителе программа упала бы. Форма or else симметрична: if X = 0 or else Slow_Check (X) then ... — если X = 0, дорогая Slow_Check не вызывается. Явное различие «считать оба» и «считать по необходимости» — ещё один пример, как Ada отделяет намерения, которые в других языках слиты. Когда важен порядок и защита от опасного второго условия, пишут and then / or else.
Оператор case: множественный выбор
Когда нужно выбрать одну из многих веток по значению одного выражения, if-elsif становится громоздким. На помощь приходит case:
type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
Today : Day := Sat;
case Today is
when Mon | Tue | Wed | Thu | Fri =>
Put_Line ("Рабочий день");
when Sat | Sun =>
Put_Line ("Выходной");
end case;
Каждая ветвь начинается с when, затем значения, стрелка => и действия. (Стрелка => в HTML экранируется.) Вертикальная черта | объединяет несколько значений в одну ветвь: Mon | Tue | ... означает «для понедельника, или вторника, или...». Можно указывать и диапазоны: when 0 .. 9 => для чисел. Завершается всё end case;.
Главное требование: полнота покрытия
А теперь — то, ради чего case в Ada заслуживает отдельного урока. Оператор case обязан покрыть каждое возможное значение проверяемого выражения. Если вы забудете хоть один вариант, это ошибка компиляции, а не молчаливое поведение. Представьте, что мы добавили в перечисление Day новое значение, но забыли обработать его в case — компилятор откажется собирать программу, пока мы не учтём новый вариант. В большинстве языков аналогичный switch молча «провалится мимо», и пропущенный случай станет невидимым багом, всплывающим в проде. Ada превращает забытый вариант в немедленную ошибку на столе.
Если явно перечислять все значения не нужно, есть ветвь when others, ловящая «всё остальное»:
case Score is
when 90 .. 100 => Put_Line ("Отлично");
when 70 .. 89 => Put_Line ("Хорошо");
when 50 .. 69 => Put_Line ("Удовлетворительно");
when others => Put_Line ("Неудовлетворительно");
end case;
Здесь when others покрывает все значения Score, не попавшие в явные диапазоны (например, 0..49 и значения выше 100, если тип их допускает), обеспечивая полноту. Но злоупотреблять when others не стоит: если вы перечисляете варианты перечисления явно, без others, то добавление нового значения заставит компилятор напомнить вам обработать его везде — это ценная страховка. when others же «глушит» эту проверку, поэтому его применяют сознательно, когда действительно нужно «всё остальное одинаково».
Пустое действие: null
Иногда в ветви не нужно ничего делать. В Ada нельзя оставить ветвь пустой — это выглядело бы как забытый код. Вместо этого пишут явный оператор null;, означающий «осознанно ничего не делать»:
case Signal is
when Red => Stop;
when Yellow => null; -- осознанно ничего не делаем
when Green => Go;
end case;
Этот крошечный null; — важный сигнал читателю: «здесь бездействие намеренно, а не забыто». Опять язык заставляет сделать намерение явным.
Как работает под капотом проверка полноты
Откуда компилятор знает, все ли значения покрыты? Из типа проверяемого выражения. Если это перечисление Day, компилятор точно знает все семь его значений и сверяет, что каждое учтено хотя бы одной ветвью или others. Если это Integer range 0 .. 100, он знает границы и проверяет покрытие всего диапазона. Здесь снова видно, как сильная система типов работает на надёжность: именно потому, что тип точно описывает множество значений, компилятор может статически доказать полноту выбора. В языках со слабыми типами такая проверка невозможна — компилятор не знает, какие значения «бывают». Связка «точные типы плюс обязательное покрытие» даёт практический эффект: сопровождая программу годами и расширяя перечисления, вы получаете от компилятора список всех мест, которые нужно дописать. Ни один вариант не потеряется молча. Для систем с долгим жизненным циклом это бесценно.
Проверка принадлежности и квантифицированные выражения
Рядом с if и case в Ada живёт пара выразительных средств, делающих условия короче и яснее. Первое — проверка принадлежности оператором in: она проверяет, попадает ли значение в диапазон, подтип или множество, и читается почти как фраза на естественном языке:
if Age in 18 .. 65 then -- вместо Age >= 18 and Age <= 65
Put_Line ("Трудоспособный возраст");
end if;
if Letter in 'A' .. 'Z' | 'a' .. 'z' then -- буква латиницы
Put_Line ("Это буква");
end if;
if Today not in Sat | Sun then -- рабочий день
Put_Line ("Сегодня работаем");
end if;
Выражение Age in 18 .. 65 заменяет громоздкое Age >= 18 and Age <= 65 одной ясной проверкой. Вертикальная черта | объединяет несколько вариантов: 'A' .. 'Z' | 'a' .. 'z' — «заглавная или строчная латинская буква». Форма not in проверяет отсутствие в множестве. Особенно ценно, что in работает с подтипами: if X in Valid_Range проверяет принадлежность диапазону подтипа целиком. Это и короче, и надёжнее ручных сравнений, где легко перепутать строгое и нестрогое неравенство или забыть одну из границ.
Второе средство — квантифицированные выражения (Ada 2012), позволяющие проверить условие сразу для всех или хотя бы одного элемента диапазона или массива, прямо как кванторы «для всех» и «существует» из логики:
All_Positive : Boolean := (for all I in A'Range => A (I) > 0);
Has_Zero : Boolean := (for some I in A'Range => A (I) = 0);
Выражение (for all I in A'Range => A (I) > 0) истинно, если каждый элемент массива положителен; for some — если хотя бы один удовлетворяет условию. (Стрелки => и операторы > в HTML экранируются.) Раньше такие проверки писались циклом с флагом и ранним выходом — теперь это одно декларативное выражение, читаемое как математическое утверждение. Квантифицированные выражения особенно мощны в связке с контрактами: предусловие with Pre => (for all I in A'Range => A (I) /= 0) прямо заявляет «все элементы ненулевые» как проверяемое (а в SPARK — доказуемое) требование. Вместе in и кванторы сдвигают стиль кода от «как проверить» (пошаговый цикл) к «что проверить» (декларативное условие) — а декларативность, как правило, и читаемее, и устойчивее к ошибкам, что полностью в духе приоритетов Ada.
Объединяет всё, о чём шёл урок, одна нить — Ada стремится сделать условия не только проверяемыми, но и читаемыми как утверждение. Обязательное покрытие case гарантирует, что ни один вариант не забыт; and then/or else делают порядок и безопасность вычисления явными; проверка in заменяет пары неравенств ясной фразой; квантифицированные выражения превращают циклы-проверки в логические высказывания. Во всех случаях язык подталкивает писать что должно быть верно, а не как это пошагово проверить, и одновременно гарантирует полноту и корректность проверки. Для критичной системы это двойная выгода: код легче читать и ревьюировать (а ревью — ключевая часть сертификации), и в нём меньше места для классических ошибок ветвления — забытого случая, перепутанной границы, опасного порядка вычисления. Освоив эти конструкции, вы заметите, что условия в вашем коде становятся короче и яснее, а целые классы ошибок управления потоком, привычные в других языках, в Ada просто не возникают — их закрывает либо компилятор, либо сама форма выражения.
Частые ошибки и заблуждения
- Писать
else ifвместоelsif. В Ada промежуточная ветвь — слитноеelsif;else ifсоздаст вложенныйif, требующий своегоend if;. - Забывать вариант в
case. Неполное покрытие — ошибка компиляции; либо перечислите все значения, либо добавьтеwhen others. - Злоупотреблять
when others. Он глушит проверку полноты; для перечислений часто полезнее обойтись без него, чтобы компилятор напоминал о новых значениях. - Оставлять ветвь «пустой». Бездействие выражается явным
null;— это сигнал, что пропуск намеренный. - Использовать
andтам, где нуженand then. Для защиты опасного второго условия (деление, обращение по индексу) применяйте сокращённое вычислениеand then/or else.
Итоги
- Оператор
ifиспользуетthen, слитноеelsif,elseи явноеend if;; равенство проверяется одиночным=, неравенство —/=. - Логика:
and/orвычисляют оба операнда, аand then/or else— сокращённо, что защищает опасное второе условие. - Оператор
caseвыбирает ветвь по значению;|объединяет значения, допустимы диапазоны, завершаетсяend case;. - Ключевое требование:
caseобязан покрыть все значения типа — пропуск варианта есть ошибка компиляции, а не тихий баг. - Полнота проверяется благодаря точному знанию множества значений из типа;
when othersловит остальное,null;выражает осознанное бездействие.