Переменные, константы и присваивание :=

Переменные и константы в Ada: почему присваивание обозначается :=, как объявить неизменяемое значение, что такое инициализация и почему язык так педантичен к объявлениям.

Объявление в Ada связывает имя с типом (и, возможно, начальным значением); переменная может меняться, константа — нет, и это решается при объявлении.

Форма объявления

Объявление переменной в Ada читается почти как предложение: «имя — это значение такого-то типа». Синтаксис: имя, двоеточие, тип, точка с запятой.

Count       : Integer;
Temperature : Float;
Letter      : Character;

Здесь объявлены три переменные: целое Count, число с плавающей точкой Temperature и символ Letter. Двоеточие : читается как «имеет тип». Можно объявить несколько переменных одного типа списком через запятую:

X, Y, Z : Integer;

Обратите внимание на дисциплину: в Ada вы обязаны указать тип. Нет вывода типа «угадай сам по значению», как в некоторых современных языках. Это намеренно: явный тип — часть документации и контракта. Читая объявление, вы сразу знаете природу величины, а компилятор получает информацию для проверок.

Присваивание: знак :=

А вот и одна из самых узнаваемых черт Ada (унаследованная от Algol и Pascal): присваивание обозначается составным символом :=, а не одиночным =.

Count := 10;
Temperature := 36.6;
Letter := 'A';

Читается это как «Count становится равным 10». Почему так? Потому что в Ada (как в математике) символ = зарезервирован за сравнением на равенство, а не за присваиванием. Это устраняет классический источник ошибок из языков С-семейства, где = (присвоить) и == (сравнить) легко перепутать, и опечатка вида if (x = 5) компилируется с неожиданным смыслом. В Ada такая путаница невозможна: := и = — синтаксически разные вещи, и поставить присваивание там, где ждут условие, просто не получится. Запомните накрепко: := — присвоить, = — проверить равенство. В HTML двоеточие и знак равенства не требуют экранирования, но рядом часто стоят <= и >=, которые экранировать нужно.

Символьный литерал пишется в одинарных кавычках: 'A' — это символ, тогда как "A" в двойных кавычках — это строка из одного символа. Различие строгое, перепутать типы нельзя.

Инициализация при объявлении

Объявление и первое присваивание можно совместить — это называется инициализацией:

Count       : Integer := 0;
Temperature : Float   := 36.6;
Greeting    : String  := "Привет";

Это удобно и поощряется: переменная сразу получает осмысленное значение. Здесь возникает важная для Ada тема — неинициализированные переменные. Если объявить Count : Integer; без начального значения, его содержимое не определено (мусор из памяти). Чтение такой переменной до первого присваивания — ошибка программиста. Хорошая практика Ada — инициализировать сразу либо присвоить значение до первого чтения. Более того, инструменты Ada (и тем более SPARK) умеют обнаруживать использование неинициализированных данных, потому что это классический источник коварных багов. Философия языка: лучше явно задать начало, чем полагаться на случайное содержимое памяти.

Константы: значение, которое нельзя менять

Чтобы объявить величину, которая не должна меняться после установки, добавляют слово constant между двоеточием и типом, обязательно с инициализацией:

Pi          : constant Float   := 3.141_592_653;
Max_Retries : constant Integer := 5;
App_Name    : constant String  := "FlightCtl";

После такого объявления попытка присвоить константе новое значение — Pi := 3.0; — будет отвергнута компилятором. Это не пожелание, а гарантия. Зачем это нужно? Во-первых, ради безопасности: значения, которые по смыслу постоянны (физические константы, лимиты, имена), защищены от случайного изменения. Во-вторых, ради читаемости: видя constant, читатель сразу понимает, что величина фиксирована и можно на неё опираться. В-третьих, компилятор может оптимизировать работу с константами. Подчёркивания в 3.141_592_653 снова чисто косметические — группируют цифры.

Именованные числа: константы без типа

У Ada есть тонкая, но полезная разновидность констант — именованные числа (named numbers). Если присвоить имени числовой литерал без указания конкретного типа, получится «универсальная» константа, совместимая с любым подходящим числовым типом:

Speed_Of_Light : constant := 299_792_458;     -- универсальное целое
Half           : constant := 0.5;              -- универсальное вещественное

Поскольку тип не указан, Speed_Of_Light можно использовать там, где ожидается любой целочисленный тип, без преобразований. Это удобно для математических констант, которые должны «дружить» с разными типами. Контраст: Pi : constant Float := 3.14; привязан именно к Float, а Half : constant := 0.5; — универсален. Эта деталь отражает заботу Ada о точной работе с числами, к которой мы вернёмся в разделе про типы.

Как работает под капотом строгость объявлений

Почему Ada требует объявлять всё заранее и с типом? Потому что это даёт компилятору полную картину до начала исполнения. Зная тип каждой переменной, он проверяет совместимость каждого присваивания, каждой операции. Зная, что нечто — константа, он запрещает её менять. Зная диапазон (об этом дальше), он контролирует границы. Вся эта информация — статическая, известная при компиляции, и потому ошибки ловятся до запуска. Сравните с языками, где переменная возникает в момент первого присваивания и может в разные моменты хранить разные типы: там компилятор почти не может вам помочь, и ошибки всплывают в рантайме у пользователя. Ada выбирает противоположный полюс: максимум информации заранее, максимум проверок до запуска.

Покажем небольшую осмысленную процедуру, собирающую всё вместе:

