Generics против ООП: два полиморфизма
Generics и ООП — два разных полиморфизма: статический (по типам, во время компиляции) и динамический (по объектам, во время выполнения). Зрелый инженер выбирает по задаче.
Параметрический против включающего полиморфизма: generics дают статический полиморфизм — один код работает с разными типами, связывание происходит на этапе компиляции; тэгированные типы (ООП) дают динамический полиморфизм — выбор операции происходит во время выполнения по фактическому типу объекта.
Ada — один из немногих языков, где оба вида полиморфизма реализованы первоклассно и ортогонально, и важно понимать, когда какой уместен. Этот урок завершает раздел о generics, ставя их в контекст: чем они хороши, где их граница, и как они соотносятся с объектно-ориентированной моделью, которую мы детально разберём в финальном разделе курса. Понимание этого выбора отличает того, кто «знает синтаксис», от того, кто проектирует системы.
Два полиморфизма: в чём разница
Представьте задачу «обработать набор фигур». Решение через generics: обобщённый алгоритм параметризуется типом фигуры, и для каждого типа компилятор порождает свой специализированный код — все типы известны на этапе компиляции, никакой неоднородности в одной коллекции. Решение через ООП: все фигуры наследуют общий тэгированный тип, в одной коллекции лежат объекты разных конкретных типов, а вызов Area диспетчеризуется во время выполнения к нужной реализации.
| Свойство | Generics (статический) | ООП / tagged (динамический) |
| Момент связывания | компиляция | выполнение |
| Однородность коллекции | один тип на экземпляр | разные типы в одной коллекции |
| Стоимость вызова | прямой вызов, можно инлайн | косвенный (через таблицу диспетчеризации) |
| Предсказуемость для RT | максимальная | требует учёта диспетчеризации |
| Расширение новым типом | новый инстанс (правка места использования) | новый потомок (без правки существующего кода) |
Грубое правило: если множество типов фиксировано и известно, а важны скорость и предсказуемость — generics. Если набор типов открыт и расширяется, а в одной структуре должны сосуществовать разнородные объекты — ООП. Нередко лучшие решения сочетают оба: обобщённый контейнер (статический полиморфизм по структуре) хранит элементы тэгированного типа (динамический полиморфизм по поведению элемента).
Идиомы: generics как «паттерны», встроенные в язык
Многое из того, что в других языках оформляют как «паттерны проектирования», в Ada выражается обобщёнными единицами напрямую. Обобщённая итерация по структуре — это паттерн «Итератор», заданный одним with procedure Process (...). Семейство Ada.Containers — это «Коллекции» как библиотека. Обобщённый объект, параметризованный стратегией сравнения, — это «Стратегия». Язык настолько выразителен в обобщениях, что паттерны не нужно «изобретать» — они становятся прямыми конструкциями.
-- "Итератор" как обобщённая процедура: обходит массив, применяя операцию
generic
type Element is private;
type Index is (<>);
type Arr is array (Index range <>) of Element;
with procedure Process (Item : in out Element); -- что делать с каждым
procedure For_Each (Container : in out Arr);
procedure For_Each (Container : in out Arr) is
begin
for I in Container'Range loop
Process (Container (I)); -- вызываем переданную операцию
end loop;
end For_Each;
-- Применение: удвоить все элементы
procedure Double (X : in out Integer) is
begin
X := X * 2;
end Double;
type Int_Array is array (Positive range <>) of Integer;
procedure Double_All is new For_Each
(Integer, Positive, Int_Array, Process => Double);
Границы обобщённого подхода
У статического полиморфизма есть естественные пределы, которые честный инженер должен знать. Во-первых, неоднородность: обобщённый экземпляр «стек целых» хранит только целые; чтобы в одной структуре лежали и целые, и строки, и пользовательские объекты, нужен динамический полиморфизм (общий тэгированный предок). Во-вторых, раздувание кода при стратегии раскрытия: множество экземпляров крупного шаблона могут увеличить размер бинарника (реализации с разделяемым кодом это смягчают). В-третьих, выбор реализации во время выполнения: если конкретный тип становится известен лишь в рантайме (из конфигурации, из сети), generics не помогут — нужна динамическая диспетчеризация.
Именно эти границы и закрывает объектно-ориентированная модель Ada — тэгированные типы, наследование, диспетчеризация по 'Class, интерфейсы, — которой целиком посвящён заключительный раздел курса. Там вы увидите, как разнородные объекты живут в одной коллекции, как добавить новый тип, не трогая существующий код, и как Ada удерживает даже динамический полиморфизм в рамках своей дисциплины безопасности.
Как работает под капотом
Разница двух полиморфизмов видна в сгенерированном коде. Вызов через generic — это прямой вызов конкретной подпрограммы, известной компилятору; его можно встроить (inline), оптимизировать, и он не несёт косвенности. Вызов через диспетчеризацию (ООП) идёт через таблицу методов конкретного объекта: на этапе компиляции известна лишь сигнатура, а адрес тела выбирается в рантайме по тегу объекта. Отсюда и компромиссы: generics быстрее и предсказуемее, но «застывают» на этапе компиляции; ООП гибче и расширяемее, но платит косвенностью. Ada не навязывает выбор — она даёт оба инструмента и оставляет решение инженеру, что полностью в духе языка: явный контроль вместо скрытых умолчаний.
Проектное решение: когда что выбирать
Выбор между статическим (generics) и динамическим (ООП) полиморфизмом — одно из тех проектных решений, которые определяют качество архитектуры на годы вперёд, поэтому стоит сформулировать ориентиры подробнее. Ключевой вопрос: известно ли множество типов на этапе компиляции и фиксировано ли оно? Если да — например, вы пишете контейнер, который будет инстанцирован под конкретные, заранее известные типы элементов, — generics дают прямой вызов, возможность инлайна, отсутствие косвенности и максимальную предсказуемость по времени. Если множество типов открыто и должно расширяться без правки существующего кода (новые виды устройств, новые форматы сообщений, плагины) — нужен динамический полиморфизм тэгированных типов, где новый потомок добавляется, не трогая старое.
Второй вопрос: нужна ли неоднородность в одной структуре данных? Обобщённый контейнер хранит элементы одного типа: «вектор целых», «список строк». Если же в одной коллекции должны сосуществовать объекты разных конкретных типов с общим интерфейсом — фигуры разных видов, узлы разных классов, — это невозможно выразить generics и требует class-wide типа (T'Class) с диспетчеризацией. Третий вопрос — про время выполнения и сертификацию: в жёстком реальном времени косвенный диспетчеризуемый вызов нужно учитывать в анализе времени, и иногда его сознательно избегают в пользу статически связанного обобщённого кода; некоторые стандарты безопасности и вовсе ограничивают динамическую диспетчеризацию.
Сила Ada — в наличии обоих инструментов
Главная мысль, которой стоит завершить раздел: уникальность Ada не в том, что она предлагает generics или ООП, а в том, что она предлагает оба механизма первоклассно и ортогонально, оставляя выбор инженеру. Многие языки навязывают один путь: ранние ООП-языки тяготели к динамике для всего, ранний C — к ручному обобщению через указатели. Ada же позволяет применять статический полиморфизм там, где важны скорость и предсказуемость, динамический — там, где важны расширяемость и неоднородность, и даже сочетать их: обобщённый контейнер (статический полиморфизм по структуре), хранящий элементы тэгированного типа (динамический полиморфизм по поведению элемента). Эта свобода выбора, подкреплённая строгой проверкой обоих механизмов, — прямое выражение философии языка: дать инженеру явный контроль над тем, какой ценой достигается гибкость, вместо того чтобы прятать решение за умолчаниями.
Сочетание полиморфизмов в одном дизайне
Лучшие архитектуры на Ada нередко используют оба полиморфизма вместе, и стоит увидеть, как именно они складываются. Типичный приём — обобщённый контейнер, хранящий элементы тэгированного типа. Контейнер (вектор, список) параметризован статически: его инстанцируют под класс-широкий тип Shape'Class, и структура хранения, обход, управление памятью связываются на этапе компиляции — быстро и предсказуемо. А вот элементы внутри полиморфны динамически: в одной коллекции лежат круги, прямоугольники, любые будущие фигуры, и вызов их операций диспетчеризуется по фактическому типу. Так статический полиморфизм отвечает за «структуру вообще», а динамический — за «поведение конкретного элемента».
Это разделение ответственности глубоко осмысленно. Структура данных (как хранить и обходить) обычно фиксирована и выигрывает от статической эффективности обобщений. Набор же конкретных типов элементов открыт и расширяем, и выигрывает от гибкости наследования. Соединяя их, инженер получает контейнер, который и эффективен, и расширяем одновременно — то, чего ни один из полиморфизмов не дал бы в одиночку. Именно поэтому уникальность Ada не в наличии generics или ООП по отдельности, а в их ортогональности: язык позволяет выбрать для каждой оси задачи (структура против поведения, фиксированное против расширяемого) подходящий механизм и аккуратно совместить их, оставляя инженеру осознанный контроль над тем, какой ценой достигается каждое свойство системы.
Частые ошибки
- Тянуть generics туда, где нужна неоднородность. Если в одной коллекции должны сосуществовать объекты разных типов, это задача для тэгированных типов, а не для обобщённого контейнера одного типа.
- Использовать ООП-диспетчеризацию в горячем цикле реального времени без анализа. Косвенный вызов и его время должны быть учтены; там, где типы фиксированы, generics дадут прямой вызов и предсказуемость.
- Считать, что generics и ООП исключают друг друга. Сильные решения часто сочетают их: обобщённый контейнер тэгированных элементов объединяет оба полиморфизма.
- Изобретать «паттерны» поверх языка. Многие классические паттерны (Итератор, Стратегия, Коллекции) в Ada — прямые обобщённые конструкции; не усложняйте то, что язык уже выражает.
Итоги
- Generics — статический (параметрический) полиморфизм: связывание на компиляции, один тип на экземпляр, прямой вызов, максимальная предсказуемость.
- ООП (тэгированные типы) — динамический полиморфизм: связывание в рантайме, разнородные объекты в одной коллекции, расширение без правки старого кода.
- Выбор по задаче: фиксированные типы и скорость → generics; открытый набор и неоднородность → ООП; часто их сочетают.
- Многие паттерны проектирования в Ada выражаются обобщёнными единицами напрямую (Итератор, Стратегия, Коллекции).
- Границы generics (неоднородность, runtime-выбор типа) закрывает объектная модель — тема финального раздела курса.