Записи: поля, дискриминанты и варианты

Записи в Ada: объединение разнородных полей в один тип, а затем — записи с дискриминантом и вариантные записи, которые меняют свою структуру в зависимости от параметра, оставаясь типобезопасными.

Запись (record) — составной тип, объединяющий именованные поля разных типов в единое целое; дискриминант параметризует запись, а вариантная часть меняет набор полей в зависимости от дискриминанта.

Базовые записи: разнородные данные вместе

Массив хранит много элементов одного типа. Запись же объединяет разнородные поля, относящиеся к одной сущности. Это аналог структур из других языков:

type Person is record
   Name : String (1 .. 20);
   Age  : Natural;
   Active : Boolean;
end record;

P : Person;
begin
   P.Age    := 30;          -- доступ к полю через точку
   P.Active := True;

Между record и end record; перечислены поля: каждое со своим именем и типом. Доступ к полю — через точку: P.Age. Запись объединяет логически связанные данные (имя, возраст, статус человека) в один объект, который можно передавать, присваивать и сравнивать целиком. Как и массивы, записи — значения целиком: P2 := P1; копирует все поля, P1 = P2 сравнивает их поэлементно.

Агрегаты записей и значения по умолчанию

Запись тоже задаётся агрегатом — позиционно или по именам полей:

P : Person := (Name => "Ада Лавлейс         ",
               Age  => 36,
               Active => True);

Именованный агрегат записи особенно нагляден: видно, какому полю что присвоено. Компилятор, как и с массивами, проверит, что покрыты все поля — забытое поле без значения по умолчанию вызовет ошибку. Полям можно задать значения по умолчанию прямо в объявлении типа:

type Config is record
   Retries : Natural := 3;        -- значение по умолчанию
   Verbose : Boolean := False;
end record;

C : Config;     -- получит Retries=3, Verbose=False автоматически

Теперь объект C создаётся уже осмысленно инициализированным, без отдельного присваивания. Значения по умолчанию в записях — хороший способ гарантировать, что объект никогда не начинает жизнь с мусором.

Записи с дискриминантом: параметризованные записи

Теперь — мощная и характерная для Ada конструкция. Дискриминант — это параметр самой записи, влияющий на её устройство; чаще всего им задают размер внутренних массивов:

type Buffer (Size : Positive) is record
   Data : String (1 .. Size);    -- длина зависит от дискриминанта!
   Used : Natural := 0;
end record;

B1 : Buffer (10);     -- буфер на 10 символов
B2 : Buffer (256);    -- буфер на 256 символов, но тип ТОТ ЖЕ

Дискриминант Size указывается в скобках после имени типа и используется внутри — здесь он задаёт длину поля Data. При создании объекта вы задаёте конкретное значение дискриминанта: Buffer (10) и Buffer (256) — один тип, но разного внутреннего размера. Это похоже на неограниченные массивы, но мощнее: дискриминант — полноценное поле, его можно прочитать (B1.Size даст 10), и от него может зависеть структура записи. Дискриминантные записи позволяют параметризовать данные, не плодя типы и не прибегая к динамической памяти — что важно для встраиваемых систем.

Вариантные записи: структура по выбору

Кульминация раздела — вариантные записи. Иногда набор нужных полей зависит от некоторого признака. Например, геометрическая фигура: для круга нужен радиус, для прямоугольника — стороны, для точки — ничего. Хранить все поля сразу расточительно и небезопасно. Ada решает это вариантной частью, управляемой дискриминантом:

type Shape_Kind is (Point, Circle, Rectangle);

type Shape (Kind : Shape_Kind) is record
   X, Y : Float;                       -- общие поля для всех фигур
   case Kind is                        -- вариантная часть
      when Point =>
         null;                         -- у точки нет доп. полей
      when Circle =>
         Radius : Float;
      when Rectangle =>
         Width, Height : Float;
   end case;
end record;

Дискриминант Kind определяет, какие поля присутствуют. У всех фигур есть координаты X, Y, но дальше идёт case Kind is — вариантная часть. Для Circle существует поле Radius, для RectangleWidth и Height, а для Point дополнительных полей нет (null). Обратите внимание: вариантная часть использует тот же case с тем же требованием покрыть все значения дискриминанта. Создаются объекты так:

C : Shape := (Kind => Circle, X => 0.0, Y => 0.0, Radius => 5.0);
R : Shape := (Kind => Rectangle, X => 1.0, Y => 1.0, Width => 4.0, Height => 3.0);

Безопасность вариантных записей

Вот что делает вариантные записи Ada особенными по сравнению с «объединениями» (union) из C. Доступ к полю варианта проверяется на соответствие дискриминанту. Если попытаться прочитать C.Width у объекта, чей Kind = Circle (у круга нет ширины), это поднимет Constraint_Error. В C аналогичный union позволяет интерпретировать одну и ту же память как угодно — источник опасных ошибок и уязвимостей. Ada же помнит, какой вариант активен (через дискриминант), и не даёт обратиться к неактивному полю. Это типобезопасный размеченный союз: структура гибкая, но доступ всегда корректен. Обрабатывают вариантную запись обычно через case по дискриминанту:

