Ада-95. Компилятор GNAT

         

Преобразование численных типов


Поскольку тип Float и тип Integer - различные типы, то Ада не допускает смешивания величин этих типов в одном выражении. Однако, встречаются случаи, когда нам необходимо комбинировать значения этих типов. В таких ситуациях нам необходимо производить преобразование значений одного численного типа в значения другого численного типа.

Ада позволяет явно указывать необходимость преобразования значения типа Float

в значение типа Integer, и наоборот. Для выполнения такого преобразования используется синтаксис подобный синтаксису вызова функции:

X : Integer := 4; Y : Float;

Y := Float(X);

. . .

X := Integer(Y);

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

Следует заметить, что при преобразовании вещественного значения Float в целое значение Integer

Ада использует традиционные для математики правила округления, то есть:

Значение Float

Округленное значение Integer



1.5 2
1.3 1
-1.5 -2
-1.3 -1



Прерывания


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

Хотя обработчик прерываний может быть защищенной процедурой или входом задачи, ассоциация с входом задачи, в настоящее время, считается устаревшим средством языка.

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

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

В последнем случае, такие события, вне Ада-системы, могут носить название сигналов.



Примеры целочисленных описаний


Ниже приводятся примеры различных целочисленных описаний Ады.

-- описания целочисленных статических переменных

Count : Integer; X, Y, Z : Integer; Amount : Integer := 0;

-- описания целочисленных констант (иначе - именованных чисел)

Unity : constant Integer := 1; Speed_Of_Light : constant := 300_000; -- тип Universal_Integer

A_Month : Integer range 1..12;

-- описания целочисленных типов и подтипов -- ( см. разделы "Подтипы" и "Производные типы" )

subtype Months is Integer range 1..12; -- огранниченный тип Integer

-- подтипы - совместимы с их базовым типом (здесь - Integer) -- например, переменная типа Month может быть "смешана" с переменными -- типа Integer

type File_Id is new Integer; -- новый целочисленный тип, производный -- от типа Integer

type Result_Range is new Integer range 1..20_000; -- производный тип с объявлением ограничения

type Other_Result_Range is range 1..100_000; -- тип производный от Root_Integer

-- при этом, компилятор будет выбирать подходящий размер целочисленного значения -- для удовлетворения требований задаваемого диапазона



Примеры организации взаимодействия с C


Простым примером импорта C-функции может служить пример использования функции read системы UNIX в процедуре, написанной на Аде:

procedure Read( File_descriptor : in Integer; Buffer : in out String; No_Bytes : in Integer; No_Read : out Integer) is

function Read( File_descriptor : Integer; Buffer : System.Address; No_Bytes : Integer) return Integer;

pragma Import(C, read, "read");

begin

