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

         

Исключение Constraint_Error


Исключение Constraint_Error возбуждается в следующих случаях:

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

Рассмотрим пример:

procedure Constraint_Demo is

X : Integer range 1..20; Y : Integer;

begin

Put("enter a number "); Get(Y); X := Y; Put("thank you"); end Constraint_Demo;

Если пользователь вводит значение выходящее за диапазон значаний 1..20, то нарушается ограничение диапазона значений для X, и происходит исключение Constraint_Error.

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

При этом, строка

Put("thank you");

выполнена не будет.

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

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

procedure Constraint_Demo2 is

X : array (1..5) of Integer := (1, 2, 3, 4, 5); Y : Integer := 6;



begin

X(Y) := 37; end Constraint_Demo2;

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



Исключение Numeric_Error


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

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

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

procedure Numeric_Demo is

X : Integer; Y : Integer;

begin

X := Integer'Last; Y := X + X; -- вызывает Numeric_Error

end Numeric_Demo;



Исключение Program_Error


Исключение Program_Error возбуждается в следующих случаях:

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

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

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

procedure Program_Demo is

Z : Integer;

function Y(X : Integer) return Integer is

begin

if X < 10 then

return X; elsif X < 20 then

return X end if; end Y; -- если мы попали в эту точку, то это значит, -- что return не был выполнен

begin

Z := Y(30); end Program_Demo;



Исключение Storage_Error


Исключение Storage_Error возбуждается в следующих случаях:

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



Исключение Tasking_Error


Исключение Tasking_Error возбуждается в случаях межзадачного взаимодействия.

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



Исключения


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

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

- ошибки, которые обнаруживаются на этапе компиляции программы

- ошибки, которые обнаруживаются во время выполнения программы

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

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

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

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

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

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

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

фирмы Borland).

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



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


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

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



Элементарные сведения: описание, создание, инициализация


Предположим, что у нас есть следующие описания:

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;

Предположим также, что нам необходимо разместить экземпляр объекта типа Person

в области динамической памяти.

Для этого нам надо описать ссылочный тип, значения которого будут ссылаться на значения (объекты) типа Person и переменную этого ссылочного типа (заметим, что все ссылочные типы Ады описываются с помощью использования зарезервированного слова access):

type Person_Ptr is access Person; -- описание ссылочного типа Someone : Person_Ptr; -- переменная (объект) ссылочного типа

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

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

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

Таким образом, после приведенных выше описаний, переменная Someone

имеет значение null.

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

необходимо выполнить следующее:

Someone := new Person;

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

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

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

Someone.Name := "Fred"; Someone.Age := 33;

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

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




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

Someone.all := Person'("Fred", 33); -- вариант с позиционным агрегатом

Someone.all := Person'( Name => "Fred"; -- вариант с именованным агрегатом Age => 33 );

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

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

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

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

Someone := new Person'("Fred", 33); -- вариант с позиционным агрегатом

Someone := new Person'( Name => "Fred"; -- вариант с именованным агрегатом Age => 33 );

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

Таблица сравнения синтаксиса указателей/ссылочных типов

в языках Паскаль, Си и Ада:

Паскаль

C

Ада

Доступ к полям указываемого
объекта
a^.fieldname *a.fieldname
a->fieldname
a.fieldname
Копирование указателя b := a; b = a; b := a;
Копирование указываемого
обекта
b^ := a^; *b = *a; b.all := a.all

с комментариев. Для облегчения понимания


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

-- это комментарий --- это тоже комментарий

Концепция рандеву


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

Основополагающая идея механизма рандеву достаточно проста.

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

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

Необходимо обратить внимание на несимметричность такого механизма взаимодействия.

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

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

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

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

Этот способ взаимодействия двух задач называется рандеву.

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

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

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

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

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

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

Если реализация Ада-системы не обеспечивает соответствия этим требованиям, то очередь обслуживается в порядке поступления вызовов (FIFO - First-In-First-Out).



Конкатенация


Символ & может быть использован как знак операции конкатенации двух массивов.

declare

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

