Безопасное создание объектов

Эта глава раскрывает некоторые аспекты управления объектами. Под объектами здесь мы понимаем как мелкие объекты в виде простых констант и переменных элементарного типа, например Integer, так и большие объекты из области ООП.

Язык Ада предоставляет развитые и гибкие инструменты в этой области. Эти инструменты, главным образом, не являются обязательными, но хороший программист использует их по‐возможности, а хороший менеджер настаивает на их использовании по‐возможности.

Переменные и константы

Как мы уже видели, мы можем определить переменные и константы, написав:

Top : Integer;  --  переменная

Max : constant Integer := 100;  --  константа

соответственно. Top — это переменная и мы можем присваивать ей новые значения, в то время как Max — это константа и ее значение не может быть изменено. Заметьте, что в определении константы задавать начальное значение необходимо. Переменным тоже можно задать начальное значение, но это не обязательно.

Преимущество констант в том, что их нельзя нечаянно изменить. Эта конструкция не только удобный «предохранитель». Она также помогает каждому, кто читает программу, сразу обозначая статус объекта. Важная деталь тут в том, что значение не обязательно должно быть статическим, т. е. известным в момент компиляции. В качестве примера приведем код вычисления процентных ставок:

procedure Nfv_2000 (X: Float) is
   Factor: constant Float := 1.0 + X/100.0;
begin
   return 1000.0 * Factor**2 + 500.0 * Factor - 2000.00;
end Nfv_2000;

Каждый вызов функции Nfv_2000 получает новое значение X и, следовательно, новое значение Factor. Но Factor не изменяется в течении одного вызова. Хотя этот пример тривиальный и легко видеть, что Factor не изменяется, необходимо выработать привычку использовать constant везде, где это возможно.

Параметры подпрограммы это еще один пример концепции переменных и констант.

Параметры бывают трех видов: in, in out и out. Если вид не указан в тексте, то, по умолчанию, используется in. В версиях языка вплоть до Ада 2005 включительно функции могли иметь параметры только вида in. Это ограничение имело методологический характер, чтобы склонить программиста не писать функции, имеющие побочные эффекты. На практике, однако, это ограничение на самом деле не работает. Создать функцию, имеющую побочные эффекты, легко, достаточно использовать ссылочный параметр или присваивание нелокальной переменной. Более того, в случае, когда побочный эффект необходим, программист был лишен возможности обозначить это явно, использовав подходящий вид параметра. Учитывая вышесказанное, стандарт Ада 2012, наконец, разрешил функциям иметь параметры всех трех видов.

Параметр вида in это константа, получающая значение аргумента вызова подпрограммы. Таким образом, X, из примера с Nfv_2000, имеет вид in и поэтому является константой, т. е. мы не можем присвоить ей значение и у нас есть гарантия, что ее значение не меняется. Соответствующий аргумент может быть любым выражением требуемого типа.

Параметры вида in out и out являются переменными. Аргумент вызова также должен быть переменной. Различие между этими видами касается начального значения. Параметр вида in out принимает начальное значение аргумента, в то время как параметр вида out не имеет начального значения (либо оно задается начальным значением, определенным самим типом параметра, например null для ссылочных типов).

Примеры использования всех трех видов параметров мы встречали в определении процедур Push и Pop в главе «Безопасная Архитектура»:

procedure Push (S: in out Stack; X: in Float);

procedure Pop (S: in out Stack; X: out Float);

Правила, касающиеся аргументов подпрограмм, гарантируют, что свойство «константности» не нарушается. Мы не можем передать константы, такие как Factor, при вызове Pop, поскольку соответствующий параметр имеет вид out. В противном случае это дало бы возможность Pop поменять значение Factor.

Различие между константами и переменными также затрагивает ссылочные типы и объекты. Так, если мы имеем:

type Int_Ptr is access all Integer;

K: aliased Integer;

KP: Int_Ptr := K'Access;

CKP: constant Int_Ptr := K'Access;

то значение KP можно изменить, а значение CKP — нет. Хотя мы не можем заставить CKP ссылаться на другой объект, мы можем поменять значение K:

