Тэгированные типы: основа ООП

Тэгированный тип — фундамент ООП в Ada: запись, несущая «тег» своего фактического типа, которую можно расширять наследованием и по которой работает динамическая диспетчеризация.

Тэгированный тип (tagged type) — запись, помеченная ключевым словом tagged: каждый её объект во время выполнения несёт скрытый тег своего конкретного типа, что делает возможными наследование с расширением полей и динамический выбор операций (диспетчеризацию).

Объектно-ориентированное программирование пришло в Ada с ревизией 1995 года, превратив её в один из первых мейнстримных языков с полноценным ООП. Но Ada не стала копировать модель C++ или Smalltalk — она встроила объекты в свою существующую систему типов органично и с фирменной строгостью. Здесь нет отдельного понятия «класс» как контейнера данных и методов; вместо этого роль объекта играет тэгированная запись, а методы — это обычные подпрограммы, у которых первый параметр имеет тэгированный тип. Такой подход кажется непривычным тем, кто пришёл из Java, но он мощнее и честнее: он явно показывает, что «метод» — это операция над типом, а не магическое свойство, спрятанное внутри.

Зачем нужны тэгированные типы

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

--  Базовый тэгированный тип — корень будущей иерархии
type Shape is tagged record
   X, Y : Float := 0.0;            -- общие для всех фигур координаты
end record;

--  Примитивные операции (методы): подпрограммы с параметром типа Shape
function Area (S : Shape) return Float;
procedure Move (S : in out Shape; DX, DY : Float);

function Area (S : Shape) return Float is (0.0);  -- у абстрактной фигуры площади нет
procedure Move (S : in out Shape; DX, DY : Float) is
begin
   S.X := S.X + DX;
   S.Y := S.Y + DY;
end Move;

Подпрограммы Area и Move, объявленные в той же области, что и тип Shape, и имеющие параметр этого типа, называются примитивными операциями — это и есть «методы» в терминологии Ada. Именно они наследуются потомками и участвуют в диспетчеризации (об этом — в следующих уроках).

Точечная нотация: знакомый синтаксис вызова

С Ada 2005 примитивные операции можно вызывать в привычном «объектном» стиле через точку: S.Area вместо Area (S), S.Move (1.0, 2.0) вместо Move (S, 1.0, 2.0). Это синтаксический сахар — обе формы эквивалентны, — но он делает код ближе к ожиданиям программиста из мира Java/C++ и подчёркивает, что операция «принадлежит» объекту.

declare
   S : Shape;
begin
   S.Move (3.0, 4.0);             -- то же, что Move (S, 3.0, 4.0)
   Ada.Text_IO.Put_Line (Float'Image (S.Area));   -- 0.00000E+00
end;

Тег и идентичность типа во время выполнения

Тег — это не абстракция, а вполне конкретная сущность. Тип Ada.Tags.Tag из стандартной библиотеки представляет «идентификатор типа», а атрибут 'Tag позволяет получить тег объекта или типа. Это даёт интроспекцию: можно узнать имя фактического типа объекта (Ada.Tags.Expanded_Name (S'Tag)), сравнить теги, проверить принадлежность. Но в идиоматичной Ada прямое сравнение тегов используют редко — обычно вместо ручной проверки типа полагаются на диспетчеризацию, которая делает выбор автоматически и безопасно. Тег существует «под капотом» именно для того, чтобы механизм диспетчеризации работал.

with Ada.Tags; use Ada.Tags;
with Ada.Text_IO; use Ada.Text_IO;
...
declare
   S : Shape;
