Copyright (C) А.Гавва V-0.4w май 2004

14. Контролируемые типы (controlled types)

14.1 Общие сведения

Для того чтобы обеспечить сохранение абстракции, при автоматическом распределении и освобождении системных ресурсов, Ada95 предусматривает контролируемые типы. Контролируемыми типами называют типы, производные от типа Controlled или от лимитированного типа Limited_Controlled. Оба этих типа описаны в стандартном пакете Ada.Finalization:


package Ada.Finalization is
pragma Preelaborate(Finalization);

    type Controlled is abstract tagged null record;

    procedure Initialize (Object : in out Controlled);
    procedure Adjust (Object : in out Controlled);
    procedure Finalize (Object : in out Controlled);


    type Limited_Controlled is abstract tagged limited null record;

    procedure Initialize (Object : in out Limited_Controlled);
    procedure Finalize (Object : in out Limited_Controlled);


private
    . . . -- стандартом не определено

end Ada.Finalization;

Основная идея использования объектов контролируемых и лимитированных контролируемых типов заключана в том, что вызов управляющих операций Initialize, Adjust и Finalize осуществляется автоматически:

Необходимо заметить, что объекты типа Controlled или Limited_Controlled не возможно использовать непосредственно, поскольку оба типа являются абстрактными. Вместо этого, для использования контролируемых объектов, необходимо описать тип, который будет производным от типа Controlled или от типа Limited_Controlled.

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

Чтобы более наглядно продемонстрировать идею использования контролируемых типов, рассмотрим следующее:


declare
    A:  T;    -- создание объекта A и вызов Initialize(A)
    B:  T;    -- создание объекта B и вызов Initialize(B)
begin
    . . .
    A := B;   -- вызов Finalize(A), копирование значения B в A и вызов Adjust(A)
    . . .
end;   -- вызов Finalize(A) и Finalize(B)

В данном случае, предполагается, что тип T является производным от типа Controlled. Отличие использования типов, производных от типа Limited_Controlled, заключается только в отсутствии операции Adjust.

При использовании объектов контролируемых типов, следует обратить внимание на то, что в случаях когда при описании объекта контролируемого типа указывается значение инициализации:


. . .

C:  T := значение_инициализации;  -- вызов Initialize(C) не выполняется!!!

. . .

то автоматический вызов операции Initialize выполняться не будет.

14.2 Управление динамическими объектами

Одним из широко распространенных случев использования объектов контролируемых типов являестя решение проблемы "утечки" памяти при работе с динамическими данными.

Предположим, что у нас есть пакет Lists который содержит описание связанного списка List, и спецификация этого пакета выглядит следующим образом:


package Lists is

    type List is private;

    Underflow : exception;

    procedure Insert_At_Head(Item : in out List; Value : in Integer);
    procedure Remove_From_Head(Item : in out List; Value : out Integer);

    procedure Insert_At_Tail(Item : in out List; Value : in Integer);
    procedure Remove_From_Tail(Item : in out List; Value : out Integer);

    function Full(Item : List) return Boolean;
    function Empty(Item : List) return Boolean;

private
    type List is ... -- полное описание типа
    . . .

end Lists;

Заметим, что тип связанного списка List, который описан в этом пакете, не является контролируемым типом. Теперь, рассмотрим следующий пример, использующий описание пакета Lists:


with Lists;        use Lists;

procedure Memory_Leaks_Demo is

    A, B   : List;
    Result : Integer;

    procedure Loose_Memory is
        C : List;
    begin
        Insert_At_Head(C, 1);
        Insert_At_Head(C, 2);
    end Loose_Memory; -- при попадании в эту точку, 
                      -- C выходит за пределы области видимости
                      -- и теряется память ассоциируемая с этим узлом

begin
    Insert_At_Head(A, 1);
    Insert_At_Head(A, 2);

    Insert_At_Head(B, 3);
    Insert_At_Head(B, 4);

    B := A;
    -- B и A указывают на один и тот же список
    -- все узлы "старого" списка B - теряются

    Remove_From_Tail(A, Result);
    -- изменяет как список A, так и список B

end Memory_Leaks_Demo;

В данном примере наглядно демонстрируются проблемы "утечки" памяти при работе с объектами связанного списка List:

Рассмотрим вариант модификации пакета Lists в котором тип связанного списка List является контролируемым. Спецификация пакета будет иметь следующий вид:


with Ada.Finalization;

package Lists is

    type List is private;

    Underflow : exception;

    procedure Insert_At_Head(Item : in out List; Value : in Integer);
    procedure Remove_From_Head(Item : in out List; Value : out Integer);

    procedure Insert_At_Tail(Item : in out List; Value : in Integer);
    procedure Remove_From_Tail(Item : in out List; Value : out Integer);

    function Full(Item :List) return Boolean;
    function Empty(Item:List) return Boolean;