CKP.all := 47;  -- установить значение K равным 47

С другой стороны:

type Const_Int_Ptr is access constant Integer;

J: aliased Integer;

JP: Const_Int_Ptr := J'Access;

CJP: constant Const_Int_Ptr := J'Access;

где мы используем constant в описании типа. Это значит, мы не можем поменять значение объекта J, используя ссылки JP и CJP. Переменная JP может ссылаться на различные объекты, в то время, как CJP ссылается только на J.

Вид только‐для‐чтения и для чтения‐записи

Иногда необходимо разрешить клиенту читать переменную без возможности изменять ее. Другими словами, нужно предоставить вид только‐для‐чтения для данной переменной. Это можно сделать при помощи отложенной константы и ссылочного типа:

package P is
   type Const_Int_Ptr is access constant Integer;
   The_Ptr: constant Const_Int_Ptr;  -- отложенная константа
private
   The_Variable: aliased Integer;
   The_Ptr: constant Const_Int_Ptr := The_Variable'Access;
   ...
end P;

Клиент может читать значение The_Variable, используя The_Ptr, написав

K := The_Ptr.all;  -- косвенное чтение The_Variable

Но, поскольку ссылочный тип описан, как access constant значение объекта не может быть изменено

The_Ptr.all := K;  -- ошибка, так нельзя изменить The_Variable

В то же время, любая подпрограмма, объявленная в пакете P, имеет непосредственный доступ к The_Variable и может изменять ее значение. Этот способ особенно полезен в случает таблиц, когда таблица вычисляется динамически, но клиент не должен иметь возможности менять ее.

Используя возможности стандарта Ада 2005, можно избежать введения ссылочного типа

package P is
   The_Ptr: constant access constant Integer;
private
   The_Variable: aliased Integer;
   The_Ptr: constant access constant Integer :=
          The_Variable'Access;
   ...
end P;

Ключевое слово constant встречается дважды в объявлении The_Ptr. Первое означает, что The_Ptr это константа. Второе — что нельзя изменить объект, на который ссылается The_Ptr.

Функция‐конструктор

В таких языках, как C++, Java и C есть специальный синтаксис для функций, создающих новые объекты. Такие функции называются конструкторами, их имена совпадают с именем типа. Введение аналогичного механизма в Аде отклонили, чтобы не усложнять язык. Конструкторы в других языках имеют дополнительную семантику (например, определяют, как вызвать конструктор родительского типа, в какой момент вызывать конструктор относительно инициализации по умолчанию и т.д.). Ада предлагает несколько конструкций, которые можно использовать вместо конструкторов, например, использование дискриминантов для параметризации инициализации, использование контролируемых типов с предоставляемой пользователем процедурой Initialize (к этому мы вернемся позже), обыкновенные функции, возвращающие значение целевого типа, поэтому необходимости в дополнительных средствах нет.

Лимитируемые типы

Типы, которые мы встречали до сих пор (Integer, Float, Date, Circle и др.), имеют различные операции. Некоторые из них предопределены, к ним относятся сравнение на равенство. Другие, такие как Area у типа Circle, определены пользователем. Операция присваивания также существует у всех перечисленных выше типов.

В некоторых случаях иметь операцию присваивания нежелательно. Этому может быть две главные причины

  • тип может представлять некоторый ресурс, например права доступа, копирование которого нарушает политику безопасности
  • тип может быт реализован с использованием ссылок и копирование затронет только ссылку, а не все данные.

Мы можем предотвратить присваивание, объявив тип limited. Для иллюстрации второй причины рассмотрим стек, реализованный в виде односвязного списка:

package Linked_Stacks is
   type Stack is limited private;
   procedure Clear(S: out Stack);
   procedure Push(S: in out Stack; X: in Float);
   procedure Pop(S: in out Stack; X: out Float);
private
   type Cell is record
      Next: access Cell;
      Value: Float;
   end record;

   type Stack is access all Cell;
end Linked_Stacks;

Тело пакета может быть следующим:

