Контролируемые типы и объектно-ориентированное программирование

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

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

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

pack_box.ads:

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Finalization;
 
package Pack_Box is
    type Box is tagged private; --Объявляем свой тип как приватный
    procedure Show_Box(Obj : in Box); --Показать объект
    --Заполнение поля объекта
    procedure Set_Box(Obj : out Box; str : in Unbounded_String);
private
    --Уточняем наш тип в приватной части пакета: устанавливаем, что тип контролируемый
    type Box is new Ada.Finalization.Controlled with
        record
            My_Box : Unbounded_String;
        end record;
 
    --Перегружаем (переопределяем) наши подпрограммы родительского типа
    --Обратите внимание, они переопределяются в приватной части пакета. Можно,
    --конечно, и не в приватной, но чтобы их нельзя было вызвать напрямую, лучше
    --поместить их в приватной части:
    procedure Initialize(Obj : in out Box);
    procedure Adjust(Obj : in out Box);
    procedure Finalize(Obj: in out Box);
end Pack_Box;

[свернуть]
pack_box.adb:

with Ada.Text_IO.Unbounded_IO; use Ada.Text_IO.Unbounded_IO;
 
package body Pack_Box is
    --Заполнить единственное поле объекта
    procedure Set_Box(Obj : out Box; str : in Unbounded_String) is
    begin
        Put_Line(To_Unbounded_String("Заполняется строка My_Box объекта."));
            Obj.My_Box := str;
    end Set_Box;
 
    --Показать объект:
    procedure Show_Box(Obj : in Box) is
    begin
        Put_Line(Obj.My_Box);
    end Show_Box;
 
    --Инициализация объекта
    procedure Initialize(Obj : in out Box) is
    begin
        Put_Line(To_Unbounded_String("Инициализация объекта"));
    end Initialize;
 
    --Подгонка объекта
    procedure Adjust(Obj : in out Box) is
    begin
        Put_Line(To_Unbounded_String("Подгонка объекта"));
    end Adjust;
 
    --Уничтожение объекта
    procedure Finalize(Obj : in out Box) is
    begin
        Put_Line(To_Unbounded_String("Уничтожение объекта"));
    end Finalize;
begin
    Null;
end Pack_Box;

[свернуть]
main.adb:

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Text_IO; use Ada.Text_IO;
with Pack_Box;
 
procedure main is
    First : Pack_Box.Box; --Здесь выполняется Initialize
    Second : Pack_Box.Box; --Здесь выполняется Initialize
begin
    New_Line;
    First.Set_Box(To_Unbounded_String("Это первый объект"));
    New_Line;
 
    --В следующей строке происходит сразу несколько действий:
    --1. Уничтожается переменная Copy - Finalize
    --2. В переменную Copy копируется содержимое переменной Chest - Adjust
    Second := First;
    Second.Show_Box; --Содержимое Second равно содержимому First
 
    New_Line;
    --Меняем содержимое объекта Second:
    Second.Set_Box(To_Unbounded_String("Это второй объект"));
    New_Line;
 
    --Показать объекты:
    Second.Show_Box;
    First.Show_Box;
    New_Line;
 
    --Здесь происходит уничтожение обеих переменных (объектов)
end main;

[свернуть]

Используя контролируемые типы можно решить вопрос утечки при размещении объектов в динамической памяти. Если на пальцах, то утечка - это когда в процессе работы утрачивается доступ к какому-то участку памяти, при этом программа считает, что память занята какими-то данными, и не может использовать этот участок в своих целях. Когда таких участков становится много, то наступает самое веселье 🙂 Выполнение программы приходится прекращать (ну или чаще всего она сама это делает) и искать некорректный код.

Рассмотрим один из примеров утечки. Будем создавать объекты в динамической памяти с помощью ссылок:

pack_box.ads

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings; use Ada.Strings;
with Ada.Unchecked_Deallocation;
 
package Pack_Box is
    type Box is tagged private;
    type Ref_Box is access Box;
    type Ref_String is access String;
 
    procedure Show_Box(Obj : in Box);
    procedure Set_Box(Obj : out Box; str : in string);
    --Процедура будет получать ссылку на объект и вызывать приватную процедуру Free_Box:
    procedure Free_Box(Obj : out Ref_Box);
    procedure Free_String is new Ada.Unchecked_Deallocation(String, Ref_String);