private

    -- обычные описания для списка

    type Node;
    type Ptr is access Node;

    type Node is
        record
            Value : Integer;
            Next  : Ptr;
        end record;


    -- только "голова" списка - "специальная"

    type List is new Ada.Finalization.Controlled with
        record
            Head : Ptr;
        end record;


    -- Initialize не нужна (указатели автоматически устанавливаются в null)

    -- procedure Initialize(Item : in out List);
    procedure Adjust(Item : in out List);
    procedure Finalize(Item : in out List);

end Lists;

Примечательным фактом является описание подпрограмм Adjust и Finalize в приватной части спецификации пакета. Это предотвращает от непосредственной возможности их неуместного вызова клиентами. Тело пакета Lists (с подробными комментариями) будет иметь вид:


with Unchecked_Deallocation;

package body Lists is

    -- подпрограмма освобождения памяти занимаемой узлом
    procedure Free is new Unchecked_Deallocation(Node, Ptr);

    ------------------------------------------------------------
    -- реализация остальных внутренностей (Insert_At_Head, Remove_From_Head...)

        . . .

    --------------------------------------------------------------
    -- дан указатель на список, подпрограмма будет размещать
    -- в памяти новый, идентичный первому, список
    function Copy_List(Item : Ptr) return Ptr is
    begin
        if Item = null then
            return null;
        else
            return new Node'(Item.Value, Copy_List(Item.Next));
        end if;
    end Copy_List;

    ------------------------------------------------------------
    -- при присваивании B := A, B будет только переписано содержимым A.
    -- для связанного списка это подразумевает, что оба, A и B,
    -- указывают на один и тот же объект.
    -- теперь, необходимо сделать физическую копию узлов, на которые указывает B,
    -- и переставить указатель начала списка на начало копии списка, который мы
    -- только что сделали
    procedure Adjust(Item : in out List) is
    begin
            Item.Head := Copy_List(Item.Head);
    end Adjust;

    ------------------------------------------------------------
    -- освободить всю память, занимаемую узлами списка,
    -- при разрушении списка
    procedure Finalize(Item : in out List) is
        Upto : Ptr := Item.Head;
        Temp : Ptr;
    begin
        while Upto /= null loop
            Temp := Upto;
            Upto := Upto.Next;
            Free(Temp);
        end loop;

        Item.Head := null;
    end Finalize;

end Lists;

Ниже представлен пример программы которая использует модифицированную версию пакета Lists (где тип List является контролируемым).


with Lists;        use Lists;

procedure Controlled_Demo is

    A : List;                -- автоматический вызов Initialize(A);
    B : List;                -- автоматический вызов Initialize(B);

begin
    Insert_At_Head(A, 1);
    Insert_At_Head(A, 2);

    Insert_At_Head(B, 3);
    Insert_At_Head(B, 4);
        ------------------------------------------
        --
        --    A --> | 2 |-->| 1 |--> null
        --    B --> | 4 |-->| 3 |--> null
        --
        ------------------------------------------

    B := A;
        ------------------------------------------
        --
        --    Finalize(B);
        --    освобождение узлов B, до перезаписи
        --
        --    A --> | 2 |-->| 1 |--> null
        --    B --> null                                
        --
        --    копирование A в B
        --    теперь они _оба_ указывают на один и тот же список
        --
        --    A --> | 2 |-->| 1 |--> null
        --    B ----^                                
        --
        --    Adjust(B);
        --    B копирует список, на который он в текущий момент
        --    указывает, и после этого указывает на новый список
        --
        --    A --> | 2 |-->| 1 |--> null
        --    B --> | 2 |-->| 1 |--> null
        --
        ------------------------------------------

end Controlled_Demo;
        ------------------------------------------
        --
        --    Finalize(A), Finalize(B).
        --    освобождение памяти ассоциируемой с A и B
        --
        --    A --> null
        --    B --> null
        --
        ------------------------------------------

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

14.3 Счетчик использования

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

Для большей ясности, представим себе случай, когда тип данных описывает пользователя, получающего доступ к базе данных. Объектов-пользователей базы данных может быть множество. При этом, необходимо протоколировать действия каждого пользователя, осуществляемые при работе с базой данных, в едином файле протокола.

Подобная ситуация подразумевает, что при создании хотя бы одного объекта указанного типа данных, файл протокола состояний должен быть открыт. Далее, при последующем создании объектов этого типа, открытие новых файлов для протоколирования состояний не выполняется. При удалении не используемых объектов, файл протокола должен быть закрыт только тогда, когда удалены все объекты указанного типа данных. Таким образом, нам необходимо осуществлять подсчет количества существующих объектов определенного типа данных, которые совместно используют ресурс системы.

