Ограниченные приватные типы (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.
Проверьте себя
1. Что именно запрещает ограниченный приватный тип (limited private) по сравнению с обычным приватным?
AОбъявление переменных этого типа
BВстроенное присваивание (:=) и сравнение на равенство (=)
CПередачу объекта в подпрограммы
DЧтение полей внутри тела пакета
2. Как современная Ada возвращает limited-объект из функции без копирования?
AЧерез обычный return с временной переменной
BЧерез расширенный return (return X : T do ... end return), строящий объект на месте
Climited-объект вернуть из функции в принципе нельзя
DЧерез глобальную переменную