with Ada.Text_IO;
with Ada.Integer_Text_IO;
use  Ada.Text_IO, Ada.Integer_Text_IO;

procedure Circle_Area is
   Radius : constant Integer := 5;
   Area   : Integer;
begin
   Area := 3 * Radius * Radius;   -- грубая оценка площади
   Put ("Площадь примерно: ");
   Put (Area);
   New_Line;
end Circle_Area;

Здесь Radius — константа (радиус не меняется), Area — переменная, которую мы вычисляем. Put из Ada.Integer_Text_IO печатает целое, New_Line переводит строку. Обратите внимание на оговорку use Ada.Text_IO, Ada.Integer_Text_IO; — через запятую можно открыть сразу несколько пакетов.

Блоки declare и время жизни переменных

Раз переменные нельзя объявлять посреди операторов, возникает естественный вопрос: а что если временная переменная нужна только в середине процедуры, для одного локального вычисления? Заводить её в общей зоне объявлений наверху — значит расширять её область жизни шире необходимого, что считается дурным тоном (чем уже область переменной, тем меньше шанс случайной ошибки). Ada решает это блоком declare — локальной областью прямо внутри тела:

begin
   Put_Line ("Начало");
   declare
      Temp : Integer := Compute_Something;   -- живёт только в этом блоке
      Doubled : Integer := Temp * 2;
   begin
      Put_Line ("Результат:" & Integer'Image (Doubled));
   end;
   Put_Line ("Конец");        -- здесь Temp уже не существует
end;

Блок declare ... begin ... end; вводит собственную маленькую зону объявлений и собственное тело. Переменные Temp и Doubled рождаются при входе в блок и исчезают при выходе из него — за его пределами их имена не видны и память освобождается. Это даёт точный контроль над временем жизни: переменная существует ровно там, где нужна, и ни строкой дольше. Принцип «минимальной области видимости» — важная привычка надёжного программирования: чем короче живёт переменная, тем меньше кода может её случайно испортить и тем легче рассуждать о её значении.

Блоки declare особенно ценны вместе с инициализацией: переменная блока обычно сразу получает осмысленное значение из окружающего контекста, как Temp в примере. Это устраняет окно, в котором переменная существует, но ещё не инициализирована. Кроме того, блок может иметь имя (как именованные циклы) и собственную обработку исключений — то есть это полноценная структурная единица, а не просто косметика. В сумме константы, инициализация и блоки declare образуют связную систему управления данными: значение объявляется как можно ближе к месту использования, инициализируется сразу и защищается от изменения, если по смыслу постоянно. Эта дисциплина — не формальность, а практический заслон от целого спектра ошибок, связанных с «живущими слишком долго» и «неинициализированными» переменными, которые в менее строгих языках являются хроническим источником багов.

Полезно увидеть и то, как из этих кирпичиков складывается типичный осмысленный фрагмент: константы вверху как «настройки», рабочие переменные с инициализацией, локальные блоки для промежуточных расчётов. Опытный разработчик Ada читает такой код как хорошо организованный документ — сразу видно, что неизменно (константы), что вычисляется (переменные) и где границы каждой величины (блоки). Эта дисциплина не возникает сама собой в языках, где переменную можно объявить где угодно и менять что угодно; Ada же подталкивает к ней самой своей структурой. В результате даже простая процедура получается самодокументирующейся: расположение и форма объявлений несут смысл. Для кода, который будут читать и сопровождать годами незнакомые люди, такая встроенная организованность — не косметика, а практический вклад в надёжность, потому что понятный код легче проверять и труднее сломать незаметной правкой.

Частые ошибки и заблуждения

  • Писать = вместо := для присваивания. В Ada = — это сравнение; присваивание всегда :=. Перепутать невозможно по синтаксису, но новички пишут по привычке.
  • Забыть инициализировать. Неинициализированная переменная содержит мусор; читать её до первого присваивания — ошибка. Инициализируйте сразу, где можно.
  • Пытаться менять константу. Присваивание constant-величине отвергается компилятором — это и есть смысл константы.
  • Путать 'A' и "A". Одинарные кавычки — символ (Character), двойные — строка (String); это разные типы.
  • Ожидать вывод типа. Ada требует явно указывать тип в объявлении; «угадывания» по значению нет.

Итоги

  • Объявление имеет форму «Имя : Тип;» и обязательно указывает тип — вывода типов нет, это часть документации и контракта.
  • Присваивание обозначается :=, а = зарезервировано за сравнением — это устраняет классическую путаницу языков С-семейства.
  • Инициализация при объявлении (X : Integer := 0;) поощряется; неинициализированные переменные опасны и отслеживаются инструментами.
  • Слово constant делает величину неизменяемой, и компилятор гарантирует запрет на её изменение.
  • Именованные числа без типа (constant := 0.5;) дают универсальные числовые константы, совместимые с разными типами.
Проверьте себя
1. Почему в Ada присваивание обозначается :=, а не =?
AЭто опечатка в стандарте
BПотому что = зарезервировано за сравнением на равенство, что устраняет путаницу
CЧтобы код был длиннее
DСлучайное наследие
2. Что произойдёт при попытке изменить значение constant?
AИзменится молча
BОшибка компиляции — константу нельзя менять
CПрограмма перезапустится
DЗначение удвоится
3. Требует ли Ada указывать тип при объявлении переменной?
AНет, выводит сам
BДа, тип указывается обязательно — вывода типов нет
CТолько для чисел
DТолько в константах