Для решения таких задач удобно использовать контролируемые типы. Рассмотрим следующий пример спецификации пакета:


with  Ada.Text_IO;
with  Ada.Finalization;     use   Ada.Finalization;

package Log is

    type Item   is private;
    
    procedure Put_To_Log (Self: in out Item; S: in String);

private

    type Item   is new  Limited_Controlled with
        record

            . . .   -- описание полей расширения

        end record;

    procedure Initialize (Self: in out Item);
    procedure Finalize (Self: in out Item);

    The_Log_File: Ada.Text_IO.File_Type;
    The_Counter:  Natural := 0;

end Log;

Здесь тип Item описывает тип объектов при обработке которых используется общий файл протокола The_Log_File. Для вывода информации о состоянии объекта типа Item в файл протокола The_Log_File используется процедура Put_To_Log. Для подсчета количества существующих в текущий момент объектов типа Item используется переменная The_Counter.

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


package body Log is

    procedure Initialize (Self: in out Item) is
    begin
        The_Counter := The_Counter + 1;

        if  The_Counter = 1   then
            Ada.Text_IO.Open (File => The_Log_File,
                              Mode => Ada.Text_IO.Append_File,
                              Name => "log.txt");
        end if;
    end Initialize;

    procedure Finalize (Self: in out Item) is
    begin
        if  The_Counter = 1   then
            Ada.Text_IO.Close (The_Log_File);
        end if;

        The_Counter := The_Counter - 1;
    end Finalize;

    procedure Put_To_Log (Self: in out Item; S: in String) is
    begin

        . . .   -- вывод необходимых данных в файл The_Log_File

    end Put_To_Log;

end Log;

Как видно из примера, открытие файла протокола The_Log_File, при создании первого объекта типа Item, и инкремент количества существующих в текущий момент объектов типа Item в переменной The_Counter выполняется автоматически вызываемой процедурой Initialize. Декремент количества существующих в текущий момент объектов типа Item в переменной The_Counter и закрытие файла протокола The_Log_File выполняется автоматически вызываемой процедурой Finalize.

Следует заметить, что при рассмотрении данного примера мы не заостряли внимание на структуре типа Item и реализации процедуры Put_To_Log, которая выводит информацию о состоянии объекта в файл протокола, поскольку в данном случае это не имеет принципиального значения.

14.4 Блокировка ресурса

Каноническим примером использования инициализации (Initialize) и очистки (Finalize) совместно со ссылочным дискриминантом может служить следующий пример организации монопольного доступа к общему ресурсу:


type  Handle(Resource: access Some_Thing) is
        new Finalization.Limited_Controlled with null record;

procedure Initialize (H: in out Handle) is
begin
    Lock(H.Resource);
end Initialize;

procedure Finalize (H: in out Handle) is
begin
    Unlock(H.Resource);
end Finalize;

    . . .

procedure P (T: access Some_Thing) is
    H:  Handle(T);
begin
    . . . -- монопольная обработка T
end P;

В данном случае, суть идеи заключается в том, что описание H внутри процедуры P приводит к автоматическому вызову операции Initialize которая, в свою очередь, вызывает операцию блокировки ресурса Lock, после чего, внутри процедуры P, ресурс используется монопольно. Операция очистки Finalize, которая вызывает операцию освобождения ресурса Unlock, будет гарантированно вызвана, вне зависимости от того как произошло завершение работы процедуры P (нормальным образом, в результате исключения или в результате абортирования обработки). Подобный прием удобен для гарантированной организации работы парных операций, подобных операциям открытия и закрытия файлов.

14.5 Отладка контролируемых типов
       Некоторые рекомендации

Сложность отладки и проверки контролируемых типов заключается в том, что процедуры Initialize, Finalize и Adjust вызываются автоматически, без какого-либо явного указания в программе. В таком случае, можно поместить в тела соответствующих процедур Initialize, Finalize и Adjust инструкции которые отображают диагностические сообщения, например, следующим образом:


procedure Initialize (Self: in out Controlled_Type) is
begin
    . . .   -- код инициализации

    Ada.Text_IO.Put_Line("Initialize called for Controlled_Type");
end Initialize;

procedure Finalize (Self: in out Controlled_Type) is
begin
    . . .   -- код очистки

    Ada.Text_IO.Put_Line("Finalize called for Controlled_Type");
end Finalize;

procedure Adjust (Self: in out Controlled_Type) is
begin
    . . .   -- код подгонки

    Ada.Text_IO.Put_Line("Adjust called for Controlled_Type");
end Adjust;

Не смотря на простоту данного подхода, он является достаточно эффективным способом проверки корректности выполняемых действий.

Перечислим также некоторые рекомендации которые помогут избежать некоторых ошибок при написании процедур Initialize, Finalize и Adjust:


Copyright (C) А.Гавва V-0.4w май 2004