Приватные типы: абстрактные типы данных
Приватный тип даёт клиенту имя и операции, но прячет внутреннее устройство — это абстрактный тип данных, встроенный в язык.
Приватный тип (private type) — тип, чьё имя видно клиенту, а представление (структура) объявлено в приватной части спецификации и недоступно снаружи: с такими значениями можно делать только то, что разрешают объявленные операции.
В прошлом уроке наш Stack был единственным на всю программу — его состояние жило в теле как глобальные переменные. Это удобно, но негибко: невозможно завести два независимых стека. Приватные типы решают именно эту задачу. Они превращают пакет из «одного объекта» в «фабрику объектов»: клиент объявляет сколько угодно переменных приватного типа, и каждая несёт своё состояние, по-прежнему не зная, как оно устроено внутри.
Зачем нужны приватные типы
Абстрактный тип данных (ADT) — фундаментальная идея инженерии ПО: тип определяется набором операций над ним, а не своим представлением. Целое число — это не «32 бита», а «нечто, что умеет складываться, сравниваться, печататься». Если пользователь типа полагается только на операции, реализация свободна меняться. Ada даёт прямой синтаксис для ADT через ключевое слово private.
Спецификация приватного типа делится на две зоны. Видимая часть (до слова private) объявляет имя типа и операции — это контракт. Приватная часть (после private) объявляет полное представление — оно нужно компилятору, но логически закрыто для клиента. Зачем компилятору видеть представление в спецификации, если клиенту нельзя? Чтобы знать размер типа и уметь размещать переменные на стеке без обращения к динамической памяти. Это сознательный компромисс Ada: представление физически записано в .ads, но правила видимости запрещают клиенту им пользоваться.
-- stack.ads
package Stacks is
type Stack is private; -- клиент видит ИМЯ, не устройство
procedure Push (S : in out Stack; Item : in Integer);
procedure Pop (S : in out Stack; Item : out Integer);
function Is_Empty (S : Stack) return Boolean;
Overflow : exception;
Underflow : exception;
private
-- Приватная часть: представление видно компилятору, но не клиенту
Max : constant := 100;
type Index is range 0 .. Max;
type Data_Array is array (1 .. Max) of Integer;
type Stack is record
Data : Data_Array;
Top : Index := 0;
end record;
end Stacks;
Теперь клиент пишет так:
with Stacks;
procedure Demo is
A, B : Stacks.Stack; -- ДВА независимых стека
X : Integer;
begin
Stacks.Push (A, 1);
Stacks.Push (B, 99);
Stacks.Pop (A, X); -- X = 1, на B не влияет
end Demo;
Попытка написать A.Top := 5; или A.Data (1) := 0; вызовет ошибку компиляции: поля Top и Data вне видимости клиента. Снаружи Stack — атомарная сущность, к которой применимы только Push, Pop и Is_Empty.
Что разрешено делать с приватным типом
Даже не зная представления, клиент получает от языка базовый набор операций, который Ada гарантирует для любого приватного типа: присваивание (:=), проверку на равенство и неравенство (=, /=), передачу как параметр, объявление переменных и констант. Всё остальное (арифметика, доступ к «полям», порядок) недоступно, пока разработчик пакета явно не объявит соответствующие операции в видимой части.
Это поведение по умолчанию — присваивание и равенство — иногда нежелательно. Если ваш тип содержит, например, файловый дескриптор или указатель на уникальный ресурс, копирование через := может породить две переменные, ссылающиеся на один ресурс, — классический источник ошибок. Для таких случаев существует следующий уровень закрытости — limited private, который мы разберём в отдельном уроке: он запрещает само присваивание.
Полное объявление и согласованность
Тип, объявленный type Stack is private; в видимой части, обязан получить полное объявление в приватной части того же пакета — например, как record, как массив или как производный тип. Компилятор следит за согласованностью: нельзя объявить тип приватным и «забыть» дать ему представление. Это снова работает принцип «обещанное должно быть реализовано», но теперь на уровне типов.
-- Приватным может быть не только record. Например, дескриптор как число:
package Handles is
type Handle is private;
function New_Handle return Handle;
function Image (H : Handle) return String;
private
type Handle is new Natural; -- внутри это просто число,
-- но клиент об этом не знает
end Handles;
Как работает под капотом
Компилятор обрабатывает спецификацию пакета в два прохода видимости. Для клиентского кода действует «видимая проекция» типа: известно имя, известны объявленные операции, известен размер (чтобы разместить переменную), но имена полей и структура спрятаны. Для кода внутри тела пакета (и внутри приватной части спецификации) тип раскрыт полностью — там можно обращаться к S.Top и S.Data. Так получается «двойное гражданство»: один и тот же тип имеет полное представление внутри пакета и абстрактное — снаружи.
Именно поэтому представление обязано стоять в приватной части спецификации, а не в теле: тело компилируется отдельно и позже, а размер типа нужен компилятору уже в момент компиляции спецификации, чтобы клиенты могли размещать объекты. Языки, прячущие реализацию полностью (например, через указатель-«pimpl» в C++), платят за это лишним уровнем косвенности и обращением к куче; Ada же позволяет приватному типу жить прямо на стеке.
Приватные типы и сравнение с другими языками
Стоит сопоставить приватные типы Ada с тем, как инкапсуляцию достигают другие языки, — это проясняет, что именно даёт подход Ada. В C++ закрытость выражается ключевым словом private внутри класса, но приватные поля всё равно объявлены в заголовке, который видят все клиенты: они не могут к ним обращаться, но зависят от них по компиляции, и изменение приватного поля заставляет пересобрать всех. Чтобы спрятать реализацию полностью, в C++ прибегают к идиоме «pimpl» (указатель на скрытую реализацию), платя за это разыменованием указателя и обращением к куче при каждом доступе. Java прячет поля за модификаторами доступа, но объекты всегда живут в куче и достигаются по ссылке — стековых значений-объектов в Java нет вовсе.
Приватный тип Ada сочетает достоинства обоих миров без их недостатков. Представление спрятано от клиента логически (правилами видимости), но известно компилятору физически (оно в приватной части спецификации), поэтому объект может размещаться прямо на стеке, без косвенности и без кучи. Вы получаете полную инкапсуляцию интерфейса и при этом эффективность value-типа. Для систем реального времени и встраиваемого ПО, где обращения к динамической памяти нежелательны или вовсе запрещены, это решающее преимущество: абстракция не тянет за собой накладных расходов.
Приватный тип как граница ответственности
Есть и проектная сторона. Приватный тип проводит чёткую границу ответственности: за инвариант значения отвечает исключительно код пакета, а не разбросанные по программе клиенты. Если значение типа Date всегда должно быть корректной датой, гарантировать это можно, только если никто извне не может залезть в поля и выставить «32 января». Приватность делает такую гарантию выполнимой: все пути изменения значения проходят через операции пакета, и достаточно, чтобы эти операции поддерживали инвариант. Это фундамент надёжного кода — сосредоточить ответственность за корректность данных в одном месте, а не размазать её по всей системе. В следующих уроках раздела о контрактах мы увидим, как Type_Invariant делает эту гарантию ещё и проверяемой автоматически.
Идиома «полное скрытие» и её цена
Иногда требуется спрятать представление приватного типа даже от компилятора клиента — например, чтобы изменение размера внутренней структуры вообще не влияло на клиентский код. Для этого применяют идиому, где приватный тип объявляется как access-тип (указатель) на скрытую в теле структуру: в приватной части стоит лишь указатель фиксированного размера, а настоящие поля живут в теле и невидимы никому извне. Это аналог идиомы «pimpl» из C++ и даёт максимальную развязку: внутренности можно менять как угодно, не затрагивая представления в спецификации.
Но у полного скрытия есть цена, и зрелый инженер её взвешивает. Указатель означает размещение объекта в куче и обращение через косвенность при каждом доступе — это и накладные расходы по времени, и зависимость от динамической памяти, нежелательная во встраиваемых системах. Поэтому в Ada такую идиому применяют сознательно и выборочно: для большинства приватных типов выгоднее обычное размещение на стеке (представление в приватной части), а полное скрытие через указатель приберегают для случаев, где развязка реализации действительно важнее эффективности. Это характерный для языка осознанный компромисс: инструмент есть, но применяется там, где его выгода перевешивает издержки, а не по умолчанию.
Стоит закрепить и фундаментальную роль приватных типов в проектировании на Ada. Они не просто «скрывают поля» — они задают словарь предметной области на уровне типов. Объявляя Account, Temperature, Matrix приватными типами с осмысленными операциями, инженер строит из них язык, на котором затем выражается логика всей системы: вместо манипуляций сырыми числами и массивами код оперирует понятиями задачи. Это поднимает уровень рассуждений: ошибки уровня представления (перепутать индексы, нарушить структуру) становятся невозможными, потому что представления попросту не видно, а остаются лишь ошибки уровня логики, которые заметнее и реже. Приватный тип — это кирпич, из которого складывают абстракции, понятные человеку, и именно поэтому он, а не пакет с глобальным состоянием, является основным способом строить компоненты в Ada. Освоив его, вы получаете главный инструмент управления сложностью, на котором держится весь остальной язык.
Частые ошибки
- Пытаться обратиться к полям приватного типа из клиента.
A.Topвне пакета — ошибка компиляции. Это и есть цель приватности: снаружи нет полей, есть только операции. - Забыть полное объявление в приватной части. Объявив
type T is private;, обязательно дайтеtype T is ...;послеprivate— иначе тип неполный и пакет не скомпилируется. - Полагаться на присваивание там, где его быть не должно. Для типов с уникальными ресурсами стандартное копирование через
:=опасно; такие типы стоит делатьlimited private. - Класть представление в тело «для большей секретности». Размер типа нужен компилятору в спецификации; представление приватного типа обязано быть в приватной части
.ads, а не в.adb.
Итоги
- Приватный тип = абстрактный тип данных: имя и операции видны, представление скрыто.
- Спецификация делится на видимую часть (контракт) и приватную часть (представление для компилятора).
- Клиент может объявлять много переменных приватного типа — каждая несёт своё состояние.
- По умолчанию доступны присваивание и сравнение на равенство; всё прочее — только через объявленные операции.
- Полное объявление обязано стоять в приватной части того же пакета; размер типа нужен компилятору уже там.