begin
   Put_Line (Expanded_Name (S'Tag));   -- печатает полное имя типа, напр. "...SHAPE"
end;

Как работает под капотом

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

Объектная модель Ada в сравнении с другими языками

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

Поначалу это кажется непривычным, но за этим стоит глубокая последовательность. Во-первых, исчезает искусственная асимметрия: в Java вызов a.f(b) трактует a особо (это «получатель»), а b — обычный аргумент, хотя оба могут быть равноправны по смыслу; в Ada F (A, B) симметрично, а диспетчеризация при необходимости выбирается по любому из управляющих параметров тэгированного типа. Во-вторых, операции естественно встраиваются в уже существующую систему пакетов и видимости Ada — «класс» не вводит отдельных правил, он переиспользует пакет как единицу инкапсуляции. Точечная нотация A.F (B), добавленная в Ada 2005, даёт привычный объектный синтаксис как удобный сахар, не меняя этой стройной семантики.

Почему ООП пришло в Ada позже и осторожно

Полезно понимать, что объектная модель появилась в Ada лишь с ревизией 1995 года — на двенадцать лет позже первой версии языка. Это не отставание, а осознанная сдержанность. Авторы не стали слепо копировать тогдашнюю моду на «всё есть объект», а интегрировали ООП так, чтобы оно сосуществовало с уже имеющимися механизмами — сильной типизацией, пакетами, generics — и не подрывало гарантий безопасности. Результат — модель, в которой за объектную механику (тег, таблица диспетчеризации, косвенный вызов) платят только те типы, что явно объявлены tagged, а обычные записи остаются легковесными значениями без рантайм-накладных расходов. Принцип «не платишь за то, чем не пользуешься» проведён здесь так же строго, как в C++, но при этом ООП Ada аккуратнее в части безопасности: явные overriding, контролируемое наследование, интерфейсы без ромбовидной проблемы — всё это плоды позднего и продуманного внедрения.

Примитивные операции и наследование на практике

Чтобы объектная модель Ada улеглась окончательно, важно ясно понимать механику примитивных операций при наследовании. Когда вы объявляете потомка Circle от Shape, он автоматически наследует все примитивные операции родителя: и Area, и Move становятся доступны для Circle без всякого повторного объявления. Дальше у вас выбор по каждой операции: оставить унаследованную как есть (так Circle наследует Move — перемещение координат одинаково для всех фигур) либо переопределить её своей версией (так Circle переопределяет Area, ведь площадь круга считается иначе). Унаследованные и переопределённые операции вместе образуют полный набор методов потомка.

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

Частые ошибки

  • Ожидать наследования от обычной записи. Расширять наследованием можно только tagged-типы; нетэгированную запись потомком не продолжить. Нужна иерархия — объявляйте корень tagged.
  • Искать «класс» как в Java. В Ada нет отдельной конструкции class с данными и методами внутри; «класс» = тэгированный тип плюс его примитивные операции (подпрограммы с параметром этого типа).
  • Считать примитивной любую подпрограмму с параметром типа. Примитивной операция становится, только если объявлена в той же области видимости (пакете), что и сам тип; объявленная в другом месте — обычная подпрограмма, не наследуется и не диспетчеризуется.
  • Злоупотреблять ручной проверкой 'Tag. Сравнение тегов вручную обычно лишнее: для выбора поведения по типу существует диспетчеризация, которая делает это автоматически и безопаснее.

Итоги

  • Тэгированный тип (tagged record) — основа ООП в Ada: его объекты несут рантайм-тег своего конкретного типа.
  • Методы — это примитивные операции: подпрограммы с параметром тэгированного типа, объявленные в той же области; они наследуются и диспетчеризуются.
  • Точечная нотация (S.Area) — синтаксический сахар, эквивалентный Area (S), привычный по другим ООП-языкам.
  • Тег даёт рантайм-идентичность типа ('Tag, Ada.Tags), но прямое сравнение тегов в идиоматичной Ada применяют редко.
  • За ООП-механику (тег, таблица диспетчеризации) вы платите только при tagged; обычные записи остаются легковесными.
Проверьте себя
1. Что в Ada называется примитивной операцией (методом) тэгированного типа?
AЛюбая функция, где-либо принимающая объект этого типа
BПодпрограмма с параметром этого типа, объявленная в той же области видимости (пакете), что и сам тип
CТолько процедуры, но не функции
DПодпрограмма, объявленная внутри ключевого слова class
2. Чем тэгированная запись физически отличается от обычной записи Ada?
AНичем, разница только в синтаксисе объявления
BОбъект тэгированного типа дополнительно несёт скрытый тег — указатель на таблицу диспетчеризации своего конкретного типа
CТэгированная запись не может иметь полей
DОбычная запись хранится в куче, а тэгированная — на стеке