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 осуществляется автоматически:
- Initialize - "инициализация", вызывается сразу после создания объекта.
- Finalize - "очистка", вызывается непосредственно перед разрушением объекта (включая переопределение содержимого объекта).
- Adjust - осуществляет "подгонку" содержимого объекта после выполнения присваивания.
Необходимо заметить, что объекты типа 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:
Первой инструкцией подпрограмм Finalize и Adjust должна быть проверка if, которая проверяет, что объект не nil. Следует остерегаться любого декларативного кода, который может быть выполнен до проверки на nil.
Подпрограммы Finalize и Adjust должны быть симметричны и инверсны по отношению друг к другу.
Если контролируемый тип является производным от контролируемого родителя, то процедура Initialize производного типа всегда должна вызывать реализацию Initialize родителя перед выполнением инициализации необходимой для части расширения.
Если контролируемый тип является производным от контролируемого родителя, то Finalize и Adjust производного типа должны всегда вызывать реализацию Finalize и Adjust родителя после выполнения действий, необходимых для части расширения.
При тестировании подпрограммы Finalize, необходимо проверить, что значение объекта после очистки действительно будет nil.
Для агрегата или вызова функции, реализация конкретного компилятора может создавать, а может и не создавать отдельный анонимный объект. Следовательно, подпрограммы Finalize и Adjust должны поддерживать создание временных анонимных объектов (следует остерегаться любых ограничений на число существующих объектов).
Следует помнить, что при программировании контролируемых типов, любое присваивание, описание константы или динамическое размещение которое использует инициализационный агрегат, в результате, может привести к вызову Finalize и/или Adjust. В частности, не следует выполнять подобных операций при реализации процедур Finalize и Adjust (это может привести к бесконечно рекурсивным вызовам).
Copyright (C) А.Гавва V-0.4w май 2004