private
    type Box is tagged
        record
            My_Box : Ref_String;
        end record;
    --Так как тип Box окончательно определяется только в приватной части пакета,
    --то подпрограмму на основе дженерика для него получается создать только
    --после уточнения типа:
    procedure Free is new Ada.Unchecked_Deallocation(Box, Ref_Box);
 
end Pack_Box;

[свернуть]
pack_box.adb

package body Pack_Box is
    --Заполнить единственное поле объекта
    procedure Set_Box(Obj : out Box; str : in String) is
    begin
        Put_Line("Заполняется строка My_Box объекта.");
        Obj.My_Box := new String'(str);
    end Set_Box;
 
    --Показать объект:
    procedure Show_Box(Obj : in Box) is
    begin
        Put_Line(Obj.My_Box.all);
    end Show_Box;
 
    procedure Free_Box(Obj : out Ref_Box) is
    begin
        --Строка в записи Box выделялась динамически, поэтому при
        --уничтожении объекта память, занимаемую этой строкой, нужно
        --освободить (вернуть программе). Если этого не сделать, то
        --в этом месте мы получим утечку памяти. Т.е. когда объект будет уничтожен,
        --строка в памяти останется
        Free_String(Obj.My_Box);
 
        Free(Obj); --Удалить объект
    end Free_Box;
 
begin
    Null;
end Pack_Box;

[свернуть]
main.adb

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Text_IO; use Ada.Text_IO;
with Pack_Box;
 
procedure main is
    Obj_A : Pack_Box.Ref_Box;
    Obj_B : Pack_Box.Ref_Box;
 
begin
    Obj_A := new Pack_Box.Box;
    Obj_B := new Pack_Box.Box;
 
    Obj_A.Set_Box("Это первый объект");
    Obj_B.Set_Box("Это второй объект");
 
    Obj_A.Show_Box;
    Obj_B.Show_Box;
 
    Obj_A := Obj_B;
    --Obj_A (ссылка) теперь ссылается туда же, куда и Obj_B, а вот доступ к
    --участку памяти, на который ссылалась переменная Obj_A до операции присваивания
    --утерян. При этом память осталась занята и не была возвращена программе.
    --Следовательно здесь мы получаем утечку.
    --Для предотвращения утечки перед предыдущей операцией нужно вставить
    --строку Pack_Box.Free_Box(Obj_A);
 
    Obj_B.Show_Box;
    Obj_A.Show_Box;
 
    Pack_Box.Free_Box(Obj_A);
    --СЛЕДУЮЩАЯ СТРОКА ПРИВЕДЁТ К ОШИБКЕ, т.к. Obj_A ссылается туда же, куда и Obj_B,
    --а для Obj_A память освобождена в предыдущей строке
    --Pack_Box.Free_Box(Obj_B);
 
end main;

[свернуть]

Из приведённого примера можно увидеть, что вроде бы в простой задаче мы получили сразу два места, в которых возможна утечка памяти. И если необходимость очистка памяти, занимаемой строкой My_Box, при уничтожении объекта (Obj_A и Obj_B) более-менее очевидна, то необходимость очистки памяти при простом присваивании одной ссылки другой (Obj_A := Obj_B) достаточно неявна и невидна.

К сведению (в целях напоминания): если нам нужно не перенаправить ссылку Obj_A на другой участок памяти (куда ссылается Obj_B), а создать точную копию объекта Obj_B, то достаточно записи Obj_A.all := Obj_B.all; Процедура Adjust сама всё сделает, как надо.

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

ИМХО, с одной стороны, это одно из преимуществ Ады: писать и сопровождать код без лишних нагромождений намного проще, да и многие ошибки по умолчанию не возникнут. С другой стороны, С и С++ имеют в своём арсенале больше инструментов для прямой работы с объектами динамической памяти. Мне, например, при работе с большим количеством однотипных объектов в динамической памяти, для быстрого доступа к ним удобно было бы создать динамический массив этих объектов и обращаться к ним по индексу. С/С++ это могут, тут указатель можно направить хоть в космос (под свою ответственность). Но, как говорится, на вкус и цвет все фломастеры разные. Можно вообще на уровень ассемблера опуститься (кстати, Ада это тоже может).