A : Vector (1..10); B : Vector (1..5) := (1, 2, 3, 4, 5); C : Vector (1..5) := (6, 7, 8, 9, 10); begin

A := B & C; Put_Line("hello" & " " & "world"); end;



Лексические соглашения


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

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



Лимитированные приватные типы (limited private types)


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

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

package Compare_Demo is

type Our_Text is private;

. . .

private

type Our_Text (Maximum_Length : Positive := 20) is

record

Length : Index := 0; Value : String(1..Maximum_Length); end record;

. . .

end Compare_Demo;

Здесь, тип Our_Text описан как приватный и представляет из себя запись.

В данной записи, поле длины Length определяет число символов которое содержит поле Value (другими словами - число символов которые имеют смысл).

Любые символы, находящиеся в позиции от Length + 1 до Maximum_Length будут нами игнорироваться при использовании этой записи.

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

В результате, он будет последовательно сравнивать значения поля Length

и значения всех остальных полей записи.

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

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

Изменим рассмотренный выше пример следующим образом:

package Compare_Demo is

type Our_Text is limited private;

. . .

function "=" (Left, Right : in Our_Text) return Boolean;

. . .

private

type Our_Text (Maximum_Length : Positive := 20) is

record

Length : Index := 0; Value : String(1..Maximum_Length); end record;

. . .

end Compare_Demo;

Теперь, тип Our_Text описан как лимитированный приватный тип.

Также, в спецификации пакета, описана функция знака операции сравнения на равенство "=".

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




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

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

возвращает значение тип которого отличается от предопределенного логического типа Boolean

(полное имя - Standard.Boolean), то совмещение знака операции неравенства "/="

необходимо описать явно.

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

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

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

. . . procedure Init (T : in out Our_Text; S : in String); . . .

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


Лимитированные записи


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

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

type Person is limited

record

Name : String(1..Max_Chs); -- строка имени Height : Height_Cm := 0; -- рост в сантиметрах Sex : Gender; -- пол end record;

Mike : Person; Corrina : Person;

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

. . . Mike := Corrina; -- ОШИБКА КОМПИЛЯЦИИ!!! -- для лимитированных записей присваивание запрещено . . .

if Corrina = Mike then -- ОШИБКА КОМПИЛЯЦИИ!!! -- для лимитированных записей сравнение запрещено Put_Line("This is strange"); end if; . . .

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



Литералы


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

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

Примеры целочисленных литералов, представляющих значение числа 2000:

2000 2_000 2E3 -- для целочисленных литералов разрешена экспоненциальная форма 2E+3

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

2#1100# -- двоичная система счисления 8#14# -- восьмеричная 10#12# -- десятичная (здесь, указана явно) 16#C# -- шестнадцатиричная 7#15# -- семиричная

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

3.14 100.0 0.0

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

'a' 'b'

Примечание:

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

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

"это строковый литерал" "a" -- это тоже строковый литерал, хотя и односимвольный

Примечание:

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



Логические операции


Если знаки логических операций "and", "or", "xor", "not" допускается использовать для индивидуальных компонентов массива, то использование знаков логических операций для такого массива также будет допустимым (например, в случае массива логических значений типа Boolean).



Локальные переменные


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

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

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



Локальные подпрограммы


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

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

К таким локальным подпрограммам можно обращаться только из подпрограммы которая их содержит.

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

begin

Put (Number); New_Line; end Display_Values;

begin

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

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

"не видна" и не может быть вызвана из любого другого места.



Массивы (array)


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

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



Массивы-константы


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

type Months is (Jan, Feb, Mar, .... , Dec); subtype Month_Days is Integer range 1..31; type Month_Length is array (Jan..Dec) of Month_Days;

Days_In_Month : constant Month_Length := (31, 28, 31, 30, ... , 31);



Механизмы наследования


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

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

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



Методы Ады: подпрограммы, операции и знаки операций


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

Знаки операций представляются следующими символами (или комбинациями символов):

"=", "/=", "<", ">", "<=", ">=", "&", "+", "-", "/", "*".