No_Read := Read(File_descriptor, Buffer(Buffer'First)'Address, No_Bytes); end Read;

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

Примером такой функции может служить C-функция printf.

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

Однако, Ада позволяет взаимодействовать с функциями, имеющими переменное число параметров, но при этом тип параметров однороден.

Для взаимодействие с такими функциями можно использовать типы неограниченных (unconstrained) массивов.

Предположим, что существует функция, написанная на C, описание которой имеет следующий вид:

void something(*int[]);

Мы можем использовать эту функцию в Аде следующим образом:

type Vector is array(Natural range <>) of Integer;

procedure Something(Item : Vector) is

function C_Something(Address : System.Address); pragma Import(C, C_Something, "something");

begin

if Item'Length = 0 then

C_Something(System.Null_Address); else

C_Something(Item(Item'First)'Address); end if; end Something;

Рассмотрим более сложный пример, который демонстрирует использование C-функции execv системы UNIX, описанную в C следующим образом:

int execv(const char *path, char *const argv[]);

В данном случае, дополнительную сложность вызывает необходимость трансляции Ада-строк в массивы символов C-стиля.

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





---------------------------------------------------------- type String_Ptr is access all String; type String_Array is array(Natural range <>) of String_Ptr;
function execv( Path : String; Arg_List : String_Array) return Interfaces.C.Int;
--------------------------------------------------------- -- execv заменяет текущий процесс на новый. -- Список аргументов передается как параметры командной -- строки для вызываемой программы. -- -- Для вызова этой подпрограммы можно: -- -- Option2 : aliased String := "-b"; -- Option3 : aliased String := "-c"; -- Option4 : String := "Cxy"; -- Result : Interfaces.C.Int; -- ... -- Result := execv(Path => "some_program", -- -- построение массива указателей на строки... -- argv => String_Array'(new String'("some_program"), -- new String'("-a"), -- Option2'Unchecked_Access, -- Option3'Unchecked_Access, -- new String'('-' & Option4)); -- -- Допустимо использовать любую комбинацию -- динамически размещаемых строк и 'Unchecked_Access -- к aliased переменным. -- Однако, необходимо отметить, что нельзя выполнить -- "some_String"'Access, поскольку Ада требует имя, -- а не значение, для отрибута 'Access
------------------------------------------------------------

Теперь, реализация может быть выполнена следующим образом:

function execv( Path : String; argv : String_Array ) return Interfaces.C.Int is
Package C renames Interfaces.C; Package Strings renames Interfaces.C.Strings;
C_Path : constant Strings.Chars_Ptr(1..Path'Length + 1) := Strings.New_String(Path);
type Char_Star_Array is array(1..argv'Length + 1) of
Strings.Char_Array_Ptr;
C_Argv : Char_Star_Array; Index : Integer; Result : C.int;
------------------------------------------------------------ function C_Execv( Path : Strings.Char_Ptr; C_Arg_List : Strings.Char_Ptr) return C.int; pragma Import(C, C_Execv, "execv"); ------------------------------------------------------------
begin
-- установка массива указателей на строки
Index := 0; for I in argv'Range loop
Index := Index + 1; C_Argv(Index) := Strings.New_String(argv(I).all)); end loop;
-- добавление C-значения null в конец массива адресов
C_Argv(C_Argv'Last) := Strings.Null_Ptr;
-- передача адресов первых элементов каждого параметра, -- как это ожидает C
Result := C_Execv( C_Path(1)'Address, C_Argv(1)'Address));
-- освобождение памяти, поскольку часто это не выполняется
for I in argv'Range loop
Strings.Free(argv(I)); end loop;
Strings.Free(C_Path);
return Result; end execv;

Примечательно, что передается адрес первого элемента массива, а не адрес самого массива.
Массивы, описываемые как неограниченные, зачастую содержат вектор с дополнительной информацией, которая включает верхнюю и нижнюю границу массива.
Copyright (C) А.Гавва V-0.4w май 2004

Примитивные и не примитивные операции над тэговыми типами Наследование операций


Концепция примитивной операции типа имеет важное значение в Аде.

В общем случае, примитивная операция над типом T - это или предопределенный знак операции (например, знаки операций для выполнения действий над целыми числами типа Integer), или операция (подпрограмма или знак операции) которая описывается в том же самом пакете в котором описывается тип T

вслед за описанием типа T, и принимает параметр типа T

или возвращает значение типа T (в случае функции).

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

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

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

Описание тэгового типа в подпрограмме, с последующим описанием операций над этим тэговым типом не подразумевает того, что такие операции будут примитивными.

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

Для уточнения сказанного рассмотрим следующий пример:

package Simple_Objects is

type Object_1 is tagged

record

Field_1 : Integer; end record;

-- примитивные операции типа Object_1 procedure Method_1 (Self: in out Object_1);

type Object_2 is new Object_1 with

record

Field_2 : Integer; end record;

-- примитивные операции типа Object_2 procedure Method_2 (Self: in out Object_2); procedure Method_2 (Self: in out Object_1); -- НЕДОПУСТИМО!!! -- должна быть примитивной операцией для Object_1, -- но недопустима поскольку следует за описанием типа Object_2 -- который является производным от типа Object_1

end Simple_Objects;

<


В подобных случаях говорят, что описание типа Object_1

становится "замороженным" при обнаружении описания типа Object_2, производного от типа Object_1.

Подобное "замораживание" осуществляется также в случаях когда обнаруживается описание какого-либо объекта (переменной) типа Object_1.

Как только описание типа "заморожено", описание примитивных операций этого типа становится невозможным.

Следует также обратить внимание на то, как выполняется наследование примитивных операций, - подобных подпрограмме Method_1 для типа Object_1, в показанном выше примере, - для производного типа.

Подразумевается, что такие операции идентичным образом описываются неявно сразу за описанием производного типа.

Причем, тип парамета, где тип параметра соответствует типу предка, заменяется на производный тип.

Таким образом, для приведенного выше примера, в случае типа Object_2, выполняется неявное описание операции Method_1, наследуемой от типа-предка Object_1.

Такое неявное описание будет иметь следующий вид:

. . . procedure Method_1 (Self: in out Object_2); . . .

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

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

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

Следовательно, результат работы таких подпрограмм, в случае производного типа, будет не корректным (точнее - не полным).

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



В качестве примера, рассмотрим следующее описание:

package Simple_Objects is

type Object_1 is tagged

record

Field_1 : Integer; end record;

-- примитивные операции типа Object_1 procedure Method_1 (Self: in out Object_1); . . .

package Constructors is -- внутренний пакет содержащий не наследуемые -- операции типа Object_1

function Create (Field_1_Value: in Integer) return Object_1; . . .

end Constructors; . . .

end Simple_Objects;

Здесь, функция Create, которая возвращает значение типа Object_1, расположена во внутреннем пакете Constructors.

В результате такого описания, функция Create

(а также другие подпрограммы для типа Object_1, расположенные во внутреннем пакете Constructors) не будет наследоваться потомками типа Object_1

(типами, производными от типа Object_1).

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

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

Действительно, для получения таких сведений необходимо изучить исходные тексты с описаниями всех типов-предков требуемого производного типа.

Осуществление этого может быть достаточно трудоемким процессом для сложной иерархии типов.

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


Принципы подавления исключений


Как правило, существует два источника которые выполняют возбуждение исключений при обнаружении некоторых ошибочных условий.

Один из них - это механизмы аппаратной проверки, которые зависят от конкретно используемого оборудования.

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

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

Таким средством Ады является директива компилятора Supress (подавление проверок).

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

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

Следует заметить, что многогранность директивы Supress

сильно зависит от реализации конкретного компилятора, и различные реализации компиляторов свободны в предоставлении (или игнорировании) любых свойств этой директивы.

Исключение Constraint_Error имеет несколько подавляемых проверок:

pragma Suppress (Access_Check); pragma Suppress (Discriminant_Check); pragma Suppress (Idex_Check); pragma Suppress (Length_Check); pragma Suppress (Range_Check); pragma Suppress (Division_Check); pragma Suppress (Owerflow_Check);

Исключение Program_Error имеет только одну подавляемую проверку:

pragma Suppress (Elaboration_Check);

Исключение Storage_Error также имеет только одну подавляемую проверку:

pragma Suppress (Storage_Check);



Принудительное завершение abort


Ада позволяет принудительно завершать выполнения объекта задачи.

Это может быть выполнено с помощью инструкции прекращения, которая может иметь следующий вид:

abort Some_Task_Name;

Здесь Some_Task_Name - это имя какого-либо объекта задачи.

Считается, что принудительно прекращенная задача находится в "ненормальном" (abnormal) состоянии и не может взаимодействовать с другими задачами.

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

Это подразумевает, что прекращается выполнение любых инструкций, расположенных в теле задачи, за исключением тех, которые вызывают операции, отложенные до принудительного прекращения (abort-deffered operations).

Следует заметить, что использование принудительного прекращения выполнения задачи является "аварийным" действием, и должно применяться только в тех случаях, когда принудительное прекращение выполнения задачи действительно необходимо (например, когда задача "зависла").

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



Приоритеты


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

pragma Interrupt_Priority ( expression );

Отсутствие выражения expression воспринимается как установка максимального системного приоритета (Interrupt_Priority'Last).

Пока обрабатываются операции этого защищенного объекта, прерывания с равным или низшим приоритетом будут заблокированы.

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

Благодаря этому прерывания становятся не блокируемыми.

Любой обработчик прерывания обрабатывается с приоритетом своего защищенного объекта, который может быть выше чем приоритет прерывания, если этот же обработчик защищенного объекта обрабатывает более одного вида прерывания.

В дополнение к этому, для динамического изменения приоритера, может быть использована процедура Set_Priority, расположенная в пакете Ada.Dynamic_Priorities.

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



Приоритеты задач


Каждая задача Ады может обладать своим собственным приоритетом выполнения, который задается с помощью директивы компилятора Priority:

pragma Priority ( expression );

Непосредственное использование этой директивы компилятора допускается:

внутри спецификации задачи, внутри спецификации защищенного типа или объекта, в описательной части тела подпрограммы

Значение результата выражения expression, используемого для непосредственного указания приоритета, должно принадлежать целочисленному типу Integer, причем при указании директивы Priority в описательной части тела подпрограммы выражение expression должно быть статическим, а его значение должно принадлежать диапазону значений подтипа Priority, который описан в пакете System.

Например:

task Some_Task is

pragma Priority (5); . . . end Some_Task;

Приоритет выполнения задачи определяет ее привилегии на обладание системными ресурсами (например, процессором).

В простейшем случае, если две задачи с разными приоритетами готовы к выполнению, то к выполнению будет допущена та задача, приоритет которой выше.

При равенстве приоритетов порядок выполнения задач не определен.

В Ada83 приоритет задачи строго фиксировался при ее описании.

Согласно стандарту Ada95, приоритет задачи может быть изменен в процессе существования задачи (кроме приоритета, указанного для подпрограммы), то есть задачи могут иметь динамически изменяемые приоритеты.

Средства динамического определения и изменения текущего приоритета задачи предоставляются предопределенным стандартным пакетом Ada.Dynamic_Priorities.

Спецификация этого пакета проста и имеет следующий вид:

with System; with Ada.Task_Identification; -- спецификация этого пакета обсуждается в 15.2.7, -- при рассмотрении вопроса идентификации задач package Ada.Dynamic_Priorities is

procedure Set_Priority (Priority : in System.Any_Priority; T : in Ada.Task_Identification.Task_ID := Ada.Task_Identification.Current_Task);

function Get_Priority (T : Ada.Task_Identification.Task_ID := Ada.Task_Identification.Current_Task) return System.Any_Priority;

end Ada.Dynamic_Priorities;

Следует заметить, что правила планирования выполнения задач на основе статических и динамических приоритетов рассматриваются в приложении D (Annex D) стандарта Ada95, в котором указываются требования для систем реального времени.



Присваивание


Целому массиву может присваиваться значение другого массива. Оба массива должны быть одного и того же типа. Если оба массива одного и того же неограниченного типа, то они должны содержать одинаковое количество элементов.

declare

My_Name : String(1..10) := "Dale "; Your_Name : String(1..10) := "Russell "; Her_Name : String(21..30) := "Liz "; His_Name : String(1..5) := "Tim "; begin

Your_Name := My_Name; -- это корректно, поскольку в обоих случаях Your_Name := Her_Name; -- оба массива имеют одинаковое количество -- элементов His_Name := Your_Name; -- это приведет к возбуждению исключения: -- хотя обе переменные одного и того же типа, -- но они имеют различную длину (число элементов) end;



Приватные дочерние модули (private child units)


Дочерние модули могут быть приватными.

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

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

В спецификацию приватных дочерних пакетов разрешается включать приватную часть спецификации их пакетов-родителей, поскольку такие дочерние модули - приватны.

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

private package Stacks.Statistics is

procedure Increment_Push_Count;

end Stacks.Statistics;

Процедура Stacks.Statistics.Increment_Push_Count могла бы быть вызвана внутри реализации пакета Stacks. Такая процедура не будет доступна ни одному внешнему, по отношению к этой иерархии модулей, клиенту.

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



Приватные типы (private types)


Рассмотрим пример пакета который управляет счетами в бухгалтерской книге.

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

- изъятие средств (Withdraw)

- размещение средств (Deposit)

- создание счета (Create)

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

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

package Accounts is

type Account is private; -- описание будет представлено позже

procedure Withdraw(An_Account : in out Account; Amount : in Money);

procedure Deposit( An_Account : in out Account; Amount : in Money); function Create( Initial_Balance : Money) return Account; function Balance( An_Account : in Account) return Integer;

private -- эта часть спецификации пакета -- содержит полные описания type Account is

record

Account_No : Positive; Balance : Integer; end record; end Accounts;

В результате такого описания, тип Account будет приватным. Следут заметить, что Ада разрешает использовать следующие предопределенные операции над объектами приватного типа вне этого пакета:

- присваивание

- проверка на равенство (не равенство)

- проверки принадлежности ("in", "not in")

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

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

Таким образом, приватность типа сохраняется только вне пакета.

В данном примере необходимо обратить внимание на то, что спецификация пакета разделена на две части.


Все что находится до зарезервированного слова private - это общедоступная часть описаний, которая будет "видна" всем пользователям пакета. Все что находится после зарезервированного слова private - это приватная часть описаний, которая будет "видна" только внутри пакета (и в его дочерних модулях; см. "Дочерние модули").

Может показаться противоречивым размещение приватных описаний в спецификации пакета.

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

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

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

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

Например:

with Accounts; use Accounts;

procedure Demo_Accounts is

Home_Account : Account; Mortgage : Account; This_Account : Account;

begin

Mortgage := Accounts.Create(Initial_Balance => 500.00); Withdraw(Home_Account, 50);

. . .

This_Account := Mortgage; -- присваивание приватного типа - разрешено

-- сравнение приватных типов if This_Account = Home_Account then

. . .

end Demo_Accounts;


Привязка объекта к фиксированному адресу памяти


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

Простым примером подобной ситуации может быть то, что операционная система MS-DOS хранит значение времени в фиксированных адресах памяти 46E и 46C (шестнадцатеричные значения).

Более точная спецификация этих значений следующая:

046E - 046F  -  время дня в часах
046C - 046D  -  число отсчетов таймера с начала текущего часа

(один отсчет таймера равен 5/91 секунды)

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

Для осуществления этого, можно привязать переменную Time_Hight типа Integer

к фиксированному адресу 16#046E# следующим образом:

Time_Hight_Address : constant Address := To_Address (16#046E#);

type Time is range 0 .. 65365; for Time'Size use 16;

Time_Hight : Time; for Time_Hight'Address use Time_Hight_Address;

Следует заметить, что здесь, тип Time является беззнаковым 16-битным целым. Величина адреса 16#046E# должна иметь тип Address, который описывается в пакете System. Стандартная функция To_Address, которая выполняет преобразование целочисленного значения в значение адреса, описывается в пакете System.Storage_Elements.



Проблема предпочтительного управления


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

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

Кроме того, состояние управляющих элементов может изменяться с момента вызова входа (защищенного модуля или задачи) до момента начала обработки обращения к входу.

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

Однако в тех ситуациях, когда для построения логики обслуживания какого-либо сервиса, необходимо использование внутри сервера предпочтительного управления (preference control), такие ограничения, как правило, не соблюдаются.

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

После вызова со стороны клиента подобный сервер должен начать обработку запроса только в том случае, если этот запрос может быть удовлетворен немедленно.

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

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

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

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

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

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

В таком случае необходимы последовательные вызовы двух (или более) входов, но попытки запрограммировать это в Ada83, как правило, затруднительны, поскольку в этом случае также теряется атомарность операций и требуется обеспечить видимость внутреннего протокола работы сервера, что нежелательно.



Проблемы механизма рандеву


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

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

В литературе подобное взаимоотношение задач, как правило, называют "поставщик-потребитель".

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

Необходимость передачи данных между двумя задачами, которые обычно выполняются асинхронно, требует приостановки выполнения задач для осуществления "переговоров".

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

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

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

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

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

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

Кроме того, с методологической точки зрения, идеология механизма рандеву строго ориентирована на управление и находится вне новейших объектно-ориентированных подходов.

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

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



Проблемы обусловленные применением ссылочных типов


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

Рассмотрим следующий пример:

declare

type Person_Name is new String (1 .. 4); type Person_Age is Integer range 1 .. 150;

type Person is

record

Name : Person_Name; Age : Person_Age; end record;

X : Person_Ptr := new Person'("Fred", 27); Y : Person_Ptr := new Person'("Anna", 20);

begin

X := Y; -- *****

Y.all := Person'("Sue ", 34); Put(X.Name);

end;

В этом примере, основной интерес для нас представляет строка помеченная звездочками.

В ней ссылочному объекту X присваивается значение ссылочного объекта Y, а не значение объекта, на который ссылается Y.

После этого оба ссылочных объекта, - и X, и Y, - ссылаются на один и тот же объект, размещенный в области динамической памяти.

Первым интересным моментом является то, что теперь изменение объекта на который ссылается переменная Y будет неявно изменять объект на который ссылается переменная X

(такое неявное изменение часто называют "побочным эффектом").

Поэтому в результате выполнения кода этого примера будет выводиться строка "Sue ".

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

Вторым интересным моментом является то, что после выполнения присваивания ссылочному объекту X значения ссылочного объекта Y, теряется ссылка на объект, на который до присваивания ссылался ссылочный объект X.

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

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

После этого, любая попытка разместить какой-либо объект в области динамической памяти приведет к ошибке Storage_Error.



Проблемы с областью видимости при обработке исключений определяемых пользователем


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

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

Рассмотрим следующий пример:

with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is

procedure Problem_In_Scope is

Cant_Be_Seen : exception; begin raise Cant_Be_Seen; end Problem_In_Scope;

begin

Problem_In_Scope;

exception

when Cant_Be_Seen => Put("just handled an_exception"); end Demo;

Этот пример не корректен.

Проблема в том, что область видимости исключения Cant_Be_Seen

ограничивается процедурой Problem_In_Scope, которая, собственно и является источником этого исключения.

То есть, исключение Cant_Be_Seen не видимо и о нем ничего не известно за пределами процедуры Problem_In_Scope.

Поэтому, это исключение не может быть точно обработано процедурой Demo.

Решить эту проблему можно использованием опции others в обработчике исключений внешней процедуры Demo:

with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is

procedure Problem_In_Scope is

Cant_Be_Seen : exception; begin raise Cant_Be_Seen; end Problem_In_Scope;

begin

Problem_In_Scope;

exception

when others => Put("just handled some exception"); end Demo;

Другая проблема возникает тогда, когда в соответствии с правилами области видимости, исключение, описываемое в одной процедуре, перекрывает (или прячет) исключение, описываемое в другой процедуре:

with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is

Fred : exception; -- глобальное исключение

------------------------------------ procedure P1 is

begin

raise Fred; end P1;

------------------------------------ procedure P2 is

Fred : exception; -- локальное исключение

begin

P1;

exception

when Fred => Put("wow, a Fred exception"); end P2; ------------------------------------

begin

P2;

exception

when Fred => Put("just handled a Fred exception"); end Demo;

<


Выводом такой процедуры будет "just handled a Fred exception". Исключение, обрабатываемое в процедуре P2, будет локально описанным исключением. Такое поведение подобно ситуации с областью видимости обычных переменных.

Для решения этой проблемы, процедуру P2 можно переписать следующим образом:

------------------------------------ procedure P2 is

Fred : exception;

begin

P1;

exception

when Fred => -- локальное исключение Put("wow, an_exception");

when Demo.Fred => -- "более глобальное" исключение Put("handeled Demo.Fred exception"); raise; end P2;

Теперь, обработчик исключения процедуры P2

выдаст сообщение "handeled Demo.Fred exception" и, с помощью инструкции raise, осуществит передачу исключения Demo.Fred в обработчик исключения процедуры Demo, который, в свою очередь, выдаст сообщение "just handled a Fred exception".


Процедуры


Процедуры Ады подобны процедурам Паскаля и используются для реализации самых разнообразных алгоритмов.

Общий вид описания процедуры выглядит следующим образом:

procedure имя_процедуры [ (формальные_параметры) ] ;    спецификация процедуры, определяющая имя процедуры и профиль ее формальных параметров (если они есть)

Общий вид тела процедуры:

procedure имя_процедуры [ (формальные_параметры) ] is    спецификация процедуры, определяющая имя процедуры и профиль ее формальных параметров (если они есть)
      
. . .   описательная (или декларативная) часть, которая может содержать локальные описания типов, переменных, констант, подпрограмм...
      
begin     
. . .    исполняемая часть процедуры, которая описывает алгоритм работы процедуры;

обязана содержать хотя бы одну инструкцию

      
end [ имя_процедуры ];    здесь, указание имени процедуры опционально

Таким образом, описание содержит только спецификацию процедуры и определяет правила ее вызова (иначе - интерфейс), а тело содержит спецификацию и последовательность инструкций, которые выполняются при вызове процедуры.

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

procedure Demo(X: Integer; Y: Float) is

begin

null; -- пустая инструкция end Demo;

Вызов процедуры производится также как и в языке Паскаль, например:

Demo(4, 5.0);

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



Производные типы


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

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

type Child_Type is new Parent_Type;

Такое описание создает новый тип данных - Child_Type, при этом Parent_Type - это тип-предок для типа Child_Type, а тип Child_Type - это производный тип от типа Parent_Type.

В данном случае, тип Child_Type будет обладать такими же характеристиками что и его тип-предок Parent_Type: у него такой же диапазон допустимых значений как и у типа-предка, для него допустимы те же операции, которые допустимы для типа-предка (говорят, что тип Child_Type унаследовал операции от типа-предка Parent_Type).

Однако, в этом случае, производный тип Child_Type - это абсолютно самостоятельный и независимый тип данных. Он не совместим ни с каким другим типом, включая тип Parent_Type, от которого он был произведен, и другими типами, производными от типа Parent_Type. Это подразумевает, что при необходимости комбинирования значений типа Child_Type

со значениями типа Parent_Type требуется выполнять преобразование типов. В Аде, разрешается выполнять преобразование значений производного типа в значения типа-предка и наоборот.

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

Parent : Parent_Type; Child : Child_Type := Child_Type (Parent); -- конвертирует значение Parent, -- имеющее тип Parent_Type -- в значение типа Child_Type

Описание производного типа может указывать ограничение диапазона значений типа-предка, например:

type Child_Type is new Parent_Type range Lowerbound..Upperbound;

<


В этом случае диапазон значений производного типа Child_Type будет ограничен значениями нижней границы диапазона (Lowerbound) и верхней границы диапазона (Upperbound).

Механизм производства новых типов данных из уже существующих типов позволяет создавать целые семейства родственных типов, которые обычно называют классами. Так, например, тип Integer принадлежит к целому классу целочисленных типов. Класс целочисленных типов является, в свою очередь, подмножеством более обширного класса дискретных типов.

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

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

Например, класс дискретных типов предусматривает атрибут 'First, который наследуется всеми дискретными типами. Класс целочисленных типов добавляет к унаследованным от класса дискретных типов операциям знак операции арифметического сложения "+". Эти механизмы более полно рассматриваются при обсуждении тэговых типов.

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

type Employee_No is new Integer; type Account_No is new Integer range 0..999_999;

Здесь Employee_No и Account_No различные и не смешиваемые типы, которые нельзя комбинировать между собой без явного использования преобразования типов. Производные типы наследуют все операции объявленные для базового типа. Например, если была объявлена запись которая имела процедуры Push и Pop, то производный тип автоматически унаследует эти процедуры.



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

Это определяется использованием производных типов, без типа-предка.

type Name is range <некоторый_диапазон_значений>;

Например,

type Data is range 0..2_000_000;

В этом случае ответственность, за выбор подходящего размера для целого, ложится на компилятор. Таким образом, для PC, этот размер может быть 32 бита, что эквивалентно типу Long_Integer, а для рабочей станции Unix, этот размер можт остаться равным 32-битному целому, но это будет эквивалентно типу Integer. Такое предоставление компилятору возможности выбирать освобождает программиста от обязанности выбирать. Поэтому перекомпиляция программы на другой машине не требует изменения исходного текста.


Простое принятие обращений к входам


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

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

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

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

task body Anonimous_Task is

begin

accept Start; . . . end Anonimous_Task;

task body Simple_Task is

begin

. . . accept Read (Value: out Integer) do

Value := Some_Value; end Read; . . . accept Request (Low) (Item: in out Integer) do

Some_Low_Item := Item; . . . Item := Some_Low_Item; end Request;

accept Request (Middle) (Item: in out Integer) do

Some_Middle_Item := Item; . . . Item := Some_Middle_Item; end Request;

accept Request (Hight) (Item: in out Integer) do

Some_Hight_Item := Item; . . . Item := Some_Hight_Item; end Request; . . . end Simple_Task;

Тело задачи Anonimous_Task содержит единственную инструкцию принятия рандеву, которая соответствует входу Start, у которого нет параметров.

Данная инструкция принятия очень проста и не содержит никакой последовательности инструкций, выполняемой в процессе принятия рандеву.

Следует заметить, что инструкция принятия accept

может не содержать в себе последовательность инструкций, даже когда соответствующий вход имеет параметры, и наоборот, она может содержать последовательность каких-либо инструкций, если параметры у соответствующего входа отсутствуют.




Кроме того, следует заметить, что последовательность инструкций, вложенная в инструкцию принятия рандеву, может содержать инструкцию выхода return

(это подобно использованию return в теле процедуры).

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

Тело задачи Simple_Task более интересно.

Первая инструкция принятия рандеву соответствует входу Read с "out"-параметром Value.

Внутри этой инструкции принятия рандеву параметру Value присваивается значение переменной Some_Value

(предполагается, что эта и другие переменные были где-либо описаны).

Далее следуют три инструкции принятия рандеву, образующие "семейство".

Все они соответствуют описанию входа Request, в спецификации задачи Simple_Task, которая описывалась с указанием типа Level, значения которого (Low, Middle и Hight) используются в качестве индекса.


Простой вызов входа


Задача-клиент осуществляет вызов входа задачи-сервера, идентифицируя как объект задачи-сервера, так и необходимый вход задачи-сервера.

Для демонстрации описаний простых вызовов входов задачи-сервера, заданных в задаче-клиенте, рассмотрим следующий пример:

declare

. . . Simple_Task_Variable : Simple_Task; . . . Read_Value : Integer; Request_Item : Integer; . . . begin

. . . Anonimous_Task.Start; . . . Simple_Task_Variable.Read (Read_Value); Simple_Task_Variable.Request (Middle) (Request_Item); . . . end;

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

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

При непосредственной обработке рандеву происходит передача любых параметров "in" и "in out" в задачу-сервер.

Затем задача-сервер выполняет последовательность инструкций, которая расположена внутри инструкции принятия (если такая последовательность существует), а задача-клиент остается в приостановленном состоянии.

Далее, после завершения этой последовательности инструкций, происходит передача любых параметров "out" и "in out" к задаче-клиенту, и обработка рандеву завершается.

После этого обе задачи продолжают свое выполнение независимо друг от друга.

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

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

Следовательно, разные реализации Ада-систем могут использовать различные правила для определения последовательности активации множества задач, а это означает, что последовательность активации множества задач одной и той же программы может отличаться в различных реализациях Ада-системы.




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

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

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

with Ada.Text_IO;

procedure Multitasking_Demo_3 is

-- спецификация типа задачи task type Simple_Task (Message: Character; How_Many: Positive) is

entry Start; -- этот вход будет использоваться для реального -- запуска задачи на выполнение

end Simple_Task;

-- тело задачи task body Simple_Task is

begin -- для Simple_Task

accept Start; -- в этом месте, выполнение задачи будет заблокировано -- до поступления вызова входа

for Count in 1..How_Many loop

Ada.Text_IO.Put_Line("Hello from Simple_Task " & Message); delay 0.1; end loop;

end Simple_Task;

-- переменные задачного типа Simple_Task_A: Simple_Task(Message => 'A', How_Many => 5); Simple_Task_B: Simple_Task(Message => 'B', How_Many => 3);

begin -- для Multitasking_Demo_3

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

Simple_Task_B.Start; Simple_Task_A.Start;

end Multitasking_Demo_3;

Как видно из исходного текста этого примера, здесь описаны два объекта задач: Simple_Task_A и Simple_Task_B.

Каждая задача имеет вход Start, который используется для управления очередностью запуска задач.

В результате сначала запускается задача Simple_Task_B, а затем Simple_Task_A.

Рассмотрим еще один простой пример программы, в котором демонстрируется использование входов с параметрами.

procedure Demo is

X : Duration := Duration(Random(100)); Y : Duration;

task Single_Entry is

entry Handshake(Me_Wait : in Duration; You_Wait : out Duration); end task;

task body Single_Entry is

A : Duration := Duration(Random(100)); B : Duration; begin

Delay A;

accept Handshake(Me_Wait : in Duration; You_Wait : out Duration) do

B := Me_Wait; You_Wait := A; end Handshake;

Delay B; end;

begin

Delay(X); Handshake(X, Y); Delay(Y); end Demo;

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

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

В этом случае задача будет аварийно завершена с исключением Tasking_Error.


Простые циклы (loop)


Примером простейшего цикла может служить бесконечный цикл. Обычно он используется совместно с инструкцией exit, рассматриваемой позже.

loop

-- инструкции тела цикла

end loop;



Проверка типа объекта во время выполнения программы


В процессе выполнения программы можно осуществить проверку объекта на принадлежность его к какому-либо индивидуальному типу путем использования атрибута 'Tag

Чтобы продемонстрировать это, предположим, что реализация процедуры Show

описывается следующим образом:

procedure Show (Self: in Root'Class) is

begin

if Self'Tag = Root'Tag then

Ada.Text_IO.Put_Line ("Root"); elsif Self'Tag = Child_1'Tag then

Ada.Text_IO.Put_Line ("Child_1"); elsif Self'Tag = Clild_2'Tag then

Ada.Text_IO.Put_Line ("Clild_2"); elsif Self'Tag = Grand_Child_2_1'Tag then

Ada.Text_IO.Put_Line ("Grand_Child_2_1"); else

Ada.Text_IO.Put_Line ("Unknown type"); end if;

end Show;

Кроме того, в процессе выполнения программы, возможно осуществление проверки принадлежности (или не принадлежности) типа объекта к какому-либо классу.

Для выполнения таких проверок используются операции проверки диапазона "in" и "not in":

. . . if Some_Instance in Child_1'Class then

. . . end if; . . .

В данном примере выполняется проверка принадлежности переменной Some_Instance

к иерархии типов, корнем которой будет тип Child_1, причем, предполагается, что переменная Some_Instance

является переменной надклассового или тэгового типа.

Следует также обратить внимание на то, что для выполнения этой проверки используется "Some_Instance in Child_1'Class", а не "Some_Instance in Child_1".



Проверки на равенство и на неравенство


Проверки на равенство и на неравенство доступны почти для всех типов Ады. Два массива считаются равными если каждый элемент первого массива равен соответствующему элементу второго массива.

if Array1 = Array2 then....



Пулы динамической памяти


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

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

Такой пул могут использовать различные объекты ссылочного типа.

Пользователь может описать свой собственный тип пула динамической памяти, используя абстрактный тип Root_Storage_Pool описанный в пакете System.Storage_Pools, и после этого ассоциировать его с объектом ссылочного типа используя атрибут 'Storage_Pool.

Например:

Pool_Object : Some_Storage_Pool_Type;

type T is access <какой-нибудь_тип>; for T'Storage_Pool use Pool_Object;

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

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

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

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

Другими примерами случаев использования пулов динамической памяти определяемых пользователем могут служить: необходимость распределения динамической памяти для объектов размер которых значительно меньше чем минимальный размер памяти, который распределяется для размещения объекта в общем пуле динамической памяти программы (иначе - в пуле по-умолчанию); требование приложения реального времени обеспечить фиксированное время выполнения операций распределения/освобождения пространства динамической памяти.



Пустая инструкция


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

null;

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



"Пустые" записи (null record) и расширения


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

Как правило, подобный тип используется для последующего построения целой иерархии типов как базовый (или корневой) тип.

Это может быть достигнуто описанием "пустой" записи при описании типа:

type Root is tagged

record

null; end record;

Для таких случаев, Ада обеспечивает специальный синтаксис описания "пустых" записей:

type Root is tagged null record;

Описание операций над таким типом традиционно, и может быть выполнено в спецификации пакета:

procedure Do_Something(Item : in out Root);

Также, бывают случаи, когда описание нового производного типа не нуждается в расширении (нет добавления новых полей), но при этом добавляются новые подпрограммы или переопределяются старые.

Вид описания такого расширения следующий:

type Child is new Root with null record;

procedure Child_Method (Item : in out Child);

В результате такого описания получается новый тип Child, производный от типа Root, который не будет иметь дополнительных компонентов, но имеет дополнительный метод - Child_Method.



Распространение исключений


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

Этот процесс поиска называется распространением исключений.

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

Если обработчик исключения не найден, то исключение распространяется дальше (еще на уровень выше). Это продолжается до тех пор пока не будет найден обработчик возникшего исключения или не будет достигнут уровень выполнения текущей задачи (наивысший уровень).

Если в случае достижения уровня выполнения текущей задачи (то есть наивысшего уровня) обработчик исключения не будет найден, то текущая задача будет аварийно завершена (другими словами, абортирована).

Если выполняется только одна задача, то библиотека времени выполнения Ады выполняет обработку возникшего исключения и аварийно завершает выполнение всей программы (другими словами, абортирует выполнение программы).

procedure Exception_Demo is

--------------------------------- procedure Level_2 is

-- здесь нет обработчика исключений begin raise Constraint_Error; end Level_2;

--------------------------------- procedure Level_1 is

begin

Level_2; exception

when Constraint_Error => Put("exception caught in Level_1"); end Level_1;

begin

Level_1;

exception

when Constraint_Error => Put("exception caught in Exception_Demo"); end Exception_Demo;

После запуска этой программы будет выдано только сообщение "exception caught in Level_1".

Следовательно, обработанное исключение не распространяется дальше.

Модифицируем процедуру Level_1 поместив инструкцию raise в ее обработчик исключения.

Наш предыдущий пример будет иметь следующий вид:

procedure Exception_Demo is

--------------------------------- procedure Level_2 is

-- здесь нет обработчика исключений begin raise Constraint_Error; end Level_2;

--------------------------------- procedure Level_1 is

begin

Level_2; exception

when Constraint_Error => Put("exception caught in Level_1"); raise; -- регенерация текущего исключения; -- дает возможность другим подпрограммам -- произвести обработку возникшего -- исключения end Level_1;

begin

Level_1;

exception

when Constraint_Error => Put("exception caught in Exception_Demo"); end Exception_Demo;

<


Теперь, инструкция raise, помещенная в обработчик исключения, вызывает распространение исключения Constraint_Error на один уровень выше, то есть, к вызвавшей подпрограмме.

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

Инструкцию raise очень удобно использовать в секции others

обработчика исключений:

. . .

exception

. . .

when others => raise; -- регенерация текущего исключения; -- дает возможность другим подпрограммам -- произвести обработку возникшего -- исключения end;

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


Расширение существующего пакета


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

Например, в пакете Stacks может понадобиться дополнительный сервис просмотра Peek.

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

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

package Stacks is

type Stack is private; procedure Push(Onto : in out Stack; Item : Integer); procedure Pop(From : in out Stack; Item : out Integer); function Full(Item : Stack) return Boolean; function Empty(Item : Stack) return Boolean;

private

-- скрытая реализация стека ... -- точка A

end Stacks;

package Stacks.More_Stuff is

function Peek(Item : Stack) return Integer;

end Stacks.More_Stuff;

По правилам Ады, спецификация родительского пакета обеспечивает для дочернего пакета такую же самую область видимости, что и для своего тела.

Следовательно, дочерний пакет видит всю приватную часть спецификации родительского пакета.

В показанном выше примере, пакет Stacks.More_Stuff является дочерним пакетом для пакета Stacks.

Значит, дочерний пакет Stacks.More_Stuff "видит" все описания пакета Stacks, вплоть до точки A.

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

Примечание:

Согласно правил именования файлов, принятым в системе компилятора GNAT, спецификация и тело пакета Stacks должны быть помещены в файлы:

stacks.ads и stacks.adb

соответственно, а спецификация и тело дочернего пакета Stacks.More_Stuff

- в файлы:

stacks-more_stuff.ads и stacks-more_stuff.adb




Клиенту, которому необходимо использовать функцию Peek, просто необходимо включить дочерний пакет в инструкцию спецификатора совместности контекста with:

with Stacks.More_Stuff;

procedure Demo is

X : Stacks.Stack;

begin

Stacks.Push(X, 5); if Stacks.More_Stuff.Peek = 5 then

. . .

end Demo;

Следует заметить, что включение дочернего пакета в инструкцию спецификатора совместности контекста with автоматически подразумевает включение в инструкцию with

всех пакетов-родителей.

Однако, инструкция спецификатора использования контекста use

таким образом не работает.

То есть, область видимости может быть получена пакетом только в базисе пакета.

with Stacks.More_Stuff; use Stacks; use More_Stuff;

procedure Demo is

X : Stack;

begin

Push(X, 5); if Peek(x) = 5 then

. . .

end Demo;

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

При этом, однако, сами подпрограммы не могут иметь дочерние модули.


Расширение существующего типа данных


Как было сказано ранее, тэговые типы, в отличие от обычных не тэговых типов, позволяют осуществлять расширение структуры данных типа предка при наследовании.

Рассмотрим следующий пример описания:

type Object_1 is tagged

record

Field_1 : Integer; end record;

В данном случае тип Object_1 содержит всего одно поле Field_1 типа Integer, и описан как тэговый тип.

Не трудно заметить, что внешний вид такого описания подобен описанию обычной записи, и отличается только наличием зарезервированного слова tagged, которое, собственно, и указывает на то, что описываемый тип является тэговым типом.

Теперь, в качестве примера, мы можем описать производные от Object_1 типы, расширяя их новыми компонентами следующим образом:

type Object_2 is new Object_1 with

record

Field_2 : Integer; end record;

type Object_3 is new Object_2 with

record

Field_3 : Integer; end record;

В данном примере, тип Object_2 является производным типом от типа Object_1, а тип Object_3 - производным типом от типа Object_2.

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

Object_1 | Object_2 | Object_3

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

Поэтому все описанные выше типы: Object_1, Object_2 и Object_3, - являются тэговыми типами, и, следовательно, обладают способностью расширения.

В результате показанных выше описаний, типы Object_1, Object_2 и Object_3

будут содержать следующие поля:

Object_1

Object_2

Object_3

Field_1 Field_1 Field_1
Field_2 Field_2
    Field_3

Примечательным фактом является то, что описание типа Object_2

не указывает явно наличие поля Field_1.

Это поле наследуется от типа Object_1.

Также, описание типа Object_3

не указывает явно наличие полей Field_1 и Field_2.

Эти поля наследуются от типа Object_2.



Разделяемые (общие) переменные


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

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

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

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

Решение этой проблемы основывается на том, что в какой-либо момент времени право на доступ к разделяемой переменной предоставляется только одной задаче.

Для обеспечения синхронизации доступа используются специальные программные (или аппаратные) средства: семафоры, критические секции, мониторы и т.д.

В стандарте Ada95 для указания разделяемых переменных используются следующие директивы компилятора:

pragma Atomic ( Local_Name ); pragma Atomic_Components ( Local_Array_Name );

pragma Volatile ( Local_Name ); pragma Volatile_Components ( Local_Array_Name );

Здесь Local_Name указывает локальное имя объекта или описание типа, а Local_Array_Name указывает локальное имя объекта-массива или описание типа-массива.

Директивы компилятора Atomic и Atomic_Components обеспечивают непрерывность операций чтения/записи для всех указанных в них объектах.

Такие объекты называют атомарными (atomic), а операции над ними выполняются только последовательно.

Директивы компилятора Volatile и Volatile_Components обеспечивают выполнение операций чтения/записи для всех указанных в них объектах непосредственно в памяти.

Примеры применения этих директив компилятора могут иметь следующий вид:

Array_Size : Positive; pragma Atomic (Array_Size); pragma Volatile (Array_Size);

Store_Array is array (1..Array_Size) of Integer; pragma Atomic_Components (Store_Array); pragma Volatile_Components (Store_Array);

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

взаимодействия задач взаимодействия Ада-программы с другими процессами управления устройствами из Ада-программ

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



Раздельная компиляция


В предыдущем примере, если будет произведено какое-нибудь изменение кода, то обе процедуры должны быть переданы компилятору (поскольку обе находятся в одном файле с исходным текстом).

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

Это несколько похоже на директиву #include, используемую в языке Си, но, в языке Ада, теперь оба файла становятся независимыми компилируемыми модулями.

В первом файле:

with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Ive_Got_A_Procedure is

X : Integer := 6; Y : Integer := 5;

procedure Display_Values(Number : Integer) is separate;

begin

Display_Values(X); Display_Values(Y); end Ive_Got_A_Procedure;

Во втором файле:

separate(Ive_Got_A_Procedure) -- примечание! нет завершайщего символа -- точки с запятой

procedure Display_Values(Number : Integer) is

begin

Put(Number); New_Line; end Display_Values;

Выделенный в самостоятельный файл (и ставший отдельно компилируемым модулем), код - идентичен тому, что было в предыдущей версии. Однако теперь, если будет изменена только внутренняя подпрограмма, то только она должна быть подвергнута перекомпиляции компилятором. Это также позволяет разделить программу на несколько частей, что может облегчить ее понимание.



Режим access


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

Заметим, что подробному обсуждению ссылочных типов Ады далее посвящена самостоятельная глава - "Ссылочные типы (указатели)".

Необходимо также обратить внимание на то, что режим передачи параметров access

был введен стандартом Ada95 и он отсутствует в стандарте Ada83.

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

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

В случае когда значение параметра равно null

генерируется исключительная ситуация Constraint_Error

(проще говоря, - ошибка).

Внутри подпрограммы, формальный параметр, использующий режим access, является константой ссылочного типа и ему нельзя присваивать новое значение, поэтому такие формальные параметры несколько подобны формальным параметрам, использующим режим "in".

Однако, поскольку параметр является значением ссылочного типа (указателем), то подпрограмма может изменить содержимое объекта на который данный параметр ссылается (указывает).

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

Любая попытка конвертирования значения такого параметра в значение именованого ссылочного типа будет проверяться на соответствие правилам области действия для ссылочных типов. При обнаружении нарушения этих правил генерируется исключительная ситуация Programm_Error.

. . .

function Demo_Access(A : access Integer) return Integer is

begin

return A.all; end Demo_Access;

. . .

type Integer_Access is access Integer;

Integer_Access_Var : Integer_Access := new Integer'(1); Aliased_Integer_Var : aliased Integer;

. . .

X : Integer := Demo_Access(Integer_Access_Var); Y : Integer := Demo_Access(Aliased_Integer_Var'Access); Z : Integer := Demo_Access(new Integer);

. . .

Режим access разрешается использовать и в процедурах, и в функциях.

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

То есть, такие функции могут обладать побочными эффектами.



Режим "in"


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

Это значит, что при входе в подпрограмму, формальный параметр инициализируется значением фактического параметра, при этом, внутри подпрограммы, он является константой и разрешает только чтение значения ассоциированного фактического параметра.

with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Demo(X : in Integer; Y : in Integer) is

begin

X := 5; -- недопустимо, in параметр доступен только по чтению Put(Y); Get(Y); -- также недопустимо end Demo;

Режим "in" разрешается использовать и в процедурах, и в функциях.



Режим "in out"


Этот режим непосредственно соответствует параметрам передаваемым по ссылке (подобно var-параметрам языка Паскаль).

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

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

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

procedure Demo(X : in out Integer; Y : in Integer) is

Z : constant Integer := X;

begin

X := Y * Z; -- это допустимо! end Demo;

Режим "in out" разрешается использовать только в процедурах.



Режим "out"


В этом режиме, при входе в подпрограмму, формальный параметр не инициализируется (!!!) значением фактического параметра.

Согласно стандарта Ada95, внутри подпрограммы, формальный параметр, использующий этот режим, может быть использован как в левой, так и в правой части инструкций присваивания (другими словами: формальный параметр доступен как для чтения, так и для записи). Согласно стандарта Ada83, внутри подпрограммы, такой формальный параметр может быть использован только в левой части инструкций присваивания (другими словами: доступен только для записи).

При этом, после выхода из подпрограммы, значение фактического параметра заменяется на значение формального параметра.

procedure Demo(X : out Integer; Y : in Integer) is

-- при входе в подпрограмму X не инициализирован!!!

begin

X := Y; end Demo;

Режим "out" разрешается использовать только в процедурах.



Режимы передачи параметров


Стандарт Ada83 предусматривал три режима передачи параметров для подпрограмм:

"in"

"in out"

"out"

Стандарт Ada95 добавил еще один режим передачи параметров:

access

Все эти режимы не имеют непосредственных аналогов в других языках программирования.

Необходимо также отметить следующее:

по-умолчанию, для передачи параметров подпрограммы, всегда устанавливается режим - "in" !!!

Для "in" / "out" скалярных значений используется механизм передачи параметров по копированию-"in" (copy-in), по копированию-"out" (copy-out).

Стандарт специфицирует, что любые другие типы могут быть переданы по copy-in/copy-out, или по ссылке.

Ada95 указывает, что лимитированные приватные типы (limited private types), которые рассматриваются позднее, передаются по ссылке, для предотвращения проблем нарушения приватности.



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


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

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

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

Объектов-пользователей базы данных может быть множество.

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

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

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

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

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

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

Рассмотрим следующий пример спецификации пакета:

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, которая выводит информацию о состоянии объекта в файл протокола, поскольку в данном случае это не имеет принципиального значения.


Селекция принятия рандеву


Предположим, что нам необходима задача-сервер, которая будет обслуживать множество задач-клиентов.

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

Вполне резонно на основе полученных ранее сведений, попытаться реализовать что-нибудь подобное следующему:

task Server_Task is

entry Service_1 [ параметры для Service_1 ] ; entry Service_2 [ параметры для Service_2 ] ; . . . entry Service_N [ параметры для Service_N ] ;

entry Stop; end task;

task body Server_Task is

. . . begin

loop

accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1;

accept Service_2 [ параметры для Service_2 ] do

. . . end Service_2;

. . .

accept Service_N [ параметры для Service_N ] do

. . . end Service_N;

accept Stop do

exit ; -- выход из цикла, и, следовательно, -- завершение задачи end Stop; end loop; end Server_Task;

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

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

Следовательно, для ситуаций, которые подобны показанной в этом примере, необходима возможность селекции (или выбора) принятия рандеву на входах задачи-сервера.

Для обеспечения селекции принятия рандеву, Ада предоставляет различные варианты инструкции отбора с ожиданием, которая задается с помощью зарезервированного слова select.

Использование инструкции отбора в теле задачи-сервера позволяет:

одновременно ожидать более одного рандеву

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




осуществлять рандеву только в случае наличия вызова рандеву

завершать выполнение задачи при отсутствии задач-клиентов, которые потенциально могут вызвать рандеву

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

task Server_Task is

entry Service_1 [ параметры для Service_1 ] ; entry Service_2 [ параметры для Service_2 ] ; . . . entry Service_N [ параметры для Service_N ] ;

entry Stop; end task;

task body Server_Task is

. . . begin

loop

select

accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1; . . . -- дополнительная последовательность инструкций, -- которая выполняется после принятия рандеву -- на входе Service_1 or

accept Service_2 [ параметры для Service_2 ] do

. . . end Service_2; or

. . . or

accept Service_N [ параметры для Service_N ] do

. . . end Service_N; or

accept Stop; exit ; -- выход из цикла, и, следовательно, -- завершение задачи end select

end loop; end Server_Task;

Как видно из исходного текста примера, инструкции принятия рандеву указываются как альтернативы выбора инструкции отбора (это несколько подобно инструкции case).

В данном случае при выполнении инструкции отбора задача-сервер циклически "опрашивает" свои входы на наличие вызова рандеву от задач-клиентов (без блокировки в состоянии ожидания вызова рандеву на каком-либо входе).

Опрос продолжается до тех пор, пока не будет обнаружен вызов рандеву на каком-либо входе, который соответствует одной из перечисленных инструкций принятия рандеву.

После обнаружения вызова выполняется соответствующая альтернатива (примечание: завершение обработки альтернативы приводит к завершению инструкции отбора).

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

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



В данном примере интересную ситуацию представляет альтернатива принятия рандеву на входе Stop.

В этом случае происходит выход из бесконечного цикла выполнения инструкции отбора, что, в свою очередь, приводит к завершению работы задачи-сервера.

Если в процессе завершения работы задачи-сервера поступит вызов рандеву на какой-либо из входов Service_1 - Service_N, то задача-клиент, вызывающая рандеву, скорее всего получит исключение Tasking_Error.

Недостаток такого подхода состоит в том, что завершение работы задачи-сервера требует явного указания вызова рандеву на входе Stop.

Чтобы избавиться от необходимости явного указания вызова рандеву на входе Stop, можно использовать инструкцию отбора, в которой указывается альтернатива завершения задачи:

task Server_Task is

entry Service_1 [ параметры для Service_1 ] ; . . . entry Service_N [ параметры для Service_N ] ; end task;

task body Server_Task is

. . . begin

loop

select

accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1; or

. . . or

accept Service_N [ параметры для Service_N ] do

. . . end Service_N; or

terminate; -- завершение работы задачи end select

end loop; end Server_Task;

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

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

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

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

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

Использование такого варианта инструкции отбора может иметь следующий вид:



loop

select

accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1; or

. . . or

accept Service_N [ параметры для Service_N ] do

. . . end Service_N; else

. . . -- последовательность инструкций, которая выполняется -- если нет ни одного вызова рандеву end select

end loop;

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

Еще одной разновидностью инструкции отбора служит инструкция отбора, использующая альтернативу таймаута (или задержки).

Такая инструкция отбора может иметь следующий вид:

loop

select

accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1; or

. . . or

accept Service_N [ параметры для Service_N ] do

. . . end Service_N; or

delay 1.0; . . . -- последовательность инструкций таймаута, -- которая выполняется в случае -- когда нет ни одного вызова рандеву -- в течение одной секунды end select

end loop;

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

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

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

Таким образом, может быть указан относительный (как в примере выше) или абсолютный интервал времени.

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



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

При этом, будет обрабатываться та альтернатива таймаута, которая имеет наименьший интервал времени.

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

При применении инструкций отбора следует учитывать, что использование альтернативы завершения работы задачи (terminate), альтернативы таймаута (delay) и раздела else

в пределах одной инструкции отбора является взаимно исключающим.

Кроме того, любой вариант инструкции отбора обязан содержать хотя бы одну альтернативу выбора, в которой указана инструкция принятия рандеву accept.

Еще одной особенностью инструкций отбора является опциональная возможность указания дополнительной проверки какого-либо условия для альтернатив принятия рандеву, завершения работы задачи и таймаута.

Для указания такой проверки используется конструкция вида: "when условие =>", - где проверяемое условие описывается с помощью логического выражения, результат вычисления которого должен иметь значение предопределенного логического типа Standard.Boolean.

Как правило, такую проверку назвают защитной или охранной (guard), а ее использование может иметь следующий вид:

declare

. . . Service_1_Counter : Integer; . . . Service_N_Counter : Integer; . . . begin

. . . loop

. . . select

when (Service_1_Counter > 0) => accept Service_1 [ параметры для Service_1 ] do

. . . end Service_1; or

. . . or

when (Service_N_Counter > 100) => accept Service_N [ параметры для Service_N ] do

. . . end Service_N; end select

end loop; . . . end;

Вычисление значения логического выражения осуществляется в начале выполнения инструкции отбора.

Если результат вычисления выражения True, то соответствующая альтернатива имеет право быть выбранной инструкцией отбора, если результат False, альтернатива выбрана не будет, причем даже в том случае, когда какая-либо задача-клиент осуществила вызов соответствующего входа и ждет обслуживания.

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

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

В противном случае альтернативу отбора можно назвать закрытой.


Селекция вызова рандеву


Инструкции отбора select

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

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

Рассмотрим простой пример инструкции отбора для выполнения условного вызова рандеву на входе Service_1

задачи-сервера Server_Task:

select

Server_Task.Service_1 [ параметры для Service_1 ] ; else

Put_Line ("Server_Task is busy!"); end select;

В этом случае, если задача-сервер Server_Task не готова к немедленному приему рандеву на входе Service_1, то вызов рандеву будет отменен и выполнится последовательность инструкций в разделе else, которая выводит сообщение "Server_Task is busy!".

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

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

loop

select

Server_Task.Service_1 [ параметры для Service_1 ] ; . . . -- опциональная последовательность инструкций, exit; -- выполняемая после рандеву else

. . . -- последовательность инструкций, -- выполняемая в случае невозможности -- осуществления рандеву end select;

end loop;

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

Пример такой инструкции для выполнения временного вызова рандеву на входе Service_1

задачи-сервера Server_Task

может иметь следующий вид:

select

Server_Task.Service_1 [ параметры для Service_1 ] ; or

delay 5.0; Put_Line ("Server_Task has been busy for 5 seconds!"); end select;

В данном случае, если в течение указанного интервала времени (здесь задан относительный интервал времени длительностью 5 секунд) задача-сервер Server_Task не приняла рандеву на входе Service_1, то вызов рандеву отменяется и выполняется последовательность инструкций, заданная альтернативой задержки, которая в этом примере выдаст сообщение "Server_Task has been busy for 5 seconds!".




Вместо относительного интервала может быть указано абсолютное значение времени:

select

Server_Task.Service_1 [ параметры для Service_1 ] ; or

delay until Christmas; Put_Line ("Server_Task has been busy for ages!"); end select;

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

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

Таким образом, ее общий вид может быть следующм:

loop

select

Server_Task.Service_1 [ параметры для Service_1 ] ; . . . -- опциональная последовательность инструкций, exit; -- выполняемая после рандеву or

delay интервал_времени

. . . -- последовательность инструкций, -- выполняемая в случае невозможности -- осуществления рандеву end select;

end loop;

Заметим, что вместо конструкции "delay интервал_времени", задающей относительный интервал, может быть использована конструкция "delay until интервал_времени", которая указывает абсолютное значение времени.

Инструкция отбора для асинхронной передачи управления (введена стандартом Ada95) позволяет задаче-клиенту продолжить выполнение какого-либо кода в то время, когда вызов рандеву ожидает обслуживания.

Пример использования такой инструкции может иметь следующий вид:

select

delay 5.0; Put_Line ("Server_Task is not serviced the Service_1 yet"); then abort

Server_Task.Service_1 [ параметры для Service_1 ] ; end select;

В данном случае начинается выполнение инструкций, расположенных между "then abort" и "end select", то есть вызов рандеву с задачей-сервером Server_Task на входе Service_1.

При этом, если истечение интервала времени из инструкции delay (или delay until), расположенной после select, произойдет раньше завершения выполнения Server_Task.Service_1, то выполнение Server_Task.Service_1 принудительно прерывается и выполняется вывод сообщения "Server_Task is not serviced the Service_1 yet".

Примечательно, что использование асинхронной передачи управления не ограничено многозадачностью, например:

select

delay 5.0; Put_Line ("So_Big_Calculation abandoned!"); then abort

So_Big_Calculation; end select;

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

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


Символьные типы Ады (Character, Wide_Character)


Ада имеет два предопределенных перечислимый типа, Character и Wide_Character, для поддержки обработки символьных значений. Согласно стандарта, любой перичислимый тип который содержит хотя бы один символьный литерал является символьным типом.

Оригинальный стандарт Ada83 описывал 7-битный тип Character. Еще до появления стандарта Ada95, это ограничение было ослаблено, но оставалось принудительным для старых компиляторов (например таких как компилятор Meridian Ada). Это создавало трудности при попытках отобразить графические символы на PC, поскольку для отображения символов с кодами большими чем ASCII-127 приходилось использовать целые числа. Такая поддержка обеспечивалась за счет специальных подпрограмм предоставляемых разработчиками соответствующего компилятора.

В настоящее время, предопределенный символьный тип Character предусматривает 256 различных символьных значений (то есть, является 8-битным), и основывается на стандарте ISO-8859-1 (Latin-1).

Некоторые символы не имеют непосредственно печатаемого значения (первые 32 символа). Такие символы используются в качестве управляющих (примером может служить символ CR - возврат каретки). Для обращения к таким символам можно использовать пакет ASCII, который является дочерним пакетом пакета Standard (благодаря этому, нет необходимости указывать пакет ASCII в спецификаторах контекста with и/или use). Например, для обращения к символу возврат каретки можно использовать: ASCII.CR. Однако, пакет ASCII содержит только первые 128 символов и считается устаревшим, и возможно, что в будущем он будет удален. Поэтому, вместо старого пакета ASCII

рекомендуется использовать пакет Ada.Characters.Latin_1, который предоставляет 256 символов. Следовательно, используя пакет Ada.Characters.Latin_1, к символу возврата каретки можно обратиться следующим образом: Ada.Characters.Latin_1.CR.

Предопределенный символьный тип Wide_Character

основывается на стандарте ISO-10646 Basic Multilingual Plane (BMP) и предусматривает 65336 различных символьных значений (использует 16 бит).



Также, Ада предоставляет пакет Ada.Characters.Handling, предлагающий набор полезных подпрограмм символьной обработки.

Система компилятора GNAT предоставляет дополнительный пакет Ada.Characters.Wide_Latin_1, который описывает символьные значения типа Wide_Character соответствующие кодировке Latin_1.

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

Standard.ASCII -- предоставляет только первые 128 символов -- (считается устаревшим)

Ada.Characters -- Ada.Characters.Latin_1 -- предоставляет 256 символов ISO-8859-1 (Latin-1) Ada.Characters.Handling -- предоставляет подпрограммы символьной обработки

Ada.Characters.Wide_Latin_1 -- дополнительный пакет из поставки -- системы компилятора GNAT

Следует заметить, что пакет ASCII считается устаревшим и его не рекомендуется использовать при написании новых программ. Вместо него необходимо использовать стандартный пакет Ada.Characters.Latin_1.

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


"Сюрпризы" переводной терминологии


Прежде чем приступить к непосредственному знакомству с Адой, есть необходимость заметить, что в англоязычной литературе, посвященной вычислительной технике в целом и программированию в частности, достаточно часто встречаются такие английские термины как operator, operation и statement. При сложившейся практике перевода такой документации на русский язык, эти термины, как правило, переводят следующим образом:

operation операция
operator   операция, оператор
statement   оператор

На первый взгляд, проблема перевода данных терминов достаточно безобидна. Однако, при таком традиционном подходе, добившись приемлемой благозвучности изложения материала, достаточно сложно избежать неоднозначностей и даже путаницы при обсуждении некоторых основополагающих вопросов. Также стоит особо отметить, что стандарт языка Ада строго различает такие понятия.

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

operation  

непосредственно обозначает понятие операции, функции или какого-либо действия, в совокупности составляющего выполнение команды (или набора команд) процессора

operator  

обозначает понятие знака или обозначения операции, функции или какого-либо действия, что подразумевает не столько само действие для выполнения операции, сколько обозначение операции в тексте программы

statement  

элемент текста программы, выражающий целостное законченное действие (или набор действий)

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

operation   операция
operator   знак операции
statement   инструкция

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



Скалярные типы данных языка Ада


К скалярным типам относятся типы данных которые определяют соответствующие упорядоченные множества значений. Эта глава описывает скалярные типы данных Ады, а также атрибуты и операции допустимые для этих типов.

Предопределенный пакет Standard содержит описания стандартных типов, таких как Integer, Float, Boolean, Character и Wide_Character, а также определяет операции, которые допускается производить над этими типами.

Следующие знаки операций допустимы для всех скалярных типов:

=, /= проверка на равенство/не равенство <, <=, >, >= меньше, меньше или равно, больше, больше или равно in, not in проверка принадлежности к диапазону

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



Смешивание позиционного и именованного сопоставления


Ада позволяет смешивать позиционное и именованное сопоставление параметров.

В этом случае должно соблюдаться следующее условие: позиционно-ассоциированные параметры должны предшествовать параметрам, которые ассоциируются по имени.

procedure Square(Result : out Integer; Number : in Integer) is

begin

Result := Number * Number; end Square;

В результате, показанная выше процедура Square может быть вызвана следующими способами:

Square(X, 4); Square(X, Number => 4); Square(Result => X, Number => 4); Square(Number => 4, Result => x);

Square(Number => 4, X); -- недопустимо, поскольку позиционно-ассоциируемый -- параметр следует за параметром, ассоциируемым -- по имени



Совмещение (overloading)


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

Хорошим примером для иллюстрации такой проблемы является процедура Insert.

В подобных случаях, для облегчения жизни программистам, Ада позволяет разным подпрограммам иметь одинаковые имена, предоставляя механизм который называется совмещением (overloading).



Совмещение подпрограмм (subprogram overloading)


Предоставляя механизм совмещения имен подпрограмм, Ада выдвигает единственное требование: подпрограммы должны быть распознаваемы (или различимы).

Две подпрограммы, имеющие одинаковые имена, будут распознаваемы если они имеют разный "профиль".

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

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

В противном случае, вы получите сообщение об ошибке, указывающее на двусмысленность обращения.

procedure Insert(Item : Integer); -- две процедуры с одинаковыми именами, procedure Insert(Item : Float); -- но имеющие разный "профиль"

Примерами совмещенных подпрограмм могут служить процедуры Put и Get из стандартного пакета Ada.Text_IO.



Совмещение знаков операций (operator overloading)


В языках подобных Паскалю знак операции "+" - совмещен.

Действительно, он может использоваться для сложения целых и вещественных чисел, и даже строк.

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

Ада разрешает программистам выполнять совмещение предопределенных знаков операций с их собственным кодом.

Следует заметить, в Аде, действие выполняемое знаком операции, реализуется путем вызова функции именем которой является знак операции заключенный в двойные кавычки.

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

В некоторых случаях, проблему двусмысленности знака операции можно преодолеть непосредственно специфицируя имя пакета (рассматривается далее).

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

Рассмотрим простой пример в котором мы хотим предусмотреть возможность сложения двух векторов:

procedure Add_Demo is

type Vector is array (Positive range <>) of Integer; A : Vector(1..5); B : Vector(1..5); C : Vector(1..5);

function "+"(Left, Right : Vector) return Vector is

Result : Vector(Left'First..Left'Last); Offset : constant Natural := Right'First - 1;

begin

if Left'Length /= Right'Length then

raise Program_Error; -- исключение, -- рассматриваются позже end if;

for I in Left'Range loop

Result(I) := Left(I) + Right(I - Offset); end loop; return Result; end "+";

begin

A := (1, 2, 3, 4, 5); B := (1, 2, 3, 4, 5); C := A + B; end Add_Demo;

В этом примере хорошо продемонстрированы многие ранее рассмотренные средства которые характерны для языка программирования Ада.