В местах, где происходит утечка, я указал, что можно сделать для её избежания. Можно, конечно отслеживать жизненные этапы каждого объекта и вручную подчищать память, как указано в примере выше, а можно просто создать переменную контролируемого типа и "обернуть" ей динамические объекты, переопределив процедуры. В следующем примере я переопределил процедуру Finalize для очистки памяти и процедуру Adjust для физического копирования элементов одного динамического списка в другой. Советую внимательно читать комментарии в коде, надеюсь, что они помогут упростить понимание материала. Рассмотрим пример (его стоит запустить и поглядывать на результат в процессе чтения кода): в программе создаётся два списка из динамических объектов, затем содержимое одного списка копируется в другой:

main.adb

with Ada.Text_IO; use Ada.Text_IO;
with Pack_Box;
 
procedure main is
    Ref_A : Pack_Box.Vector;
    Ref_B : Pack_Box.Vector;
 
begin
    Put_Line("Начало"); --Для удобства восприятия вывода информации на экран
    --Добавление элементов в первый список
    Ref_A.Vector_Add;
    Ref_A.Vector_Add;
    Ref_A.Vector_Add;
    Ref_A.Vector_Add;
    --Добавление элементов во второй список
    Ref_B.Vector_Add;
    Ref_B.Vector_Add;
    Ref_B.Vector_Add;
    Ref_B.Vector_Add;
    Ref_B.Vector_Add;
    --Вывод содержимого списков на экран
    Ref_A.Show_Vector;
    Ref_B.Show_Vector;
 
    --По умолчанию после следующей операции ссылка Ref_A должна ссылаться
    --туда же, куда и Ref_B, однако процедура Adjust была мной перегружена для
    --физического копирования элементов Ref_B. Здесь советую посмотреть реализацию
    --процедур Adjust и Finalize в пакете Pack_Box (pack_box.adb):
    Ref_A := Ref_B;
 
    --Вывод содержимого списков на экран
    Ref_A.Show_Vector;
    Ref_B.Show_Vector;
 
    Put_Line("Конец"); --Для удобства восприятия вывода информации на экран
    --Здесь уничтожаются оба списка
end main;

[свернуть]
pack_box.adb