package body Linked_Stacks is

   procedure Clear(S: out Stack) is
   begin
      S := null;
   end Clear;

   procedure Push(S: in out Stack; X: in Float) is
   begin
      S := new Cell'(S, X);
   end Push;

   procedure Pop(S: in out Stack; X: out Float) is
   begin
      X := S.Value;
      S := Stack (S.Next);
   end Pop;

end Linked_Stacks;

Объявление стека как limited private запрещает операцию присваивания.

This_One, That_One: Stack;

This_One := That_One;  -- неверно, тип Stack лимитированный

Если бы такое присваивание сработало, это привело бы к тому, что This_One указывал бы на тот же список, что и That_One. Вызов Pop с This_One просто сдвинет его по списку That_One вниз. Проблемы такого характера имеют общепринятое название — алиасинг, т. е. существование более чем одного способа сослаться на один и тот же объект. Зачастую это чревато плохими последствиями.

Объявление стека в данном примере работает успешно, благодаря тому, что он автоматически специализируется значением null, обозначающим пустой стек. Однако, бывают случаи, когда необходимо создать объект, инициализировав его конкретным значением (например, если мы объявляем константу). Мы не можем сделать это обычным способом:

type T is limited ...

...

X: constant T := Y;  --  ошибка, нельзя скопировать переменную

поскольку это повлечет копирование, что запрещено для лимитируемого типа.

Можно воспользоваться двумя конструкциями — агрегатами и функциями. Сначала рассмотрим агрегаты. Пусть наш тип представляет некий ключ, содержащий дату выпуска и некоторый внутренний код:

type Key is limited record
   Issued: Date;
   Code: Integer;
end record;

Поскольку тип лимитируемый, ключ нельзя скопировать (сейчас его содержимое видно, но мы это исправим позже). Но мы можем написать:

K: Key := (Today, 27);

так как, в этом случае, нет копирования целого ключа, вместо этого отдельные компоненты получают свои значения. Другими словами K строится по месту нахождения.

Более реалистично было бы объявить тип приватным. Тогда мы бы не имели возможности использовать агрегат, поскольку компоненты не были бы видимы. В этом случае мы бы использовали функцию, как конструктор:

package Key_Stuff is
   type Key is limited private;
   function Make_Key(...) return Key;
   ...
private
   type Key is limited record
      Issued: Date;
      Code: Integer;
   end record;
end Key_Stuff;

package body Key_Stuff is

   function Make_Key(...) return Key is
   begin
      return New_Key: Key do
         New_Key.Issued := Today;
         New_Key.Code := ...;
      end return;
   end Make_Key;
   ...
end Key_Stuff;

Клиент теперь может написать

My_Key: Key := Make_Key (...);  -- тут нет копирования

где параметры Make_Key используются для вычисления ключа.

Давайте обратим внимание на функцию Make_Key. Она содержит расширенную инструкцию возврата (return), которая начинается с объявления возвращаемого объекта New_Key. Когда результирующий тип функции лимитируемый, возвращаемый объект, на самом деле, строится сразу по месту своего нахождения (в нашем случае в переменной My_Key). Это выполняется аналогично тому, как заполняются компоненты при использовании агрегата. Поэтому копирования не выполняется.

В итоге в Аде мы имеем механизм инициализации клиентских объектов без использования копирования. Использование лимитируемых типов дает создателю ресурсов, таких как ключи, мощный механизм контроля их использования.

Контролируемые типы

Еще более мощным механизмом управлением объектами являются контролируемые типы. С их помощью можно написать код, исполняемый когда:

  1. объект создается;
  2. объект прекращает существование;
  3. объект копируется, если он не лимитируемого типа.

В основе этого механизма лежат типы Controlled и Limited_Controlled, объявленные в пакете Ada.Finalization:

package Ada.Finalization is

   type Controlled is abstract tagged private;
   procedure Initialize(Object: in out Controlled) is null;
   procedure Adjust(Object: in out Controlled) is null;
   procedure Finalize(Object: in out Controlled) is null;

   type Limited_Controlled is abstract tagged private;
   procedure Initialize(Object: in out Limited_Controlled)       is null;
   procedure Finalize(Object: in out Limited_Controlled)
      is null;
private
   ...