Другие знаки операций выражаются зарезервированными словами:

"and", "or", "xor", "not", "abs", "rem", "mod", - или могут состоят из нескольких зарезервированных слов: "and then", "or else".

Ада позволяет осуществлять программисту совмещение (overloading) знаков операций (в современной литературе по Си++ это часто называется как "перегрузка операторов").

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

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

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

Следует заметить, что Ада накладывает некоторые ограничения на использование совмещений: совмещения не допускаются для операций присваивания и проверки принадлежности диапазону, а также для знаков операций "and then" и "or else".

Операция присваивания обозначается комбинацией символов ":=". Она предопределена для всех нелимитированных типов. Операция присваивания не может быть совмещена или переименована. Присваивание запрещено для лимитированных типов. Необходимо подчеркнуть, что операция присваивания в Аде, в отличие от языков C/C++, не возвращает значение и не обладает побочными эффектами.

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

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



Многомерные массивы


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

Square_Size : constant := 5; subtype Square_Index is Integer range 1..Square_Size; type Square is array (Square_Index, Square_Index) of Integer;

Square_Var : Square := ( others => (others => 0) );

Здесь, агрегат, который инициализирует переменную Square_Var типа Square в нуль, построен как агрегат массива массивов, поэтому требуется двойное использование скобок (опции others использованы для упрощения примера).

Более сложный пример инициализации этой переменной, использующий агрегат с позиционной нотацией, может иметь следующий вид:

----------------- столбцы 1 2 3 4 5

Square_Var : Square := ( ( 1, 2, 3, 4, 5), -- строка 1 ( 6, 7, 8, 9, 10), -- строка 2 (11, 12, 13, 14, 15), -- строка 3 (16, 17, 18, 19, 20), -- строка 4 (21, 22, 23, 24, 25) ); -- строка 5

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

Square_Var(1, 5) := 5; Square_Var(5, 5) := 25;

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

Square_Size : constant := 5; subtype Square_Index is Integer range 1..Square_Size;

type Square_Row is array (Square_Index) of Integer; type Square is array (Square_Index) of Square_Row;

Square_Var : Square := ( others => (others => 0) );

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

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

Square_Var (1)(5) := 5; Square_Var (5)(5) := 25;

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

<имя_массива>'First(N) <имя_массива>'Last(N) <имя_массива>'Length(N) <имя_массива>'Range(N)

В данном случае, значение определяемое, например, как <имя_массива>'Range(N) будет возвращать диапазон N-мерного индекса.



Многозадачность


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

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

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

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

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

В качестве стандартных средств поддержки многозадачности Ады используются задачи (tasks), которые хорошо известны со времен стандарта Ada83 и описывают последовательности действий, которые способны выполняться одновременно, а также объекты защищенных типов (protected types), которые являются нововведением стандарта Ada95.

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

Каждая задача выполняется независимо от остальных задач.

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

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

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

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

Специализированные задачи отсутствуют.

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



Модель механизма диспетчеризации


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

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

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

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

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

Таким образом, наша модель для рассмотренной ранее иерархии типов (Root, Child_1, Child_2 и Grand_Child_2_1) будет иметь следующий вид:

------------ Root'Tag ---> | The_Name |---> The_Name для Root ------------

------------ Child_1'Tag ---> | The_Name |---> The_Name для Child_1 ------------

------------ Child_2'Tag ---> | The_Name |---> The_Name для Child_2 ------------

------------ Grand_Child_2_1'Tag ---> | The_Name |---> The_Name для Grand_Child_2_1 ------------

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

Базовый адрес каждой таблицы соответствует значению тэга и содержится в значении надклассового объекта.

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

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




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

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

with Simple_Objects; use Simple_Objects;

package Simple_Objects_New is

type Grand_Child_1_1 is new Child_1 with null record;