with Ada.Text_IO; use Ada.Text_IO;
package body Pack_Box is
 
    --Финализация: уничтожение объектов
    procedure Finalize(Vect : in out Vector) is
        Head : Ref_Knot := Vect.Obj;
        Tmp : Ref_Knot;
    begin
        --Пробегаем по всем элементам списка и очищаем динамически выделенную память
        while Head /= Null loop
            Put_Line("Уничтожение объекта" & Integer'Image(Head.Object_Number));
            Tmp := Head;
            Head := Head.Next;
            Free(Tmp);
        end loop;
    end Finalize;
 
    --Переопределение процедуры Adjust (присваивание)
    procedure Adjust (Vect : in out Vector) is
        --ВАЖНО: в этом месте сначала будут выполнены действия по умолчанию:
        --1. Будет полностью уничтожен первый вектор (массив, на который ссылалась
        --переменная Ref_A): для каждого элемента вектора будет выполнена процедура Finalize.
        --2. По умолчанию Ref_A будет ссылаться туда же, куда ссылается Ref_B
        --То есть, у нас получается два элемента, ссылающихся на один участок памяти
        --ТОЛЬКО ПОСЛЕ ЭТОГО начнут отрабатывать указанные ниже операции:
        --я последовательно: 1. пробегаю по списку элементов, 2. для каждого элемента выделяю
        --место в динамической памяти, 3.одновременно копирую в это место поля текущего
        --элемента массива. Затем просто привязываю ссылки к этому участку памяти, а в
        --самом конце привязываю ссылку, переданную в качестве параметра в подпрограмму, к
        --получившемуся новому связанному списку.
        --Таким образом происходит физическое копирование динамических участков памяти.
 
        Head_Exist_Vector: Ref_Knot; --ссылка на текущий элемент существующего списка
        --ссылка на текущий элемент создаваемого в динамической памяти списка
        Head_New_Vector: Ref_Knot;
        Tmp : Ref_Knot; --временный элемент для работы
        Root_New_Vector : Ref_Knot; --Ссылка на корневой элемент создаваемого списка
        --Итак, к этому моменту содержимое старого списка уничтожено, а 
    begin
        Head_Exist_Vector := Vect.Obj; --Ссылается на начало существующего списка
        --Корневой элемент нового списка пока не создан, новый список на данном этапе
        --не существует:
        Root_New_Vector := Null;
        --Пока текущий элемент существующего списка не Null
        while Head_Exist_Vector /= Null loop
            --Выделяем в памяти место под новый элемент и заполняем его:
            Tmp := new Knot'(Object_Number => Head_Exist_Vector.Object_Number, Next => Null);
            --если текущий элемент - самый первый элемент списка
            if Root_New_Vector = Null then
                Root_New_Vector := Tmp;
                Head_New_Vector := Tmp;
            else --иначе, если текущий элемент не первый
                Head_New_Vector.Next := Tmp;
                Head_New_Vector := Head_New_Vector.Next;
            end if;
            --Переходим к следующему элементу списка
            Head_Exist_Vector := Head_Exist_Vector.Next;
        end loop;
        --Привязываемся к новому списку
        Vect.Obj := Root_New_Vector;
    end Adjust;
 
 
    --Добавление нового объекта в массив
    procedure Vector_Add(Vect : in out Vector) is
        Head : Ref_Knot; -- := Vect.Obj;
        Tmp : Ref_Knot;
    begin
        cnt := cnt + 1; --счётчик объектов
        Put_Line("Создание объекта" & Integer'Image(cnt));
        --Создать объект:
        Head := new Knot;
        Head.Object_Number := cnt;
        Head.Next := Null;
        --Привязать ссылки:
        if Vect.Obj = Null then --Если список пуст
            Vect.Obj := Head;
        else --Если в списке уже есть элементы
            Tmp := Vect.Obj;
            while Tmp.Next /= Null loop
                Tmp := Tmp.Next;
            end loop;
            Tmp.Next := Head;
        end if;
    end Vector_Add;
 
    --Показать список
    procedure Show_Vector(Vect : in Vector) is
        Head : Ref_Knot := Vect.Obj;
    begin
        while Head /= Null loop
            Put(Integer'Image(Head.Object_Number) & " -> ");
            Head := Head.Next;
        end loop;
        New_Line;
    end Show_Vector;
 
begin
    cnt := 0; --считает количество объектов, под которые выделена память
end Pack_Box;

[свернуть]
pack_box.ads

with Ada.Finalization;
with Ada.Unchecked_Deallocation;
 
package Pack_Box is
    --Ссылочный тип. Под него будет выделяться память
    type Knot;
    type Ref_Knot is access Knot;
    type Knot is
        Record
            Object_Number : Integer;
            Next : Ref_Knot;
        end Record;
    --Список. Обёртка над ссылочным типом
    type Vector is tagged;
    type Vector is new Ada.Finalization.Controlled with
        Record
            Obj : Ref_Knot;
        end Record;
    --Добавить элемент в список
    procedure Vector_Add(Vect : in out Vector);
    --Удалить элемент из списка
    procedure Show_Vector(Vect : in Vector);
    --Освобождение памяти, занятой ссылочной переменной
    procedure Free is new Ada.Unchecked_Deallocation(Knot, Ref_Knot);
 
    --Переопределение процедуры Initialize здесь не обязательно, так как
    --при создании динамического объекта он по умолчанию равен Null
    procedure Adjust(Vect : in out Vector);
    procedure Finalize(Vect : in out Vector);
 
    cnt : Integer := 0; --Счётчик созданных объектов
end Pack_Box;

[свернуть]

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

2 comments on “Контролируемые типы и объектно-ориентированное программирование

  1. Спасибо за статью!

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

    В Аде это делается через storage pools и subpools. Вот пример из ARM: http://www.ada-auth.org/standards/2xrm/html/RM-13-11-6.html

    Среди любопытных проектов на тему пулов можно посмотреть Deepend
    https://sourceforge.net/projects/deepend/

    • Спасибо за ссылки! Нашёл примерно это же на википедии. Буду разбираться.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *