Ограниченные приватные типы (limited private)
Ограниченный приватный тип запрещает присваивание и сравнение — он защищает уникальные ресурсы от случайного копирования.
Ограниченный приватный тип (limited private) — приватный тип, для которого язык не предоставляет встроенного присваивания (
:=) и сравнения на равенство (=): объекты такого типа нельзя копировать, пока разработчик явно не разрешит это безопасным образом.
Обычный приватный тип всё же даёт клиенту две «бесплатные» операции — присваивание и проверку равенства. Для многих типов это удобно и правильно. Но представьте тип, который владеет уникальным ресурсом: открытым файлом, сетевым сокетом, блоком памяти, мьютексом. Что значит «скопировать» открытый файл оператором :=? Получатся две переменные, ссылающиеся на один дескриптор; закрытие одной испортит другую. А что значит сравнить два мьютекса на «равенство»? Вопрос бессмыслен. Для таких сущностей сама операция копирования — ошибка, и язык должен уметь её запретить.
Зачем нужен ещё более строгий уровень
Ada выстраивает закрытость лесенкой: публичный тип → приватный тип → ограниченный приватный тип. Каждая ступень убирает одну категорию «бесплатных» возможностей. Limited private убирает самую опасную для ресурсо-владеющих типов — неявное копирование. Это не «оптимизация», а инструмент корректности: компилятор физически не даст написать B := A;, если A — открытый файл, и тем самым предотвратит целый класс ошибок aliasing (двойного владения).
В стандартной библиотеке этот приём встречается постоянно. Тип File_Type из пакета Ada.Text_IO — ограниченный: вы не можете присвоить один файловый объект другому, потому что это разрушило бы инвариант «один объект — один поток». Файл передаётся в подпрограммы по ссылке (in out), но не копируется.
-- resource.ads — тип, владеющий уникальным дескриптором
package Resources is
type Resource is limited private; -- НЕЛЬЗЯ копировать и сравнивать
procedure Open (R : in out Resource; Name : in String);
procedure Close (R : in out Resource);
function Is_Open (R : Resource) return Boolean;
private
type Resource is record
Handle : Integer := -1; -- -1 = закрыт
Name : String (1 .. 64) := (others => ' ');
end record;
end Resources;
Клиент работает с ресурсом, но не может его клонировать:
with Resources;
procedure Demo is
R1 : Resources.Resource;
R2 : Resources.Resource;
begin
Resources.Open (R1, "data.log");
-- R2 := R1; -- ОШИБКА КОМПИЛЯЦИИ: тип limited, копирование запрещено
-- if R1 = R2 then -- ОШИБКА: встроенного "=" нет
Resources.Close (R1);
end Demo;
Инициализация вместо присваивания
Раз присваивание запрещено, возникает вопрос: как же создать и проинициализировать объект? Ответ: ограниченный объект инициализируется «на месте» — либо значениями полей по умолчанию (как Handle := -1 выше), либо через специальную конструкцию ограниченного агрегата, либо через функцию-конструктор, возвращающую limited-тип (в современном Ada функция может вернуть limited-результат, и он строится прямо в целевой переменной — build-in-place, без копирования). Идиома «фабричная функция + инициализация на месте» заменяет привычное «создать копию».
-- Современная Ada: функция строит limited-объект прямо в переменной
function Make (Name : String) return Resource is
begin
return R : Resource do -- расширенный return: строим на месте
R.Name (1 .. Name'Length) := Name;
R.Handle := 0;
end return;
end Make;
Здесь синтаксис return R : Resource do ... end return; (расширенный оператор return) создаёт объект-результат, заполняет его поля и возвращает без промежуточной копии. Для limited-типов это единственный способ вернуть значение из функции — и одновременно элегантный.
Можно ли разрешить осмысленное копирование
Иногда копирование ресурса осмысленно, но требует особой логики (например, продублировать дескриптор через системный вызов). Тогда разработчик пакета объявляет собственную процедуру Copy (Target : in out Resource; Source : in Resource) с нужной семантикой. Ключевое отличие от обычного приватного типа: копирование становится явной, контролируемой операцией, а не молчаливым следствием :=. Это и есть цель limited private — сделать опасную операцию видимой и управляемой.
Как работает под капотом
Запрет присваивания для limited-типов — чисто статическое свойство: его обеспечивает компилятор на этапе анализа, никаких затрат во время выполнения нет. Когда вы передаёте limited-объект в подпрограмму как in out, передаётся ссылка (адрес), а не копия, — поэтому большие или ресурсо-владеющие объекты ходят по программе эффективно и безопасно. Механизм build-in-place гарантирует, что даже результат функции не порождает лишних копий: компилятор «знает» итоговый адрес назначения заранее и строит объект сразу там.
Любопытно, что эта модель десятилетиями опережала мейнстрим. Идея «типов, которые нельзя копировать, только перемещать/строить на месте» пришла в C++ только с move-семантикой (C++11) и std::unique_ptr, а в Rust стала центральной (типы без Copy, владение). Ada выразила суть ещё в 1983 году декларативным словом limited.
Где limited-типы встречаются на практике
Чтобы limited private не казался экзотикой, посмотрим, где он работает в реальном коде. Самый наглядный пример — уже упомянутый File_Type из Ada.Text_IO: файловый объект нельзя копировать, его передают по ссылке в Put, Get, Close. Точно так же ограниченными делают типы, представляющие аппаратные ресурсы во встраиваемых системах: дескриптор канала DMA, захваченный таймер, выделенный буфер устройства. Каждый такой объект уникален физически — в системе ровно один такой канал, — и копирование его «образа» в программе создало бы ложное впечатление двух независимых ресурсов там, где ресурс один.
Ещё одна важная область — синхронизация. Защищённые объекты и задачи (которые мы изучим в разделе о параллелизме) по своей природе limited: мьютекс или семафор нельзя осмысленно скопировать, и язык это запрещает на уровне типа. Это не случайность, а единая линия дизайна: всё, что владеет уникальной сущностью — ресурсом, идентичностью, состоянием синхронизации, — выражается ограниченным типом, и компилятор стоит на страже, не давая случайно размножить неразмножимое.
Эволюция идеи владения в индустрии
Любопытно проследить, как мысль, заложенная в limited, постепенно стала центральной в современном системном программировании. Десятилетиями мейнстрим жил с «копируемым по умолчанию» миром C и ранних C++, где висячие ссылки и двойное освобождение памяти были обыденностью именно потому, что язык охотно копировал указатели на разделяемые ресурсы. Перелом наступил с move-семантикой C++11: появились типы вроде std::unique_ptr, которые нельзя копировать — только перемещать, передавая владение. Rust возвёл эту идею в основу языка: владение и заимствование проверяются компилятором, а тип без свойства Copy ведёт себя ровно как limited-тип Ada. Получается, что индустрия через тридцать лет независимо пришла к выводу, который авторы Ada сделали сразу: для ресурсо-владеющих сущностей запрет копирования — не ограничение, а защита. Знание limited-типов Ada даёт правильную интуицию и для современного C++, и для Rust.
Контролируемые типы: limited плюс автоматическая финализация
Ограниченные типы особенно мощны в связке с контролируемыми типами (производными от Ada.Finalization.Limited_Controlled). Такой тип получает от языка три перехватываемых момента жизни: инициализацию при создании, финализацию при выходе из области и (для неограниченной версии) корректировку при присваивании. Объединив limited и контролируемость, мы получаем тип, который автоматически захватывает ресурс при создании и гарантированно освобождает его при уничтожении — даже если область покидается из-за исключения. Это и есть строгий аналог RAII: владелец файла, блокировки или соединения сам приведёт ресурс в порядок, без единого явного вызова «закрыть» в клиентском коде.
Сила сочетания в том, что оно делает утечки ресурсов структурно невозможными в типичном коде. Раз освобождение привязано к финализации объекта, а финализация срабатывает при любом выходе из области, забыть освободить ресурс попросту негде. Запрет копирования (от limited) при этом исключает двусмысленность «у кого ответственность за ресурс»: владелец один, копий нет, момент уничтожения определён. Именно так в Ada строят надёжные обёртки над системными ресурсами — и этот образец мышления, где время жизни ресурса совпадает со временем жизни владеющего им объекта, остаётся одной из самых ценных идей, которые язык прививает инженеру.
Подведём черту под местом ограниченных типов в общей картине. Лесенка закрытости Ada — публичный тип, приватный, ограниченный приватный — это, по сути, шкала обещаний по умолчанию, которые язык даёт о значениях типа. Каждая ступень вниз отнимает одну «бесплатную» возможность ради контроля: приватность убирает доступ к структуре, limited убирает копирование. Инженер выбирает ступень осознанно, исходя из природы сущности: данные-значения (точка, дата, деньги) копируются свободно и остаются обычными или приватными; сущности с уникальной идентичностью или владением ресурсом (файл, мьютекс, дескриптор) делаются ограниченными. Этот выбор — не формальность, а проектное утверждение о том, что значит «иметь два таких объекта». Точно выражая семантику владения в типе, вы заранее исключаете целые сценарии злоупотребления, и компилятор становится вашим союзником в их предотвращении — что и есть суть программирования на языке, спроектированном для надёжности.
Частые ошибки
- Пытаться присвоить limited-объект.
B := A;для limited-типа — ошибка компиляции. Передавайте по ссылке (in out) или используйте явную процедуру копирования. - Возвращать limited-тип через обычный
return Expr;с временной копией. Используйте расширенный return (return X : T do ... end return;) — он строит результат на месте. - Ожидать встроенного
=. Для limited-типа сравнение на равенство не определено; если оно осмысленно, объявите собственный оператор"="с нужной логикой. - Делать limited «на всякий случай». Если тип — просто данные (точка, дата, деньги), копирование естественно и полезно; limited нужен именно для ресурсо-владеющих или принципиально некопируемых сущностей.
Итоги
limited privateзапрещает встроенные присваивание и сравнение на равенство.- Он защищает типы, владеющие уникальными ресурсами (файлы, сокеты, мьютексы), от опасного копирования.
- Инициализация идёт «на месте»: значения по умолчанию, ограниченные агрегаты, расширенный
return(build-in-place). - Осмысленное копирование при необходимости оформляют явной процедурой — операция становится контролируемой.
- Запрет статический (нулевая цена в рантайме); идея предвосхитила move-семантику C++ и владение Rust.