end Ada.Finalization;

Синтаксис is null введен в Ада 2005 и упрощает определение поведения по умолчанию.

Основная идея (для нелимитируемых типов) состоит в том, что пользователь наследует свой тип от Controlled и переопределяет процедуры Initialize, Adjust и Finalize. Эти процедуры вызываются, когда объект создается, копируется и уничтожается, соответственно. Отметим, что вызов этих подпрограмм вставляется автоматически компилятором и программисту не нужно явно их вызывать. Аналогично обстоит дело с лимитируемыми типами. Различие в том, что используется Limited_Controlled тип, у которого отсутствует подпрограмма Adjust, поскольку копирование запрещено. Эти подпрограммы используются, чтобы реализовать сложную инициализацию, осуществлять глубокое копирование связных структур данных, освобождение памяти по окончании жизни объекта и тому подобной деятельности, специфичной для данного типа.

В качестве примера, снова рассмотрим стек, реализованный в виде связного списка, но предоставим возможность копирования. Напишем:

package Linked_Stacks is
   type Stack is limited private;

   procedure Clear(S: out Stack);
   procedure Push(S: in out Stack; X: in Float);
   procedure Pop(S: in out Stack; X: out Float);
private

   type Cell is record
      Next: access Cell;
      Value: Float;
   end record;

   type Stack is new Controlled with record
      Header: access Cell;
   end record;

   overriding procedure Adjust(S: in out Stack);
end Linked_Stacks;

Тип Stack теперь приватный. Полное объявление типа — это теговый тип, порожденный от типа Controlled и имеющий компонент Header, аналогичный предыдущему объявлению стека. Это просто обертка. Клиент же не видит, что наш тип теговый и контролируемый. Чтобы присваивание работало корректно, мы переопределяем процедуру Adjust. Обратите внимание, что мы используем индикатор overriding, что заставляет компилятор проверить правильность параметров. Тело пакета может быть следующим:

package body Linked_Stacks is

   procedure Clear(S: out Stack) is
   begin
      S := (Controlled with Header => null);
   end Clear;

   procedure Push(S: in out Stack; X: in Float) is
   begin
      S.Header := new Cell'(S.Header, X);
   end Push;

   procedure Pop(S: in out Stack; X: out Float) is
   begin
      X := S.Header.Value;
      S.Header := S.Header.Next;
   end Pop;

   function Clone(L: access Cell) return access Cell is
   begin
      if L = null then
         return null;
      else
         return new Cell'(Clone(L.Next), L.Value);
      end if;
   end Clone;

   procedure Adjust (S: in out Stack) is
   begin
      S.Header := Clone (S.Header);
   end Adjust;
end Linked_Stacks;

Теперь присваивание будет работать как надо. Допустим, мы напишем:

This_One, That_One: Stack;

...

This_One := That_One;  --  автоматический вызов Adjust

Сперва выполняется побитовое копирование That_One в This_One, затем для This_One выполняется вызов Adjust, где вызывается рекурсивная функция Clone, которая и выполняет фактическое копирование. Часто такой процесс называют глубоким копированием. В результате This_One и That_One содержат одинаковые элементы, но их внутренние структуры никак не пересекаются.

Интересным моментом, также, может быть то, как в процедуре Clear устанавливается параметр S. Эта конструкция называется расширенным агрегатом. Первая часть агрегата — имя родительского типа, а часть после слова with предоставляет значения дополнительным компонентам, если такие имеются. Процедуры Push и Pop — тривиальны.

Читатель может спросить, что произойдет с занимаемой памятью после вызова процедуры Pop или Clear. Мы обсудим это в следующей главе, касающейся вопросов управления памятью.

Следует отметить, что процедуры Initialize и Finalize не переопределены и наследуются пустые процедуры от типа Controlled. Поэтому ничего дополнительного не выполняется в момент объявления стека. Это нам подходит, потому что компонента Header получает значение null по умолчанию, что нам и требуется. Аналогично, никаких действий не происходит, когда стек уничтожается, например при выходе из процедуры. Тут снова встает вопрос об освобождении памяти, к которому мы вернемся в следующей главе.