case C.Kind is
   when Point     => Put_Line ("Точка");
   when Circle    => Put_Line ("Радиус:" & Float'Image (C.Radius));
   when Rectangle => Put_Line ("Стороны заданы");
end case;

Здесь компилятор знает: в ветви Circle поле Radius доступно, и обращение безопасно. Полнота case снова гарантирует, что все варианты учтены.

Как работает под капотом вариантная запись

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

Записи в пакетах: приватные типы и инкапсуляция

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

package Accounts is
   type Account is private;                 -- устройство скрыто
   function Open (Initial : Natural) return Account;
   procedure Deposit (A : in out Account; Amount : Natural);
   function Balance (A : Account) return Natural;
private
   type Account is record                   -- видно только внутри пакета
      Balance : Natural := 0;
   end record;
end Accounts;

В публичной части (до слова private) объявлено type Account is private; — пользователь знает, что тип есть, и видит операции Open, Deposit, Balance, но не видит поля. Реальное устройство (запись с полем Balance) спрятано в приватной части пакета, доступной только его телу. Снаружи нельзя написать A.Balance := -100; — поле невидимо, и баланс можно менять только через предусмотренные операции. Это полная инкапсуляция: внутреннее представление защищено, инварианты (например, «баланс неотрицателен») гарантированы, а реализацию можно переделать, не затронув пользователей.

Вот почему записи и пакеты — естественные партнёры. Запись описывает данные сущности, пакет с приватным типом описывает её как абстрактный тип данных: данные плюс операции плюс сокрытие. Пользователь работает с Account через осмысленный интерфейс, не зная и не завися от того, что внутри — простое поле, сложная структура или вариантная запись. Это та самая модульность из требований Steelman, доведённая до уровня типов: каждый тип — маленький защищённый мир со своим контрактом.

Сюда же добавляются инварианты типов (Ada 2012) — условия, которые должны выполняться для любого значения приватного типа всегда: type Account is private with Type_Invariant => Balance (Account) >= 0;. (Стрелка => и оператор >= в HTML экранируются.) Компилятор проверяет инвариант на границах операций, гарантируя, что объект никогда не окажется в недопустимом состоянии. В сумме приватные записи, операции пакета и инварианты дают мощнейший способ строить надёжные абстракции: неправильное состояние становится непредставимым, потому что добраться до внутренностей в обход проверенных операций просто нельзя. Это вершина того, ради чего нужны записи, дискриминанты и варианты — не просто хранить поля, а строить защищённые, самосогласованные типы данных, которым можно доверять в критичной системе.

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

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

  • Обращаться к полю неактивного варианта. Чтение C.Width при Kind = Circle даёт Constraint_Error; сначала проверьте дискриминант (обычно через case).
  • Забывать вариант в case вариантной части. Как обычный case, вариантная часть обязана покрыть все значения дискриминанта.
  • Путать запись с массивом. Массив — много элементов одного типа по индексу; запись — разнородные именованные поля по точечному доступу.
  • Не задавать значения по умолчанию. Поля без значений по умолчанию в агрегате обязательны; для безопасной инициализации задавайте умолчания в типе.
  • Считать вариантную запись «как union в C». В Ada доступ к вариантам проверяется дискриминантом — нельзя интерпретировать память произвольно, как в C.

Итоги

  • Запись объединяет разнородные именованные поля в один тип; доступ через точку, операции (присваивание, сравнение) — над записью целиком.
  • Поля могут иметь значения по умолчанию, что гарантирует осмысленную инициализацию объекта без мусора.
  • Дискриминант параметризует запись (например, задаёт размер внутреннего массива), позволяя одному типу иметь объекты разной конфигурации без динамической памяти.
  • Вариантная запись меняет набор полей в зависимости от дискриминанта через case, моделируя «фигуру/сообщение/команду» нескольких видов.
  • Доступ к вариантным полям проверяется дискриминантом — это типобезопасная альтернатива «грязным» union из C, с сохранением гибкости и эффективности.
Проверьте себя
1. Чем вариантная запись Ada безопаснее union из C?
AОна быстрее
BДоступ к вариантным полям проверяется дискриминантом; нельзя интерпретировать память произвольно
CОна меньше по размеру
DНичем
2. Для чего служит дискриминант записи?
AДля имени поля
BПараметризует запись (например, задаёт размер внутреннего массива) без динамической памяти
CЭто комментарий
DУскоряет доступ
3. Как обращаются к полю записи?
AПо индексу в круглых скобках
BЧерез точку, например P.Age
CЧерез 'Image
DЧерез with