procedure New_Method (Self: in Grand_Child_1_1); procedure New_Method_Call (Self: in Grand_Child_1_1'Class); . . .

end Simple_Objects_New;

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

Тогда, в этом описании содержится два примечательных факта:

Мы сознательно не определили для типа Grand_Child_1_1

реализацию функции The_Name. Следовательно, она будет унаследована от типа Child_1

Описание типа Grand_Child_1_1 определяет новую примитивную диспетчеризуемую операцию - процедуру New_Method.

Согласно нашей модели, вид тэга для типа Grand_Child_1_1 будет следующий:

-------------- Grand_Child_1_1'Tag ---> | The_Name |---> The_Name для Child_1 -------------- | New_Method |---> New_Method для Grand_Child_1_1 --------------

Из данной иллюстрация видно, что поскольку тип Grand_Child_1_1

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

для его предка, типа Child_1.

Таким образом, вызов процедуры Show, для типа Grand_Child_1_1, осуществит вызов унаследованной от типа Child_1 реализации функции The_Name

(в соответствии с индексом в таблице диспетчеризации).

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


Модель прерываний Ады


Стандарт описывает следующую модель прерывания:

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

Появление (occurence) прерывания состоит из генерации (generation) и доставки (delivery).

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

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

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

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

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

Будут ли утеряны заблокированные прерывания, обычно, зависит от устройства.

Некоторые прерывания зарезервированы.

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

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

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



Модульные типы


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

Стандарт Ada95 разделяет целочисленные типы на целые числа со знаком и модульные типы. По существу, модульные типы являются целыми числами без знака. Характерной особенностью таких типов является свойство цикличности арифметических операций. Таким образом, модульные типы соответствуют целочисленным беззнаковым типам в других языках программирования (например: Byte, Word... - в реализациях Паскаля; unsigned_short, unsigned... - в C/C++).

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

. . . type Byte is mod 2 ** 8; -- (2 ** 8) = 256 Count : Byte := 255; begin

Count := Count + 1; . . .

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

Кроме этого, с модульными типами удобно использовать знаки битовых операций "and", "or", "xor" и "not". Такие операции трактуют значения модульного типа как битовый шаблон. Например:

type Byte is mod 2 ** 8; -- (2 ** 8) = 256 Some_Var_1 : Byte; Some_Var_2 : Byte; Mask : constant := 16#0F# begin

. . . Some_Var_2 := Some_Var_1 and Mask; . . .

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

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

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

type Unsigned_Byte is mod 2 ** 8; -- (2 ** 8) = 256 type Signed_Byte is range -128 .. +127;

U : Unsigned_Byte := 150; S : Signed_Byte := Signed_Byte(U); -- здесь будет сгенерировано исключение -- Constraint_Error

Этот код будет вызывать генерацию исключения Constraint_Error.

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



Надклассовые типы (wide class types)


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

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

и все производные от него типы.

Надклассовый тип не имеет явного имени и обозначается как атрибут T'Class, где T - имя соответствующего тэгового типа.

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

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

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

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

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

Root | ----------------- | | Child_1 Child_2 | | | Grand_Child_2_1

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

package Simple_Objects is

-- тип Root и его примитивные операции

type Root is tagged null record;

function The_Name (Self: in Root) return String; procedure Show (Self: in Root'Class);

-- тип Child_1 и его примитивные операции

type Child_1 is new Root with null record;

function The_Name (Self: in Child_1) return String;

-- тип Child_2 и его примитивные операции

type Child_2 is new Root with null record;

function The_Name (Self: in Child_2) return String;

-- тип Grand_Child_2_1 и его примитивные операции

type Grand_Child_2_1 is new Child_2 with null record;

function The_Name (Self: in Grand_Child_2_1) return String;

end Simple_Objects;

<


В этом случае, все типы показанной иерархии (Root, Child_1, Child_2 и Grand_Child_2_1) будут принадлежать к надклассовому типу Root'Class.

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

При описании любой переменной надклассового типа, следует учитывать, что любой надклассовый тип T'Class

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

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

V : T'Class := значение_инициализации ;

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

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

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

Instance : Root'Class := Child_1'(Root with null record);

Здесь, Instance - это переменная надклассового типа Root'Class, а ее индивидуальный тип указывается значением инициализации как тип Child_1.

Формальный параметр подпрограммы также может иметь надклассовый тип.

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

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

Как правило, такие подпрограммы называют надклассовыми подпрограммами. Например:

procedure Show (Self : in Root'Class);

В результате, любой фактический параметр, который принадлежит надклассовому типу Root'Class тэгового типа Root



(напомним, что это все типы, производные от типа Root) будет совместим с формальным параметром Self. Например:

declare

Root_Instance : Root; Child_1_Instance : Child_1; Child_2_Instance : Child_2; GRand_Child_2_1_Instance : GRand_Child_2_1;

Instance : Root'Class := Child_1'(Root with null record);

begin

Show (Root_Instance); Show (Child_1_Instance); Show (Child_2_Instance); Show (GRand_Child_2_1_Instance); Show (Instance);

end;

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

type Root_Ref is access Root'Class;

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

declare

Any_Instance: Root_Ref;

begin

Any_Instance := new Child_1'(Root with null record);

. . .

Any_Instance := new Child_2'(Root with null record); . . . end;

В показанном выше примере, переменная Any_Instance имеет тип надклассовой ссылки Root_Ref и может обозначать любой объект который принадлежит классу Root'Class.

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


Настраиваемые модули в языке Ада (generics)


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

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

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

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

Как описано Найдичем (Naiditch), настраиваемые модули во многом подобны шаблонам стандартных писем.

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

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

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

Мы создаем новую подпрограмму или пакет путем конкретизации (instantiating) настраиваемого модуля, или, другими словами, создаем из настраиваемого модуля экземпляр настроенного модуля.

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



Настраиваемые пакеты


Пакеты также могут быть настраиваемыми.

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

generic

type Element is private; -- примечательно, что это параметр -- настройки package Stacks is

procedure Push(E : in Element); procedure Pop(E : out Element); function Empty return Boolean; private

The_Stack : array(1..200) of Element; top : Integer range 0..200 := 0; end Stacks;

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

package body Stacks is

procedure Push(E : in Element) is

. . .

procedure Pop(E : out Element) is

. . .

function Empty return Boolean is

. . .

end Stacks;

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



Настраиваемые пакеты текстового ввода/вывода


Для поддержки текстового ввода/вывода численных данных и данных перечислимых типов Ада предусматривает набор настраиваемых пакетов текстового ввода/вывода:

Ada.Text_IO.Integer_IO -- для целочисленных типов Ada.Text_IO.Modular_IO -- для модульных типов Ada.Text_IO.Float_IO -- для вещественных типов с плавающей точкой Ada.Text_IO.Fixed_IO -- для вещественных типов с фиксированной точкой Ada.Text_IO.Decimal_IO -- для децимальных типов Ada.Text_IO.Enumeration_IO -- для перечислимых типов

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

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

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

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

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

Рассмотрим пример использования настраиваемого пакета Ada.Text_IO.Integer_IO, который предназначен для организации ввода/вывода целочисленных типов.

Предварительно, необходимо выполнить конкретизацию настраиваемого пакета Ada.Text_IO.Integer_IO для соответствующего целочисленного типа (в нашем случае выбран тип Integer, для простоты), и получить экземпляр настроенного модуля:

with Ada.Text_IO; use Ada.Text_IO;

package Int_IO is new Integer_IO(Integer);

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

with Ada.Text_IO; use Ada.Text_IO; with Int_IO; use Int_IO; -- указываем экземпляр настроенного модуля

procedure Demo_Int_IO is

Data_File : Text_IO.File_type;

begin

Create(File => Data_File, Mode => Out_File, Name => "data.dat");

for I in 1..10 loop -- цикл вывода в файл Put(Data_File, I); -- чисел и их квадратов Put(Data_File, " "); Put(Data_File, I * I); New_Line(Data_File); end loop;

Close(Data_File); end Demo_Int_IO;

<

процедура Create создает файл данных


Здесь, процедура Create создает файл данных "data.dat", после чего, в этот файл производится запись некоторых данных процедурами Put и New_Line. Процедура Close закрывает файл.

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

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

Ada.Short_Short_Integer_Text_IO -- для значений типа Short_Short_Integer Ada.Short_Integer_Text_IO -- для значений типа Short_Integer Ada.Integer_Text_IO -- для значений типа Integer Ada.Long_Integer_Text_IO -- для значений типа Long_Integer Ada.Long_Long_Integer_Text_IO -- для значений типа Long_Long_Integer

Ada.Short_Float_Text_IO -- для значений типа Short_Float Ada.Float_Text_IO -- для значений типа Float Ada.Long_Float_Text_IO -- для значений типа Long_Float Ada.Long_Long_Float_Text_IO -- для значений типа Long_Long_Float

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


Настраиваемые подпрограммы


Рассмотрим простой пример описания настраиваемой подпрограммы.

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

в других программных модулях.

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

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

generic

type Element is private; -- Element - это параметр настраиваемой -- подпрограммы

procedure Exchange(A, B : in out Element);

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

procedure Exchange(A, B : in out Element) is

Temp : Element;

begin

T := Temp; T := B; B := Temp; end Exchange;

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

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

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

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

procedure Swap is new Exchange(Integer);

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




procedure Swap is new Exchange(Element => Integer);

Теперь мы имеем процедуру Swap которая меняет местами переменные целого типа Integer.

Здесь, Integer является фактическим параметром настройки, а Element - формальным параметром настройки.

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

procedure Swap (A, B : in out Integer) is

Temp : Integer;

begin

. . .

end Swap;

Таких процедур Swap можно создать столько, сколько необходимо.

procedure Swap is new Exchange(Character); procedure Swap is new Exchange(Element => Account); -- ассоциация по имени

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

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


Неограниченные записи (unconstrained records)


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

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

type Accounts is (Cheque, Savings); type Account (Account_Type: Accounts := Savings) is

record

Account_No : Positive; Title : String(1..10); case Account_Type is

when Savings => Interest : Rate; when Cheque => null; end case; end record;

Здесь, дискриминант записи Account имеет значение по-умолчанию Savings. Теперь, мы можем описать запись:

Household_Account : Account;

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

Household_Account:= (Cheque, 123_456, "household ");

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

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

Кроме того, присваивание значений всему


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

type Property is array (Positive range <>) of Float; type Man (Number: Positive := 2; Size: Positive := 10) is record

Name : String (1..Size); Prop_Array : Property (1..Number); end record;

The_Man : Man;

The_Man.Name := "Ivanov I I"; The_Man.Prop_Array := (25.0, 50.0);

. . .

The_Man := ( Number => 3, Size => 8, Name => "Pyle I C", Prop_Array => (25.0, 50.0, 160.5) );

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


Низкоуровневая средства работы со ссылочными типами и физическими адресами памяти


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

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

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

Так, в стандартном пакете System представлены:

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

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

константа Memory_Size - максимально возможный размер памяти системы

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

Стандарт Ады этого не требует.

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

generic

type Object (<>) is limited private;

package System.Address_To_Access_Conversions is

type Object_Pointer is access all Object; for Object_Pointer'Size use Standard'Address_Size;

function To_Pointer (Value : Address) return Object_Pointer; function To_Address (Value : Object_Pointer) return Address;

end System.Address_To_Access_Conversions;

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

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



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


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

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

Описанию средств системного программирования Ады посвящено приложение C

(Annex C) стандарта Ada95.

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

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

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



Обобщенные ссылочные типы


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

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

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

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

Например:

Int_Var : aliased Integer; Int_Const : aliased constant Integer := 0;

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

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

или зарезервированного слова constant.

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

Например:

type Int_Var_Ptr is access all Integer; type Int_Const_Ptr is access constant Integer;

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




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

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

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

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

procedure General_Access_Demo is

type Int_Var_Ptr_Type is access all Integer; A : aliased Integer; X, Y : Int_Var_Ptr_Type;

begin

X := A'Access; Y := new Integer; end General_Access_Demo;

Здесь, переменная A имеет тип Integer и является косвенно доступной.

Переменные X и Y имеют тип Int_Var_Ptr_Type который является обобщенным ссылочным типом.

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

Переменная Y ссылается на объект типа Integer который размещен в области динамической памяти.

Ада позволяет использовать обобщенные ссылочные типы для формирования ссылок на отдельные элементы внутри составных типов данных (таких как массив и/или запись), например:

type Array_Type is array (Positive range < >) of aliased Integer;

type Record_Type is

record

A_Int_Var : aliased Integer; Int_Var : Integer; end record;

type Int_Var_Ptr_Type is access all Integer;

В данном случае тип Array_Type - это массив, состоящий из косвенно доступных элементов типа Integer, а тип Record_Type - это запись, у которой поля A_Int_Var и Int_Var имеют тип Integer, причем поле A_Int_Var косвенно доступно, а поле Int_Var - нет.

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

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



package Message_Services is

type Message_Code_Type is range 0..100;

subtype Message is String;

function Get_Message(Message_Code: Message_Code_Type) return Message;

pragma Inline(Get_Message); end Message_Services;

package body Message_Services is

type Message_Handle is access constant Message;

Message_0: aliased constant Message := "OK"; Message_1: aliased constant Message := "Up"; Message_2: aliased constant Message := "Shutdown"; Message_3: aliased constant Message := "Shutup"; . . .

Message_Table: array (Message_Code_Type) of

Message_Handle := (0 => Message_0'Access, 1 => Message_1'Access, 2 => Message_2'Access, 3 => Message_3'Access, . . . );

function Get_Message(Message_Code: Message_Code_Type) return Message is

begin

return Message_Table(Message_Code).all; end Get_Message; end Message_Services;

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


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


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

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

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

declare

X : Integer range 1..20;

begin

Put("please enter a number "); Get(X); Put("thank you");

exception

when Constraint_Error => Put("that number should be between 1 and 20"); when others => Put("some other error occurred"); end;

Здесь описаны два обработчика. В одном выполняется обработка только исключений ограничения (Constraint_Error). Второй обработчик выполняет обработку всех остальных исключений (others).

Таким образом, если пользователь вводит число в диапазоне от 1 до 20, то ошибки не происходит и появляется сообщение "thank you".

В противном случае, перед завершением выполнения появляется сообщение обработчика исключения Constraint_Error: "that number should be between 1 and 20".

В случае возникновения какого-либо другого исключения появится сообщение от второго обработчика: "some other error occurred".

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

. . . exception

. . . when Constraint_Error | Storage_Error => . . .

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

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

loop

declare

. . .

begin

. . .

Get(X); exit;

exception

when Constraint_Error => Put("that number ... end;

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

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



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


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

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

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

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

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



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


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

Контролируемыми типами называют типы, производные от типа Controlled

или от лимитированного типа Limited_Controlled.

Оба этих типа описаны в стандартном пакете Ada.Finalization:

package Ada.Finalization is

pragma Preelaborate(Finalization);

type Controlled is abstract tagged null record;

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

type Limited_Controlled is abstract tagged limited null record;

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

private

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

end Ada.Finalization;

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

осуществляется автоматически:

Initialize - "инициализация", вызывается сразу после создания объекта.

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

Adjust - осуществляет "подгонку" содержимого объекта после выполнения присваивания.

Необходимо заметить, что объекты типа Controlled или Limited_Controlled

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

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

Примечательным также является то, что предопределенные управляющие операции Initialize, Adjust и Finalize

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

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




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

declare

A: T; -- создание объекта A и вызов Initialize(A) B: T; -- создание объекта B и вызов Initialize(B) begin

. . . A := B; -- вызов Finalize(A), копирование значения B в A и вызов Adjust(A) . . . end; -- вызов Finalize(A) и Finalize(B)

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

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

. . .

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

. . .

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


Общие сведения о настраиваемых модулях


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

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

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

Как правило, описание настраиваемого модуля представлено двумя частями: спецификацией настраиваемого модуля и телом настраиваемого модуля.

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

Однажды скомпилированные настраиваемые модули помещаются в библиотеку Ады и могут быть указаны в инструкции спецификатора совместности контекста with

в других компилируемых модулях.

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

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

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

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

with Ada.Text_IO; use Ada.Text_IO;

package Int_IO is new Integer_IO(Integer);

Получившийся экземпляр настроенного модуля (пакет Int_IO), в последствии, может быть помещен в инструкции спецификации контекста with и use

любого программного модуля.

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

with Ada.Text_IO; use Ada.Text_IO; with Accounts; use Accounts;

package Account_No_IO is new Integer_IO(Account_No);



Общие сведения о подпрограммах


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

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

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

Каждый формальный параметр подпрограммы имеет имя, тип и режим передачи.

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

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

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

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

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

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

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

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

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

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

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

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



Ограниченные записи (constrained records)


Записи My_Car, My_Bicycle и His_Car, которые мы рассматривали выше, называют ограниченными. Для таких записей значение дискриминанта определяется при описании экземпляра (переменной) записи. Таким образом, однажды определенный дискриминант в последствии никогда не может быть изменен. Так, запись My_Bicycle не имеет полей Tare, Net, Petrol_Consumption, и т.д. При этом, компилятор Ады даже не будет распределять пространство для этих полей.

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



Описание целочисленных констант


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

Приведем простой пример описания константы (именованного числа):

Max_Width : constant := 10_000; -- 10_000 - имеет тип Universal_Integer

-- это не тип Integer



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


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

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

My_Very_Own_Exception : exception; Another_Exception : exception;



Описание перечислимого типа


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

type Computer_Language is (Assembler, Cobol, Lisp, Pascal, Ada); type C_Letter_Languages is (Cobol, C);

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

A_Language : Computer_Language; Early_Language : Computer_Language := Cobol; First_Language : constant Computer_Language := Assembler; Example : C_Letter_Language := Cobol;

Необходимо заметить, что порядок перечисления значений типа, при описании перечислимого типа, имеет самостоятельное значение - он устанавливает отношение порядка следования значений перечислимого типа, которое используется атрибутами 'First, 'Last,

'Pos, 'Val, 'Pred, 'Succ (см "Атрибуты типов"), а также при сравнении величин перечислимого типа. Так, для типа Computer_Language, описанного в примере выше, значение Assembler будет меньше чем значение Cobol.

В одной программе допускается использование одного и того же перечислимого литерала в описаниях различных перечислимых типов. Так, в показанном выше примере, литерал Cobol встречается при описании двух разных типов: Computer_Language и C_Letter_Languages. Такие литералы называют совмещенными (или перегруженными). При этом, Ада, в большинстве случаев, распознает одинаковые перечислимые литералы различных перечислимых типов. В случаях, когда это не возможно - необходимо использовать квалификацию типов.

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

type Primary is (Red, Green, Blue); type Rainbow is (Red, Yellow, Green, Blue, Violet); ... for I in Red..Blue loop ... -- это двусмысленно

Здесь, компилятор не может самостоятельно определить к какому из двух типов (Primary или Rainbow) принадлежит диапазон значений Red..Blue переменной цикла I

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





for I in Rainbow'(Red)..Rainbow'(Blue) loop ... for I in Rainbow'(Red)..Blue loop ... -- необходима только одна квалификация for I in Primary'(Red)..Blue loop ...

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

Описание переменных и преобразование типов


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

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

declare

Instance_1 : Object_1; Instance_2 : Object_2; Instance_3 : Object_3; begin

Instance_1.Field_1 := 1; Instance_2 := ( 1, 2 ); Instance_3 := ( Field_1 => 1, Field_2 => 2, Field_3 => 3 ); . . . end;

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

. . . Instance_1 := Object_1 (Instance_3); Instance_2 := Object_2 (Instance_3); . . .

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

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

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

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

. . . Instance_2 := (Instance_1 with 2); Instance_3 := (Instance_1 with Field_2 => 2, Field_3 => 3); . . .

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