Введение в язык программирования Ada

Release 2022-08

Aug 22, 2022

Copyright © 2018 – 2022, AdaCore

Эта книга опубликована под лицензией CC BY-SA, что означает, что вы можете копировать, распространять, переделывать, преобразовывать и использовать контент для любых целей, даже коммерческих, при условии, что вы предоставляете соответствующую информацию, предоставляете ссылку на лицензию и указываете, были ли внесены изменения. Если вы делаете ремикс, трансформируете или основываетесь на материале, вы должны распространять свои материалы под той же лицензией, что и оригинал. Вы можете найти подробную информацию о лицензии на этой странице#1

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

Этот документ был написан Рафаэлем Амьяром и Густаво А. Хоффманом с рецензией Ричарда Кеннера.

Contents:

- Введение

- История

- Ада сегодня

- Философия

- SPARK

- Императивы языка

- Hello world

- Условный оператор

- Циклы

- Циклы for

- Простой цикл

- Циклы while

- Оператор выбора

- Зоны описания

- Условные выражения

- Условное выражение

- Выражение выбора

- Подпрограммы

- Подпрограммы

- Вызовы подпрограмм

- Вложенные подпрограммы

- Вызов функций

- Виды параметров

- Вызов процедуры

- Параметры in

- Параметры in out

- Параметры out

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

- Переименование

- Модульное программирование

- Пакеты

- Использование пакета

- Тело пакета

- Дочерние пакеты

- Дочерний пакет от дочернего пакета

- Множественные потомки

- Видимость

- Переименование

- Сильно типизированный язык

- Что такое тип?

- Целочисленные типы - Integers

- Семантика операций

- Беззнаковые типы

- Перечисления

- Типы с плавающей запятой

- Основные свойства

- Точность типов с плавающей запятой

- Диапазон значений для типов с плавающей запятой

- Строгая типизация

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

- Подтипы

- Подтипы в качестве псевдонимов типов

- Записи

- Объявление типа записи

- Агрегаты

- Извлечение компонент

- Переименование

- Массивы

- Объявление типа массива

- Доступ по индексу

- Более простые объявления массива

- Атрибут диапазона

- Неограниченные массивы

- Предопределенный тип String

- Ограничения

- Возврат неограниченных массивов

- Объявление массивов (2)

- Отрезки массива

- Переименование

- Подробнее о типах

- Агрегаты: краткая информация

- Cовмещение и квалифицированные выражения

- Символьные типы

- Ссылочные типы

- Введение

- Выделение (allocation) памяти

- Извлечение по ссылке

- Другие особенности

- Взаимно рекурсивные типы

- Подробнее о записях

- Типы записей динамически изменяемого размера

- Записи с дискриминантом

- Записи c вариантами

- Типы с фиксированной запятой

- Десятичные типы с фиксированной запятой

- Обычные типы с фиксированной запятой

- Изоляция

- Простейшая инкапсуляция

- Абстрактные типы данных

- Лимитируемые типы

- Дочерние пакеты и изоляция

- Настраиваемые модули

- Введение

- Объявление формального типа

- Объявление формального объекта

- Определение тела настраиваемого модуля

- Конкретизация настройки

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

- Формальные подпрограммы

- Пример: конкретизация ввода/вывода

- Пример: АТД

- Пример: Обмен

- Пример: Обратный порядок элементов

- Пример: Тестовое приложение

- Исключения

- Объявление исключения

- Возбуждение исключения

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

- Предопределенные исключения

- Управление задачами

- Задачи

- Простая задача

- Простая синхронизация

- Оператор задержки

- Синхронизация: рандеву

- Обрабатывающий цикл

- Циклические задачи

- Защищенные объекты

- Простой объект

- Входы

- Задачные и защищенные типы

- Задачные типы

- Защищенные типы

- Контрактное проектирование

- Пред- и постусловия

- Предикаты

- Инварианты типа

- Взаимодействие с языком C

- Многоязычный проект

- Соглашение о типах

- Подпрограммы на других языках

- Вызов подпрограмм C из Ады

- Вызов Ада подпрограмм из C

- Внешние переменные

- Использование глобальных переменных C в Аде

- Использование переменных Ада в C

- Автоматическое создание связок

- Адаптация связок

- Объектно-ориентированное программирование

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

- Теговые типы

- Надклассовые типы

- Операции диспетчеризации

- Точечная нотация

- Личные и лимитируемые типы с тегами

- Надклассовые ссылочные типы

- Стандартная библиотека: Контейнеры

- Векторы

- Создание экземпляра

- Инициализация

- Добавление элементов

- Доступ к первому и последнему элементам

- Итерация

- Поиск и изменение элементов

- Вставка элементов

- Удаление элементов

- Другие операции

- Множества

- Инициализация и итерация

- Операции с элементами

- Другие операции

- Отображения для неопределенных типов

- Хэшированные отображения

- Упорядоченные отображения

- Сложность

- Стандартная библиотека: Дата и время

- Обработка даты и времени

- Задержка с использованием даты

- Режим реального времени

- Анализ производительности

- Стандартная библиотека: Строки

- Операции со строками

- Ограничение строк фиксированной длины

- Ограниченные строки

- Неограниченные строки

- Стандартная библиотека: Файлы и потоки

- Текстовый ввод-вывод

- Последовательный ввод-вывод

- Прямой ввод-вывод

- Потоковый ввод-вывод

- Стандартная библиотека: Numerics

- Элементарные функции

- Генерация случайных чисел

- Комплексные числа

- Работа с векторами и матрицами

- Приложения

- Приложение A: Формальные типы настройки

- Неопределенные версии типов

- Приложение B: Контейнеры

Footnotes

#1

http://creativecommons.org/licenses/by-sa/4.0

Введение

История

В 1970-х годах Министерство обороны Соединенных Штатов (МО) столкнулось с серьезной проблемой резкого увеличения числа языков программирования, заметив, что различные проекты использовали разные и нестандартные диалекты, языковые подмножества и расширения языков. Что бы решить эту проблему, Министерство обороны запустило конкурс на разработку нового современного языка программирования общего назначения. Победителем вышло предложение, представленное Жаном Ичбией из CII Honeywell-Bull.

Первый стандарт языка Ада был выпущен в 1983 году; впоследствии он был пересмотрен и усовершенствован в 1995, 2005 и 2012 годах, причем каждый пересмотр приносил новые полезные функции.

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

Ада сегодня

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

- Встроенные системы с ограниченным объёмом памяти (сборщик мусора не допускается).

- Прямое взаимодействие с оборудованием.

- Мягкие или жесткие системы реального времени.

- Низкоуровневое системное программирование.

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

- Программирование видеоигр#1

- Аудио в реальном времени#2

- Модули ядра#3

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

С точки зрения современных языков, наиболее близкими с точки зрения целей и уровня абстракции, вероятно, являются C++#4 и Rust#5.

Философия

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

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

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

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

- Явное действие лучше, чем неявное. Хотя это заповедь языка Python#6, Ада идет дальше, чем любой известный нам язык:

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

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

- Семантика очень чётко определена, а неопределенное поведение сведено к абсолютному минимуму.

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

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

SPARK

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

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

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

Footnotes

#1

https://github.com/АдаDoom3/АдаDoom3

#2

http://www.electronicdesign.com/embedded-revolution/assessing-ada-language-audio-applications

#3

http://www.nihamkin.com/tag/kernel.html

#4

https://en.wikipedia.org/wiki/C%2B%2B

#5

https://www.rust-lang.org/en-US/

#6

https://www.python.org

#7

https://learn.adacore.com/courses/intro-to-spark/index.html

Императивы языка

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

На других языках

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

Hello world

Вот очень простая императивная программа Ада:

greet.adb

1 with Ada.Text_IO;

2

3 procedure Greet is

4 begin

5    --  Print "Hello, World!" to the screen

6    Ada.Text_IO.Put_Line ("Hello, World!");

7 end Greet;

Runtime output

Hello, World!

Текс программы, как мы предполагаем, находится в исходном файле greet.adb.

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

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

- with используется для ссылки на внешние модули, которые необходимы в процедуре. Это похоже на import в разных языках или примерно похоже на #include в C и C++. Позже мы разберемся в деталях их работы. Здесь мы запрашиваем стандартный библиотечный модуль, пакет Ada.Text_IO, который содержит процедуру печати текста на экране: Put_Line.

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

- Put_Line - это такая же процедура, как и Greet, за исключением того, что она объявлена в модуле Ada.Text_IO. Это Ада-эквивалент printf в Си.

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

--  We start a comment in this line...

--  and we continue on the second line...

На других языках

Процедуры аналогичны функциям в C или C++, которые возвращают значение void. Позже мы увидим, как объявлять функции в Аде.

Вот вариация примера «Hello, World»:

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4 begin

5    --  Print "Hello, World!" to the screen

6    Put_Line ("Hello, World!");

7 end Greet;

Runtime output

Hello, World!

В этой версии используется конструкция Аді, известное как спецификатор использования (use clause), которая имеет форму use имя-пакета. Как видно на вызове Put_Line, эффект заключается в том, что на объекты из указанного пакета можно ссылаться напрямую – нет необходимости использовать имя-пакета. как префикс.

Условный оператор

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

Условный оператор if в Аде довольно неудивителен по форме и функции:

check_positive.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4 procedure Check_Positive is

5    N : Integer;

6 begin

7    --  Put a String

8    Put ("Enter an integer value: ");

9

10    --  Read in an integer value

11    Get (N);

12

13    if N > 0 then

14       --  Put an Integer

15       Put (N);

16       Put_Line (" is a positive number");

17    end if;

18 end Check_Positive;

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

Этот примере объявляет целочисленную переменную N, запрашивает у пользователя целое число, проверяет, является ли значение положительным, и, если да, отображается значение целого числа, за которым следует строка "является положительным числом" (is a positive number). Если значение не является положительным, то процедура не выводит ничего.

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

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

Вот небольшое изменение в примере, которое иллюстрирует оператор if с частью else:

check_positive.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4 procedure Check_Positive is

5    N : Integer;

6 begin

7    --  Put a String

8    Put ("Enter an integer value: ");

9

10    --  Reads in an integer value

11    Get (N);

12

13    --  Put an Integer

14    Put (N);

15

16    if N > 0 then

17       Put_Line (" is a positive number");

18    else

19       Put_Line (" is not a positive number");

20    end if;

21 end Check_Positive;

В этом примере, если входное значение не является положительным, то программа отображает значение, за которым следует строка "не является положительным числом" (is not a positive number).

Наш последний вариант иллюстрирует оператор if с elsif частями:

check_direction.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4 procedure Check_Direction is

5    N : Integer;

6 begin

7    Put ("Enter an integer value: ");

8    Get (N);

9    Put (N);

10

11    if N = 0 or N = 360 then

12       Put_Line (" is due north");

13    elsif N in 1 .. 89 then

14       Put_Line (" is in the northeast quadrant");

15    elsif N = 90 then

16       Put_Line (" is due east");

17    elsif N in 91 .. 179 then

18       Put_Line (" is in the southeast quadrant");

19    elsif N = 180 then

20       Put_Line (" is due south");

21    elsif N in 181 .. 269 then

22       Put_Line (" is in the southwest quadrant");

23    elsif N = 270 then

24       Put_Line (" is due west");

25    elsif N in 271 .. 359 then

26       Put_Line (" is in the northwest quadrant");

27    else

28       Put_Line (" is not in the range 0..360");

29    end if;

30 end Check_Direction;

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

Ключевое слово elsif в Аде отличается от C или C ++, где вместо него будут использоваться блоки else .. if. И еще одно отличие - это наличие конца end if в Аде, что позволяет избежать проблемы, известной как «висящий else» (dangling else).

Циклы

У Ада есть три способа записи циклов. Каждый из них отличается от циклов for в C / Java / Javascript тем, что обладает более простыми синтаксисом и семантикой в соответствии с философией Ада.

Циклы for

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

greet_5a.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet_5a is

4 begin

5    for I in 1 .. 5 loop

6       --  Put_Line is a procedure call

7       Put_Line ("Hello, World!" & Integer'Image (I));

8       --        ^ Procedure parameter

9    end loop;

10 end Greet_5a;

Runtime output

Hello, World! 1

Hello, World! 2

Hello, World! 3

Hello, World! 4

Hello, World! 5

Несколько моментов, которые следует отметить:

- 1 .. 5 - это дискретный диапазон, от 1 до 5 включительно.

- Параметр цикла I (имя произвольное) в теле цикла имеет значение в этом диапазоне.

- I является локальным для цикла, поэтому вы не можете ссылаться на I вне цикла.

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

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

- Символ & является оператором конкатенации для строковых значений.

- end loop обозначает конец цикла

"Шаг" цикла ограничен 1 (направление вперед) и -1 (назад). Чтобы выполнить итерацию в обратном направлении по диапазону, используйте ключевое слово reverse:

greet_5a_reverse.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet_5a_Reverse is

4 begin

5    for I in reverse 1 .. 5 loop

6       Put_Line ("Hello, World!"

7                 & Integer'Image (I));

8    end loop;

9 end Greet_5a_Reverse;

Runtime output

Hello, World! 5

Hello, World! 4

Hello, World! 3

Hello, World! 2

Hello, World! 1

Границы цикла for могут быть вычислены во время выполнения; они вычисляются один раз, перед выполнением тела цикла. Если значение верхней границы меньше значения нижней границы, то цикл вообще не выполняется. Это относится и к reverse циклам. Таким образом, в следующем примере вывод не производится:

greet_no_op.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet_No_Op is

4 begin

5    for I in reverse 5 .. 1 loop

6       Put_Line ("Hello, World!"

7                 & Integer'Image (I));

8    end loop;

9 end Greet_No_Op;

Build output

greet_no_op.adb:5:23: warning: loop range is null, loop will not execute [enabled by default]

Цикл for имеет более общую форму, чем та, которую мы проиллюстрировали здесь; подробнее об этом позже.

Простой цикл

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

greet_5b.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet_5b is

4    --  Variable declaration:

5    I : Integer := 1;

6    --  ^ Type

7    --             ^ Initial value

8 begin

9    loop

10       Put_Line ("Hello, World!"

11                 & Integer'Image (I));

12

13       --  Exit statement:

14       exit when I = 5;

15       --        ^ Boolean condition

16

17       --  Assignment:

18       I := I + 1;

19       --  There is no I++ short form to

20       --  increment a variable

21    end loop;

22 end Greet_5b;

Runtime output

Hello, World! 1

Hello, World! 2

Hello, World! 3

Hello, World! 4

Hello, World! 5

Этот пример имеет тот же эффект, что и Greet_5a, показанный ранее.

Он иллюстрирует несколько концепций:

- Мы объявили переменную с именем I между is и begin. Этот участок кода представляет собой зону описания. Ада чётко отделяет зону описания от секции операторов подпрограммы. Объявление может находиться в зоне описания, но не допускается в качестве оператора.

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

- Синтаксис для присваивания :=, а синтаксис для равенства =. Их невозможно перепутать, потому что, как отмечалось ранее, в Аде операторы и выражения различны, а выражения не являются допустимыми операторами.

Циклы while

Последняя форма цикла в Аде - это цикл while.

greet_5c.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet_5c is

4    I : Integer := 1;

5 begin

6    --  Condition must be a Boolean value

7    --  (no Integers).

8    --  Operator "<=" returns a Boolean

9    while I <= 5 loop

10       Put_Line ("Hello, World!"

11                 & Integer'Image (I));

12

13       I := I + 1;

14    end loop;

15 end Greet_5c;

Runtime output

Hello, World! 1

Hello, World! 2

Hello, World! 3

Hello, World! 4

Hello, World! 5

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

Эта программа имеет тот же эффект, что и предыдущие примеры.

На других языках

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

Оператор выбора

Оператор выбора case в Аде аналогичен оператору switch из C/C++, но с некоторыми важными отличиями.

Вот пример, вариация программы, которая была показана ранее с инструкцией if:

check_direction.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4 procedure Check_Direction is

5    N : Integer;

6 begin

7    loop

8       Put ("Enter an integer value: ");

9       Get (N);

10       Put (N);

11

12       case N is

13          when 0 | 360 =>

14             Put_Line (" is due north");

15          when 1 .. 89 =>

16             Put_Line (" is in the northeast quadrant");

17          when 90 =>

18             Put_Line (" is due east");

19          when 91 .. 179 =>

20             Put_Line (" is in the southeast quadrant");

21          when 180 =>

22             Put_Line (" is due south");

23          when 181 .. 269 =>

24             Put_Line (" is in the southwest quadrant");

25          when 270 =>

26             Put_Line (" is due west");

27          when 271 .. 359 =>

28             Put_Line (" is in the northwest quadrant");

29          when others =>

30             Put_Line (" Au revoir");

31             exit;

32       end case;

33    end loop;

34 end Check_Direction;

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

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

Примечательные моменты в операторе выбора в Аде:

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

- Каждое возможное значение для выражения выбора должно быть охвачено уникальной ветвью оператора case. Это будет проверено во время компиляции.

- Ветвь может указывать одно значение, например 0; диапазон значений, например 1 .. 89; или любая их комбинация (с разделителем |).

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

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

- Когда выполнение операторов в выбранной ветви завершено, управление возобновляется после последней ветви (после end case). В отличие от C, выполнение не попадает в следующую ветвь. Таким образом, в Аде не нужен (и не существует) оператор break.

Зоны описания

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

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

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

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4    X : Integer;

5 begin

6    X := 0;

7    Put_Line ("The initial value of X is "

8              & Integer'Image (X));

9

10    Put_Line ("Performing operation on X...");

11    X := X + 1;

12

13    Put_Line ("The value of X now is "

14              & Integer'Image (X));

15 end Main;

Runtime output

The initial value of X is  0

Performing operation on X...

The value of X now is  1

Давайте рассмотрим пример вложенной процедуры:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4    procedure Nested is

5    begin

6       Put_Line ("Hello World");

7    end Nested;

8 begin

9    Nested;

10    --  Call to Nested

11 end Main;

Runtime output

Hello World

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4 begin

5    loop

6       Put_Line ("Please enter your name: ");

7

8       declare

9          Name : String := Get_Line;

10          --               ^ Call to the

11          --                 Get_Line function

12       begin

13          exit when Name = "";

14          Put_Line ("Hi " & Name & "!");

15       end;

16

17       --  Name is undefined here

18    end loop;

19

20   Put_Line ("Bye!");

21 end Greet;

Build output

greet.adb:9:10: warning: "Name" is not modified, could be declared constant [-gnatwk]

Attention

Функция Get_Line позволяет получать входные данные от пользователя и выдавать результат в виде строки. Это более или менее эквивалентно функции scanf C.

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

Условные выражения

В Ада 2012 были введены выражения-аналоги условных операторов (if и case).

Условное выражение

Вот альтернативная версия примера приведённого выше; операторы if заменены на if выражения:

check_positive.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

3

4 procedure Check_Positive is

5    N : Integer;

6 begin

7    Put ("Enter an integer value: ");

8    Get (N);

9    Put (N);

10

11    declare

12       S : constant String :=

13         (if N > 0 then " is a positive number"

14          else " is not a positive number");

15    begin

16       Put_Line (S);

17    end;

18 end Check_Positive;

Выражение if вычисляет одну из двух строк в зависимости от N и присваивает это значение локальной переменной S.

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

- Выражения всех ветвей должны быть одного типа

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

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

Вот еще один пример:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4 begin

5    for I in 1 .. 10 loop

6       Put_Line (if I mod 2 = 0 then "Even" else "Odd");

7    end loop;

8 end Main;

Runtime output

Odd

Even

Odd

Even

Odd

Even

Odd

Even

Odd

Even

Эта программа выдает 10 строк вывода, чередующихся слов "Нечетный" и "Четный" ("Odd" и "Even").

Выражение выбора

Аналогично выражениям if, в Аде также есть выражения case. Они работают именно так, как вы и ожидали.

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4 begin

5    for I in 1 .. 10 loop

6       Put_Line (case I is

7                 when 1 | 3 | 5 | 7 | 9 => "Odd",

8                 when 2 | 4 | 6 | 8 | 10 => "Even");

9    end loop;

10 end Main;

Runtime output

Odd

Even

Odd

Even

Odd

Even

Odd

Even

Odd

Even

Эта программа имеет тот же эффект, что и в предыдущем примере.

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

Footnotes

Подпрограммы

Подпрограммы

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

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

В этом примере показано объявление и определение функции:

increment.ads

1 function Increment (I : Integer) return Integer;

increment.adb

1 --  We declare (but don't define) a function with

2 --  one parameter, returning an integer value

3

4 function Increment (I : Integer) return Integer is

5    --  We define the Increment function

6 begin

7     return I + 1;

8 end Increment;

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

procedure Proc;

function Func return Integer;

Вот еще один вариант предыдущего примера:

increment_by.ads

1 function Increment_By

2   (I    : Integer := 0;

3    Incr : Integer := 1) return Integer;

4 --                ^ Default value for parameters

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

Вот реализация функции, описанной выше:

increment_by.adb

1 function Increment_By

2   (I    : Integer := 0;

3    Incr : Integer := 1) return Integer is

4 begin

5    return I + Incr;

6 end Increment_By;

В наборе инструментов GNAT

Стандарт языка Ада не регламентирует в каких файлах следует расположить спецификацию и тело подпрограммы. Другими словами, стандарт не навязывает какую-либо структуру организации файлов или расширения имен файлов. К примеру, мы могли бы сохранить и спецификацию и тело указаной выше функции Increment в файле с названием increment.txt. (Мы даже могли бы поместить весь исходный код системы в один файл.) С точки зрения стандарта это вполне допустимо.

С другой стороны, набор инструментов GNAT требует следующую схему наименования файлов:

- файлы с расширением .ads содержат спецификацию, тогда, как

- файлы с расширением .adb содержат реализацию.

Таким образом, для инструментария GNAT, спецификация функции Increment должна находиться в файле increment.ads, а ее реализация должна находиться в файле increment.adb. Это правило также применяется для пакетов, которые мы обсудим позже. (Отметим, однако, что это правило можно обойти.) Дополнительные детали смотрите в курсе Introduction to GNAT Toolchain#1 или в GPRbuild User’s Guide#2.

Вызовы подпрограмм

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

show_increment.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Increment_By;

3

4 procedure Show_Increment is

5    A, B, C : Integer;

6 begin

7    C := Increment_By;

8    --              ^ Parameterless call,

9    --                value of I is 0

10    --                and Incr is 1

11

12    Put_Line ("Using defaults for Increment_By is "

13              & Integer'Image (C));

14

15    A := 10;

16    B := 3;

17    C := Increment_By (A, B);

18    --                 ^ Regular parameter passing

19

20    Put_Line ("Increment of "

21              & Integer'Image (A)

22              & " with "

23              & Integer'Image (B)

24              & " is "

25              & Integer'Image (C));

26

27    A := 20;

28    B := 5;

29    C := Increment_By (I    => A,

30                       Incr => B);

31     --                ^ Named parameter passing

32

33    Put_Line ("Increment of "

34              & Integer'Image (A)

35              & " with "

36              & Integer'Image (B)

37              & " is "

38              & Integer'Image (C));

39 end Show_Increment;

Runtime output

Using defaults for Increment_By is  1

Increment of  10 with  3 is  13

Increment of  20 with  5 is  25

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

- Позиционные параметры должны идти первыми.

- Позиционный параметр не может следовать за именованным параметром.

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

Вложенные подпрограммы

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

Это полезно по двум причинам:

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

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

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

show_increment.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Increment_By;

3

4 procedure Show_Increment is

5    A, B, C : Integer;

6

7    procedure Display_Result is

8    begin

9       Put_Line ("Increment of "

10                 & Integer'Image (A)

11                 & " with "

12                 & Integer'Image (B)

13                 & " is "

14                 & Integer'Image (C));

15    end Display_Result;

16

17 begin

18    A := 10;

19    B := 3;

20    C := Increment_By (A, B);

21    Display_Result;

22 end Show_Increment;

Runtime output

Increment of  10 with  3 is  13

Вызов функций

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

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

quadruple.adb

1 function Quadruple (I : Integer) return Integer is

2     function Double (I : Integer) return Integer is

3     begin

4        return I * 2;

5     end Double;

6

7    Res : Integer := Double (Double (I));

8    --               ^ Calling the Double

9    --                 function

10 begin

11    Double (I);

12    --  ERROR: cannot use call to function

13    --         "Double" as a statement

14

15    return Res;

16 end Quadruple;

Build output

quadruple.adb:11:04: error: cannot use call to function "Double" as a statement

quadruple.adb:11:04: error: return value of a function call cannot be ignored

gprbuild: *** compilation phase failed

В наборе инструментов GNAT

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

function Read_Int

(Stream :     Network_Stream;

Result : out Integer) return Boolean;

procedure Main is

Stream : Network_Stream := Get_Stream;

My_Int : Integer;

-- Warning: in the line below, B is

--          never read.

B : Boolean := Read_Int (Stream, My_Int);

begin

null;

end Main;

Затем у вас есть два решения, чтобы отключить это предупреждение:

- Либо аннотировать переменную с помощью pragma Unreferenced, таким образом:

B : Boolean := Read_Int (Stream, My_Int);

pragma Unreferenced (B);

- Или дайть переменной имя, которое содержит любую из строк: discard dummy ignore junk unused (без учета регистра)

Виды параметров

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

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

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

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

in

Параметр может быть только считан, но не записан

out

Параметр можно записать, а затем прочитать

in out

Параметр может быть, как считан, так и записан

По умолчанию вид параметра будет in; до сих пор большинство примеров использовали параметры вида in.

Исторически

Функции и процедуры изначально были более разными по философии. До Ада 2012 функции могли принимать только «входящие» (in) параметры.

Вызов процедуры

Параметры in

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

swap.adb

1 procedure Swap (A, B : Integer) is

2    Tmp : Integer;

3 begin

4    Tmp := A;

5

6    --  Error: assignment to "in" mode

7    --         parameter not allowed

8    A := B;

9

10    --  Error: assignment to "in" mode

11    --         parameter not allowed

12    B := Tmp;

13 end Swap;

Build output

swap.adb:8:04: error: assignment to "in" mode parameter not allowed

swap.adb:12:04: error: assignment to "in" mode parameter not allowed

gprbuild: *** compilation phase failed

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

Параметры in out

Для исправления кода, приведенного выше, можно использовать параметр in out .

in_out_params.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure In_Out_Params is

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

5       Tmp : Integer;

6    begin

7       Tmp := A;

8       A   := B;

9       B   := Tmp;

10    end Swap;

11

12    A : Integer := 12;

13    B : Integer := 44;

14 begin

15     Swap (A, B);

16

17     --  Prints 44

18     Put_Line (Integer'Image (A));

19 end In_Out_Params;

Runtime output

44

Параметр in out обеспечивает доступ для чтения и записи к объекту, переданному в качестве параметра, поэтому в приведенном выше примере видно, что значение A изменяется после вызова функции Swap.

Attention

В то время как параметры in out немного похожи на ссылки в C++ или обычные параметры в Java, которые передаются по ссылке, стандарт языка Ада не требует передачи параметров in out "по ссылке", за исключением определенных категорий типов, как будет объяснено позже.

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

Параметры out

Вид «out» применяется, когда подпрограмме необходимо выполнить запись в параметр, который может быть не инициализирован в момент вызова. Чтение значения выходного параметра разрешено, но оно должно выполняться только после того, как подпрограмма присвоила значение параметру. Параметры out немного похожи на возвращаемые значения функций. Когда подпрограмма возвращается, фактический параметр (переменная) будет иметь значение параметра в точке возврата.

На других языках

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

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

procedure Read_Int

(Stream  :     Network_Stream;

Success : out Boolean;

Result  : out Integer);

function Read_Int

(Stream :     Network_Stream;

Result : out Integer) return Boolean;

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

В наборе инструментов GNAT

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

outp.adb

1 procedure Outp is

2    procedure Foo (A : out Integer) is

3       B : Integer := A;

4       --             ^ Warning on reference

5       --               to uninitialized A

6    begin

7       A := B;

8    end Foo;

9 begin

10    null;

11 end Outp;

Build output

outp.adb:2:14: warning: procedure "Foo" is not referenced [-gnatwu]

outp.adb:3:07: warning: "B" is not modified, could be declared constant [-gnatwk]

outp.adb:3:22: warning: "A" may be referenced before it has a value [enabled by default]

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

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

mutually_recursive_subprograms.adb

1 procedure Mutually_Recursive_Subprograms is

2     procedure Compute_A (V : Natural);

3     --  Forward declaration of Compute_A

4

5     procedure Compute_B (V : Natural) is

6     begin

7        if V > 5 then

8           Compute_A (V - 1);

9           --  Call to Compute_A

10        end if;

11     end Compute_B;

12

13     procedure Compute_A (V : Natural) is

14     begin

15        if V > 2 then

16           Compute_B (V - 1);

17           --  Call to Compute_B

18        end if;

19     end Compute_A;

20 begin

21    Compute_A (15);

22 end Mutually_Recursive_Subprograms;

Переименование

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

procedure New_Proc renames Original_Proc;

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

a_procedure_with_very_long_name_that_cannot_be_changed.ads

1 procedure A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed

2   (A_Message : String);

a_procedure_with_very_long_name_that_cannot_be_changed.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed

4   (A_Message : String) is

5 begin

6    Put_Line (A_Message);

7 end A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;

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

show_renaming.adb

1 with A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;

2

3 procedure Show_Renaming is

4

5    procedure Show (S : String) renames

6      A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;

7

8 begin

9    Show ("Hello World!");

10 end Show_Renaming;

Runtime output

Hello World!

Обратите внимание, что исходное имя (A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed) по-прежнему остается доступно после объявления процедуры Show.

Можно также переименовать подпрограммы из стандартной библиотеки. Например, можно переименовать Integer'Image в Img:

show_image_renaming.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Image_Renaming is

4

5    function Img (I : Integer) return String

6      renames Integer'Image;

7

8 begin

9    Put_Line (Img (2));

10    Put_Line (Img (3));

11 end Show_Image_Renaming;

Runtime output

2

3

Переименование также позволяет вводить выражения по умолчанию, которые не были указаны в исходном объявлении. Например, можно задать "Hello World!" в качестве значения по умолчанию для параметра String процедуры Show:

with A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;

procedure Show_Renaming_Defaults is

procedure Show (S : String := "Hello World!")

renames

A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;

begin

Show;

end Show_Renaming_Defaults;

Footnotes

#1

https://learn.adacore.com/courses/GNAT_Toolchain_Intro/index.html

#2

https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug.html

Модульное программирование

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

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

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

Пакеты

Вот пример объявления пакета в Аде:

week.ads

1 package Week is

2

3    Mon : constant String := "Monday";

4    Tue : constant String := "Tuesday";

5    Wed : constant String := "Wednesday";

6    Thu : constant String := "Thursday";

7    Fri : constant String := "Friday";

8    Sat : constant String := "Saturday";

9    Sun : constant String := "Sunday";

10

11 end Week;

А вот как вы его используете:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Week;

3 --  References the Week package, and

4 --  adds a dependency from Main to Week

5

6 procedure Main is

7 begin

8    Put_Line ("First day of the week is "

9              & Week.Mon);

10 end Main;

Runtime output

First day of the week is Monday

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

Хотя спецификатор контекста with указывает зависимость, в приведенном выше примере видно, что нам всё еще нужно использовать префикс с менем пакета, чтобы сослаться на имя в этом пакете. (Если бы мы также указали спецификатор использования use Week, то такой префикс уже не был бы необходим.)

При доступе к объектам из пакета используется точечная нотация A.B, аналогичная той, что используется для доступа к полям записей.

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

На других языках

Пакеты похожи на файлы заголовков в C / C ++, но семантически сильно отличаются от них.

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

- Непосредственным следствием этого является то, что конструкция with работает на семантическом уровне, а не с помощью подстановки текста. Следовательно, когда вы работаете с пакетом указывая with, вы говорите компилятору: «Я зависим от этой семантической единицы», а не «включите сюда эту кучу текста».

- Таким образом, действие пакета не зависит от того, откуда на него ссылается with. Сравните это с C/C++, где смысл включенного текста может меняться в зависимости от контекста, в котором появляется #include.

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

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

В наборе инструментов GNAT

Стандарт языка Ада не предусматривает каких-либо особых отношений между исходными файлами и пакетами; например, теоретически вы можете поместить весь свой код в один файл или использовать свои собственные соглашения об именовании файлов. На практике, однако, каждая реализация имеет свои правила. Для GNAT каждый модуль компиляции верхнего уровня должен быть помещен в отдельный файл. В приведенном выше примере пакет Week будет находиться в файле .ads (для Ада спецификации), а основная процедура Main - в файле .adb (для Ада тела).

Использование пакета

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

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

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 --                ^ Make every entity of the

3 --                  Ada.Text_IO package

4 --                  directly visible.

5 with Week;

6

7 procedure Main is

8    use Week;

9    --  Make every entity of the Week

10    --  package directly visible.

11 begin

12    Put_Line ("First day of the week is " & Mon);

13 end Main;

Runtime output

First day of the week is Monday

Как вы можете видеть в приведенном выше примере:

- Put_Line - это подпрограмма из пакета Ada.Text_IO. Мы можем ссылаться на нее непосредственно, потому что мы указали пакет в use в верхней части основного модуля Main.

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

Тело пакета

В приведенном выше простом примере пакет Week содержит только объявления и не содержит реализаций. Это не ошибка: в спецификации пакета, которая проиллюстрирована выше, нельзя объявлять реализации. Реализации должны находиться в теле пакета.

operations.ads

1 package Operations is

2

3    --  Declaration

4    function Increment_By

5      (I    : Integer;

6       Incr : Integer := 0) return Integer;

7

8    function Get_Increment_Value return Integer;

9

10 end Operations;

operations.adb

1 package body Operations is

2

3    Last_Increment : Integer := 1;

4

5    function Increment_By

6      (I    : Integer;

7       Incr : Integer := 0) return Integer is

8    begin

9       if Incr /= 0 then

10          Last_Increment := Incr;

11       end if;

12

13       return I + Last_Increment;

14    end Increment_By;

15

16    function Get_Increment_Value return Integer is

17    begin

18       return Last_Increment;

19    end Get_Increment_Value;

20

21 end Operations;

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

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

В этом примере показано, как непосредственно использовать Increment_By:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Operations;

3

4 procedure Main is

5    use Operations;

6

7    I : Integer := 0;

8    R : Integer;

9

10    procedure Display_Update_Values is

11       Incr : constant Integer := Get_Increment_Value;

12    begin

13       Put_Line (Integer'Image (I)

14                 & " incremented by "

15                 & Integer'Image (Incr)

16                 & " is "

17                 & Integer'Image (R));

18       I := R;

19    end Display_Update_Values;

20 begin

21    R := Increment_By (I);

22    Display_Update_Values;

23    R := Increment_By (I);

24    Display_Update_Values;

25

26    R := Increment_By (I, 5);

27    Display_Update_Values;

28    R := Increment_By (I);

29    Display_Update_Values;

30

31    R := Increment_By (I, 10);

32    Display_Update_Values;

33    R := Increment_By (I);

34    Display_Update_Values;

35 end Main;

Runtime output

0 incremented by  1 is  1

1 incremented by  1 is  2

2 incremented by  5 is  7

7 incremented by  5 is  12

12 incremented by  10 is  22

22 incremented by  10 is  32

Дочерние пакеты

Пакеты можно использовать для создания иерархий. Мы достигаем этого с помощью дочерних пакетов, которые расширяют функциональность родительского пакета. Одним из примеров дочернего пакета, который мы использовали до сих пор, является пакет Ada.Text_IO. Здесь родительский пакет называется Ada, а дочерний пакет называется Text_IO. В предыдущих примерах мы использовали процедуру Put_Line из дочернего пакета Text_IO.

Важное замечание

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

Давайте начнем обсуждение дочерних пакетов с нашего предыдущего пакета Week:

week.ads

1 package Week is

2

3    Mon : constant String := "Monday";

4    Tue : constant String := "Tuesday";

5    Wed : constant String := "Wednesday";

6    Thu : constant String := "Thursday";

7    Fri : constant String := "Friday";

8    Sat : constant String := "Saturday";

9    Sun : constant String := "Sunday";

10

11 end Week;

Если мы хотим создать дочерний пакет для Week, мы можем написать:

week-child.ads

1 package Week.Child is

2

3    function Get_First_Of_Week return String;

4

5 end Week.Child;

Здесь Week - это родительский пакет, а Child - дочерний. Это соответствующее тело пакета Week.Child:

week-child.adb

1 package body Week.Child is

2

3    function Get_First_Of_Week return String is

4    begin

5       return Mon;

6    end Get_First_Of_Week;

7

8 end Week.Child;

В реализации функции Get_First_Of_Week мы можем использовать строку Mon непосредственно, хотя она объявлена в родительском пакете Week. Мы не пишем здесь with Week, потому что все элементы из спецификации пакета Week, такие как Mon, Tue и т. д., видны в дочернем пакете Week.Child.

Теперь, когда мы завершили реализацию пакета Week.Child, мы можем использовать элементы из этого дочернего пакета в подпрограмме, просто написав with Week.Child. Точно так же, если мы хотим использовать эти элементы непосредственно, мы дополнительно напишем use Week.Child. Например:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Week.Child;  use Week.Child;

3

4 procedure Main is

5 begin

6    Put_Line ("First day of the week is "

7              & Get_First_Of_Week);

8 end Main;

Runtime output

First day of the week is Monday

Дочерний пакет от дочернего пакета

До сих пор мы видели двухуровневую иерархию пакетов. Но иерархия, которую мы потенциально можем создать, этим не ограничивается. Например, мы могли бы расширить иерархию предыдущего примера исходного кода, объявив пакет Week.Child.Grandchild. В этом случае Week.Child будет родительским для пакета Grandchild. Рассмотрим эту реализацию:

week-child-grandchild.ads

1 package Week.Child.Grandchild is

2

3    function Get_Second_Of_Week return String;

4

5 end Week.Child.Grandchild;

week-child-grandchild.adb

1 package body Week.Child.Grandchild is

2

3    function Get_Second_Of_Week return String is

4    begin

5       return Tue;

6    end Get_Second_Of_Week;

7

8 end Week.Child.Grandchild;

Мы можем использовать этот новый пакет Grandchild в нашем тестовом приложении так же, как и раньше: мы можем повторно использовать предыдущее тестовое приложение адаптировав with, use и вызов функции. Вот обновленный код:

main.adb

1 with Ada.Text_IO;           use Ada.Text_IO;

2 with Week.Child.Grandchild; use Week.Child.Grandchild;

3

4 procedure Main is

5 begin

6    Put_Line ("Second day of the week is "

7              & Get_Second_Of_Week);

8 end Main;

Runtime output

Second day of the week is Tuesday

Опять же, это не предел иерархии пакетов. Мы могли бы продолжить расширение иерархии предыдущего примера, реализовав пакет Week.Child.Grandchild.Grand_grandchild.

Множественные потомки

До сих пор мы видели лишь один дочерний пакет родительского пакета. Однако родительский пакет также может иметь несколько дочерних. Мы могли бы расширить приведенный выше пример и создать пакет Week.Child_2. Например:

week-child_2.ads

1 package Week.Child_2 is

2

3    function Get_Last_Of_Week return String;

4

5 end Week.Child_2;

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

Это соответствующее тело пакета Week.Child_2:

week-child_2.adb

1 package body Week.Child_2 is

2

3    function Get_Last_Of_Week return String is

4    begin

5       return Sun;

6    end Get_Last_Of_Week;

7

8 end Week.Child_2;

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

main.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Week.Child;   use Week.Child;

3 with Week.Child_2; use Week.Child_2;

4

5 procedure Main is

6 begin

7    Put_Line ("First day of the week is "

8              & Get_First_Of_Week);

9    Put_Line ("Last day of the week is "

10              & Get_Last_Of_Week);

11 end Main;

Runtime output

First day of the week is Monday

Last day of the week is Sunday

Видимость

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

Рассмотрим пакет Book и его дочерний элемент Additional_Operations:

book.ads

1 package Book is

2

3    Title : constant String :=

4      "Visible for my children";

5

6    function Get_Title return String;

7

8    function Get_Author return String;

9

10 end Book;

book-additional_operations.ads

1 package Book.Additional_Operations is

2

3    function Get_Extended_Title return String;

4

5    function Get_Extended_Author return String;

6

7 end Book.Additional_Operations;

Это тела обоих пакетов:

book.adb

1 package body Book is

2

3    Author : constant String :=

4      "Author not visible for my children";

5

6    function Get_Title return String is

7    begin

8       return Title;

9    end Get_Title;

10

11    function Get_Author return String is

12    begin

13       return Author;

14    end Get_Author;

15

16 end Book;

book-additional_operations.adb

1 package body Book.Additional_Operations is

2

3    function Get_Extended_Title return String is

4    begin

5       return "Book Title: " & Title;

6    end Get_Extended_Title;

7

8    function Get_Extended_Author return String is

9    begin

10       --  "Author" string declared in the body

11       --  of the Book package is not visible

12       --  here. Therefore, we cannot write:

13       --

14       --  return "Book Author: " & Author;

15

16       return "Book Author: Unknown";

17    end Get_Extended_Author;

18

19 end Book.Additional_Operations;

В реализации Get_Extended_Title мы используем константу Title из родительского пакета Book. Однако, как указано в комментариях к функции Get_Extended_Author, строка Author, которую мы объявили в теле пакета Book, не отображается в пакете Book.Additional_Operations. Следовательно, мы не можем использовать его для реализации функции Get_Extended_Author.

Однако мы можем использовать функцию Get_Author из Book в реализации функции Get_Extended_Author для получения этой строки. Точно так же мы можем использовать эту стратегию для реализации функции Get_Extended_Title. Это адаптированный код:

book-additional_operations.adb

1 package body Book.Additional_Operations is

2

3    function Get_Extended_Title return String is

4    begin

5       return "Book Title: " & Get_Title;

6    end Get_Extended_Title;

7

8    function Get_Extended_Author return String is

9    begin

10       return "Book Author: " & Get_Author;

11    end Get_Extended_Author;

12

13 end Book.Additional_Operations;

Вот простое тестовое приложение для указанных выше пакетов:

main.adb

1 with Ada.Text_IO;                use Ada.Text_IO;

2 with Book.Additional_Operations; use Book.Additional_Operations;

3

4 procedure Main is

5 begin

6    Put_Line (Get_Extended_Title);

7    Put_Line (Get_Extended_Author);

8 end Main;

Runtime output

Book Title: Visible for my children

Book Author: Author not visible for my children

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

Переименование

Ранее мы упоминали, что подпрограммы можно переименовывать. Мы также можем переименовывать пакеты. Опять же, для этого мы используем ключевое слово renames. В следующем примере пакет Ada.Text_IO переименовывается как TIO:

main.adb

1 with Ada.Text_IO;

2

3 procedure Main is

4    package TIO renames Ada.Text_IO;

5 begin

6    TIO.Put_Line ("Hello");

7 end Main;

Runtime output

Hello

Мы можем использовать переименование, чтобы улучшить читаемость нашего кода, используя более короткие имена пакетов. В приведенном выше примере мы пишем TIO.Put_Line вместо более длинного имени (Ada.Text_IO.Put_Line). Этот подход особенно полезен, когда мы не используем спецификатор использования use, но хотим, чтобы код не становился слишком многословным.

Обратите внимание, что мы также можем переименовывать подпрограммы и объекты внутри пакетов. Например, мы могли бы просто переименовать процедуру Put_Line в приведенном выше примере исходного кода:

main.adb

1 with Ada.Text_IO;

2

3 procedure Main is

4    procedure Say (Something : String)

5      renames Ada.Text_IO.Put_Line;

6 begin

7    Say ("Hello");

8 end Main;

Runtime output

Hello

Footnotes

Сильно типизированный язык

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

Что такое тип?

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

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

Целочисленные типы - Integers

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

integer_type_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Integer_Type_Example is

4    --  Declare a signed integer type,

5    --  and give the bounds

6    type My_Int is range -1 .. 20;

7    --                         ^ High bound

8    --                   ^ Low bound

9

10    --  Like variables, type declarations can

11    --  only appear in declarative regions.

12 begin

13    for I in My_Int loop

14       Put_Line (My_Int'Image (I));

15       --              ^ 'Image attribute

16       --                converts a value

17       --                to a String.

18    end loop;

19 end Integer_Type_Example;

Runtime output

-1

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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

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

Целочисленные типы Ада

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

Еще один момент, который следует отметить в приведенном выше примере, - это выражение My_Int'Image (I). Обозначение вида Name'Attribute (необязательные параметры) используется для того, что в Аде называется атрибутом. Атрибут - это встроенная операция над типом, значением или какой-либо другой программной сущностью. Доступ к нему осуществляется с помощью символа ' (апостроф в ASCII).

Ада имеет несколько "встроенных" типов; Integer - один из них. Вот как тип целых чисел Integer может быть определен для типичного процессора:

type Integer is

range -(2 ** 31) .. +(2 ** 31 - 1);

Знак ** обозначает возведение в степень, в итоге, первое допустимое значение для типа Integer равно -2^31, а последнее допустимое значение равно 2^31 - 1.

Ада не регламентирует диапазон встроенного типа Integer. Реализация для 16-битного целевого процессора, вероятно, выберет диапазон от - 2^15 до 2^15 - 1.

Семантика операций

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

main.adb

1 procedure Main is

2    A : Integer := Integer'Last;

3    B : Integer;

4 begin

5    B := A + 5;

6    --  This operation will overflow, eg. it

7    --  will raise an exception at run time.

8 end Main;

Build output

main.adb:2:04: warning: "A" is not modified, could be declared constant [-gnatwk]

main.adb:3:04: warning: variable "B" is assigned but never read [-gnatwm]

main.adb:5:04: warning: possibly useless assignment to "B", value might not be referenced [-gnatwm]

main.adb:5:11: warning: value not in range of type "Standard.Integer" [enabled by default]

main.adb:5:11: warning: "Constraint_Error" will be raised at run time [enabled by default]

Runtime output

raised CONSTRAINT_ERROR : main.adb:5 overflow check failed

Существует два типа проверок переполнения:

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

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

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

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4    type My_Int is range 1 .. 20;

5    A : My_Int := 12;

6    B : My_Int := 15;

7    M : My_Int := (A + B) / 2;

8    --  No overflow here, overflow checks

9    --  are done at specific boundaries.

10 begin

11    for I in 1 .. M loop

12       Put_Line ("Hello, World!");

13    end loop;

14    --  Loop body executed 13 times

15 end Main;

Build output

main.adb:5:04: warning: "A" is not modified, could be declared constant [-gnatwk]

main.adb:6:04: warning: "B" is not modified, could be declared constant [-gnatwk]

main.adb:7:04: warning: "M" is not modified, could be declared constant [-gnatwk]

Runtime output

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

Hello, World!

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

Беззнаковые типы

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

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

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4    type Mod_Int is mod 2 ** 5;

5    --              ^ Range is 0 .. 31

6

7    A : constant Mod_Int := 20;

8    B : constant Mod_Int := 15;

9

10    M : constant Mod_Int := A + B;

11    --  No overflow here,

12    --  M = (20 + 15) mod 32 = 3

13 begin

14    for I in 1 .. M loop

15       Put_Line ("Hello, World!");

16    end loop;

17 end Main;

Runtime output

Hello, World!

Hello, World!

Hello, World!

В отличие от C/C++, такое поведение гарантировано спецификацией языка Ада и на него можно положиться при создании переносимого кода. Кроме того, для реализации определенных алгоритмов и структур данных, таких как кольцевые буферы#1, очень удобно иметь возможность использовать эффект "заворачивания" на произвольных границах, ведь модуль не обязательно должен быть степенью 2.

Перечисления

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

enumeration_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Enumeration_Example is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7    --  An enumeration type

8 begin

9    for I in Days loop

10       case I is

11          when Saturday .. Sunday =>

12             Put_Line ("Week end!");

13

14          when Monday .. Friday =>

15             Put_Line ("Hello on "

16                       & Days'Image (I));

17             --  'Image attribute, works on

18             --  enums too

19       end case;

20    end loop;

21 end Enumeration_Example;

Runtime output

Hello on MONDAY

Hello on TUESDAY

Hello on WEDNESDAY

Hello on THURSDAY

Hello on FRIDAY

Week end!

Week end!

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

type Boolean is (False, True);

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

Типы с плавающей запятой

Основные свойства

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

floating_point_demo.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Floating_Point_Demo is

4    A : constant Float := 2.5;

5 begin

6    Put_Line ("The value of A is "

7              & Float'Image (A));

8 end Floating_Point_Demo;

Runtime output

The value of A is  2.50000E+00

Приложение отобразит 2.5 как значение A.

Язык Ада не регламентирует точность (количество десятичных цифр в мантиссе) для Float; на типичной 32-разрядной машине точность будет равна 6.

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

floating_point_operations.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Floating_Point_Operations is

4    A : Float := 2.5;

5 begin

6    A := abs (A - 4.5);

7    Put_Line ("The value of A is "

8              & Float'Image (A));

9

10    A := A ** 2 + 1.0;

11    Put_Line ("The value of A is "

12              & Float'Image (A));

13 end Floating_Point_Operations;

Runtime output

The value of A is  2.00000E+00

The value of A is  5.00000E+00

Значение A равно 2.0 после первой операции и 5.0 после второй операции.

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

Точность типов с плавающей запятой

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

type T is digits <number_of_decimal_digits>;

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

custom_floating_types.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Custom_Floating_Types is

4    type T3  is digits 3;

5    type T15 is digits 15;

6    type T18 is digits 18;

7 begin

8    Put_Line ("T3  requires "

9              & Integer'Image (T3'Size)

10              & " bits");

11    Put_Line ("T15 requires "

12              & Integer'Image (T15'Size)

13              & " bits");

14    Put_Line ("T18 requires "

15              & Integer'Image (T18'Size)

16              & " bits");

17 end Custom_Floating_Types;

Runtime output

T3  requires  32 bits

T15 requires  64 bits

T18 requires  128 bits

В этом примере атрибут «'Size» используется для получения количества бит, используемых для указанного типа данных. Как видно из этого примера, компилятор выделяет 32 бита для T3, 64 бита для T15 и 128 битов для T18. Сюда входят как мантисса, так и экспонента.

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

display_custom_floating_types.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Display_Custom_Floating_Types is

4    type T3  is digits 3;

5    type T18 is digits 18;

6

7    C1 : constant := 1.0e-4;

8

9    A : constant T3  := 1.0 + C1;

10    B : constant T18 := 1.0 + C1;

11 begin

12    Put_Line ("The value of A is "

13              & T3'Image (A));

14    Put_Line ("The value of B is "

15              & T18'Image (B));

16 end Display_Custom_Floating_Types;

Runtime output

The value of A is  1.00E+00

The value of B is  1.00010000000000000E+00

Как и ожидалось, приложение будет отображать переменные в соответствии с заданной точностью (1.00E + 00 и 1.00010000000000000E + 00).

Диапазон значений для типов с плавающей запятой

В дополнение к точности для типа с плавающей запятой можно также задать диапазон. Синтаксис аналогичен записи для целочисленных типов данных — с использованием ключевого слова range. В этом простом примере создается новый тип с плавающей запятой на основе типа Float с диапазоном от -1.0 до 1.0:

floating_point_range.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Floating_Point_Range is

4    type T_Norm  is new Float range -1.0 .. 1.0;

5    A  : T_Norm;

6 begin

7    A := 1.0;

8    Put_Line ("The value of A is "

9              & T_Norm'Image (A));

10 end Floating_Point_Range;

Runtime output

The value of A is  1.00000E+00

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

floating_point_range_exception.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Floating_Point_Range_Exception is

4    type T_Norm  is new Float range -1.0 .. 1.0;

5    A  : T_Norm;

6 begin

7    A := 2.0;

8    Put_Line ("The value of A is "

9              & T_Norm'Image (A));

10 end Floating_Point_Range_Exception;

Build output

floating_point_range_exception.adb:7:09: warning: value not in range of type "T_Norm" defined at line 4 [enabled by default]

floating_point_range_exception.adb:7:09: warning: "Constraint_Error" will be raised at run time [enabled by default]

Runtime output

raised CONSTRAINT_ERROR : floating_point_range_exception.adb:7 range check failed

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

custom_range_types.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics; use Ada.Numerics;

3

4 procedure Custom_Range_Types is

5    type T6_Inv_Trig  is

6      digits 6 range -Pi / 2.0 .. Pi / 2.0;

7 begin

8    null;

9 end Custom_Range_Types;

Build output

custom_range_types.adb:1:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

custom_range_types.adb:1:20: warning: use clause for package "Text_IO" has no effect [-gnatwu]

custom_range_types.adb:5:09: warning: type "T6_Inv_Trig" is not referenced [-gnatwu]

В этом примере мы определяем тип под названием T6_Inv_Trig, который имеет диапазон от −𝜋/2 до 𝜋/2 с минимальной точностью 6 цифр. (Pi определяется в предопределенном пакете Ada.Numerics.)

Строгая типизация

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

illegal_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Illegal_Example is

4    --  Declare two different floating point types

5    type Meters is new Float;

6    type Miles is new Float;

7

8    Dist_Imperial : Miles;

9

10    --  Declare a constant

11    Dist_Metric : constant Meters := 1000.0;

12 begin

13    --  Not correct: types mismatch

14    Dist_Imperial := Dist_Metric * 621.371e-6;

15    Put_Line (Miles'Image (Dist_Imperial));

16 end Illegal_Example;

Build output

illegal_example.adb:14:33: error: expected type "Miles" defined at line 6

illegal_example.adb:14:33: error: found type "Meters" defined at line 5

gprbuild: *** compilation phase failed

Следствием этих правил является то, что в общем случае выражение «смешанного режима» типа 2 * 3.0 инициирует ошибку компиляции. В языке, таком как C или Python, такие выражения допустимы благодаря неявным преобразованиям типов. В Ада такие преобразования должны быть явными:

conv.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 procedure Conv is

3    type Meters is new Float;

4    type Miles is new Float;

5    Dist_Imperial : Miles;

6    Dist_Metric : constant Meters := 1000.0;

7 begin

8    Dist_Imperial := Miles (Dist_Metric) * 621.371e-6;

9    --               ^ Type conversion,

10    --                 from Meters to Miles

11    --  Now the code is correct

12

13    Put_Line (Miles'Image (Dist_Imperial));

14 end Conv;

Runtime output

6.21371E-01

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

conv.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Conv is

4    type Meters is new Float;

5    type Miles is new Float;

6

7    --  Function declaration, like procedure

8    --  but returns a value.

9    function To_Miles (M : Meters) return Miles is

10    --                             ^ Return type

11    begin

12       return Miles (M) * 621.371e-6;

13    end To_Miles;

14

15    Dist_Imperial : Miles;

16    Dist_Metric   : constant Meters := 1000.0;

17 begin

18    Dist_Imperial := To_Miles (Dist_Metric);

19    Put_Line (Miles'Image (Dist_Imperial));

20 end Conv;

Runtime output

6.21371E-01

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

На других языках

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

int a = 3, b = 2;

float f = a / b;

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

int a = 3, b = 2;

float f = (float)a / b;

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

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

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

main.adb

1 procedure Main is

2    A : Integer := 3;

3    B : Integer := 2;

4    F : Float;

5 begin

6    F := A / B;

7 end Main;

Build output

main.adb:6:04: warning: possibly useless assignment to "F", value might not be referenced [-gnatwm]

main.adb:6:11: error: expected type "Standard.Float"

main.adb:6:11: error: found type "Standard.Integer"

gprbuild: *** compilation phase failed

Строка с ошибкой должна быть изменена на F := Float (A) / Float (B);.

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

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

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

main.adb

1 procedure Main is

2    --  ID card number type,

3    --  incompatible with Integer.

4    type Social_Security_Number is new Integer

5      range 0 .. 999_99_9999;

6    --      ^ Since a SSN has 9 digits

7    --        max., and cannot be

8    --        negative, we enforce

9    --        a validity constraint.

10

11    SSN : Social_Security_Number :=

12      555_55_5555;

13    --   ^ You can put underscores as

14    --     formatting in any number.

15

16    I   : Integer;

17

18    --  The value -1 below will cause a

19    --  runtime error and a compile time

20    --  warning with GNAT.

21    Invalid : Social_Security_Number := -1;

22 begin

23    --  Illegal, they have different types:

24    I := SSN;

25

26    --  Likewise illegal:

27    SSN := I;

28

29    --  OK with explicit conversion:

30    I := Integer (SSN);

31

32    --  Likewise OK:

33    SSN := Social_Security_Number (I);

34 end Main;

Build output

main.adb:21:40: warning: value not in range of type "Social_Security_Number" defined at line 4 [enabled by default]

main.adb:21:40: warning: "Constraint_Error" will be raised at run time [enabled by default]

main.adb:24:09: error: expected type "Standard.Integer"

main.adb:24:09: error: found type "Social_Security_Number" defined at line 4

main.adb:27:11: error: expected type "Social_Security_Number" defined at line 4

main.adb:27:11: error: found type "Standard.Integer"

main.adb:33:04: warning: possibly useless assignment to "SSN", value might not be referenced [-gnatwm]

gprbuild: *** compilation phase failed

Тип Social_Security, как говорят, является производным типом; его родительский тип - Integer.

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

Синтаксис перечислений использует синтаксис range <диапазон>:

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7

8    type Weekend_Days is new

9      Days range Saturday .. Sunday;

10    --  New type, where only Saturday and Sunday

11    --  are valid literals.

12 begin

13    null;

14 end Greet;

Build output

greet.adb:1:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

greet.adb:1:19: warning: use clause for package "Text_IO" has no effect [-gnatwu]

greet.adb:4:18: warning: literal "Monday" is not referenced [-gnatwu]

greet.adb:4:26: warning: literal "Tuesday" is not referenced [-gnatwu]

greet.adb:4:35: warning: literal "Wednesday" is not referenced [-gnatwu]

greet.adb:5:18: warning: literal "Thursday" is not referenced [-gnatwu]

greet.adb:5:28: warning: literal "Friday" is not referenced [-gnatwu]

greet.adb:8:09: warning: type "Weekend_Days" is not referenced [-gnatwu]

Подтипы

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7

8    --  Declaration of a subtype

9    subtype Weekend_Days is

10      Days range Saturday .. Sunday;

11    --     ^ Constraint of the subtype

12

13    M : Days := Sunday;

14

15    S : Weekend_Days := M;

16    --  No error here, Days and Weekend_Days

17    --  are of the same type.

18 begin

19    for I in Days loop

20       case I is

21          --  Just like a type, a subtype can

22          --  be used as a range

23          when Weekend_Days =>

24             Put_Line ("Week end!");

25          when others =>

26             Put_Line ("Hello on "

27                       & Days'Image (I));

28       end case;

29    end loop;

30 end Greet;

Build output

greet.adb:13:04: warning: "M" is not modified, could be declared constant [-gnatwk]

greet.adb:15:04: warning: variable "S" is not referenced [-gnatwu]

Runtime output

Hello on MONDAY

Hello on TUESDAY

Hello on WEDNESDAY

Hello on THURSDAY

Hello on FRIDAY

Week end!

Week end!

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

subtype Natural  is Integer range 0 .. Integer'Last;

subtype Positive is Integer range 1 .. Integer'Last;

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7

8    subtype Weekend_Days is

9      Days range Saturday .. Sunday;

10

11    Day     : Days := Saturday;

12    Weekend : Weekend_Days;

13 begin

14    Weekend := Day;

15    --         ^ Correct: Same type, subtype

16    --           constraints are respected

17    Weekend := Monday;

18    --         ^ Wrong value for the subtype

19    --           Compiles, but exception at runtime

20 end Greet;

Build output

greet.adb:1:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

greet.adb:1:19: warning: use clause for package "Text_IO" has no effect [-gnatwu]

greet.adb:4:26: warning: literal "Tuesday" is not referenced [-gnatwu]

greet.adb:4:35: warning: literal "Wednesday" is not referenced [-gnatwu]

greet.adb:5:18: warning: literal "Thursday" is not referenced [-gnatwu]

greet.adb:5:28: warning: literal "Friday" is not referenced [-gnatwu]

greet.adb:11:04: warning: "Day" is not modified, could be declared constant [-gnatwk]

greet.adb:12:04: warning: variable "Weekend" is assigned but never read [-gnatwm]

greet.adb:14:04: warning: useless assignment to "Weekend", value overwritten at line 17 [-gnatwm]

greet.adb:17:04: warning: possibly useless assignment to "Weekend", value might not be referenced [-gnatwm]

greet.adb:17:15: warning: value not in range of type "Weekend_Days" defined at line 8 [enabled by default]

greet.adb:17:15: warning: "Constraint_Error" will be raised at run time [enabled by default]

Runtime output

raised CONSTRAINT_ERROR : greet.adb:17 range check failed

Подтипы в качестве псевдонимов типов

Ранее мы видели, что мы можем создавать новые типы, объявляя type Miles is new Float. Но нам также может потребоваться переименовать тип, просто чтобы ввести альтернативное имя-псевдоним для существующего типа. Следует отметить, что псевдонимы типов иногда называются синонимами типов.

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

undetected_imperial_metric_error.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Undetected_Imperial_Metric_Error is

4    --  Declare two type aliases

5    subtype Meters is Float;

6    subtype Miles is Float;

7

8    Dist_Imperial : Miles;

9

10    --  Declare a constant

11    Dist_Metric : constant Meters := 100.0;

12 begin

13    --  No conversion to Miles type required:

14    Dist_Imperial := (Dist_Metric * 1609.0) / 1000.0;

15

16    --  Not correct, but undetected:

17    Dist_Imperial := Dist_Metric;

18

19    Put_Line (Miles'Image (Dist_Imperial));

20 end Undetected_Imperial_Metric_Error;

Build output

undetected_imperial_metric_error.adb:14:04: warning: useless assignment to "Dist_Imperial", value overwritten at line 17 [-gnatwm]

Runtime output

1.00000E+02

В приведенном выше примере тот факт, что и метры (Meters), и мили (Miles) являются подтипами Float, позволяет нам смешивать переменные обоих типов без преобразования типов. Это, однако, может привести к всевозможным ошибкам в программировании, которых мы стремимся избежать, как можно видеть в необнаруженной ошибке, выделенной в приведенном выше коде. В этом примере ошибка в присвоении значения в метрах переменной, предназначенной для хранения значений в милях, остается необнаруженной, поскольку и метры (Meters), и мили (Miles) являются подтипами Float. Поэтому, для случаев, подобных приведенному выше, рекомендуется использовать строгую типизацию, опеделив тип X производным от типа Y (type X is new Y).

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

Paid_Amount, Due_Amount : Float;

Мы можем написать:

subtype Amount is Float;

Paid, Due : Amount;

На других языках

Например, в C для создания псевдонима типа можно использовать объявление typedef. Например:

typedef float meters;

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

- C++: using meters = float;

- Swift: typealias Meters = Double

- Kotlin: typealias Meters = Double

- Haskell: type Meters = Float

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

subtype Meters is Float range 0.0 .. 1_000_000.0;

Рассмотрим другой пример:

subtype Degree_Celsius is Float;

subtype Liquid_Water_Temperature is

Degree_Celsius range 0.0 .. 100.0;

subtype Running_Water_Temperature is

Liquid_Water_Temperature;

В этом примере Liquid_Water_Temperature не является псевдонимом Degree_Celsius, поскольку добавляет новое ограничение, которое не было частью объявления Degree_Celsius. Однако здесь есть два псевдонима типа:

- Degree_Celsius является псевдонимом Float;

- Running_Water_Temperature является псевдонимом Liquid_Water_Temperature, даже если сам Liquid_Water_Temperature имеет ограниченный диапазон.

Footnotes

#1

https://ru.wikipedia.org/wiki/Кольцевой_буфер

Записи

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

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

Объявление типа записи

Вот пример простого объявления записи:

type Date is record

--  The following declarations are

--  components of the record

Day   : Integer range 1 .. 31;

Month : Months;

--  You can add custom constraints

--  on fields

Year  : Integer range 1 .. 3000;

end record;

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

type Date is record

Day   : Integer range 1 .. 31;

Month : Months := January;

--  This component has a default value

Year  : Integer range 1 .. 3000 := 2012;

--                                 ^ Default value

end record;

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

Агрегаты

Ada_Birthday    : Date := (10, December, 1815);

Leap_Day_2020   : Date := (Day   => 29,

Month => February,

Year  => 2020);

--                         ^ By name

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

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

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

Извлечение компонент

Для доступа к компонентам экземпляра записи используется операция, называемая извлечением компоненты. Она имеет форму точечной нотации. Например, если мы объявляем переменную Some_Day типа записи Date, упомянутого выше, мы можем получить доступ к компоненте Year, написав Some_Day.Year.

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

record_selection.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Record_Selection is

4

5    type Months is

6      (January, February, March, April,

7       May, June, July, August, September,

8       October, November, December);

9

10    type Date is record

11       Day   : Integer range 1 .. 31;

12       Month : Months;

13       Year  : Integer range 1 .. 3000 := 2032;

14    end record;

15

16    procedure Display_Date (D : Date) is

17    begin

18       Put_Line ("Day:" & Integer'Image (D.Day)

19                 & ", Month: "

20                 & Months'Image (D.Month)

21                 & ", Year:"

22                 & Integer'Image (D.Year));

23    end Display_Date;

24

25    Some_Day : Date := (1, January, 2000);

26

27 begin

28    Display_Date (Some_Day);

29

30    Put_Line ("Changing year...");

31    Some_Day.Year := 2001;

32

33    Display_Date (Some_Day);

34 end Record_Selection;

Runtime output

Day: 1, Month: JANUARY, Year: 2000

Changing year...

Day: 1, Month: JANUARY, Year: 2001

Как вы можете видеть в этом примере, мы можем использовать точечную нотацию в выражении D.Year или Some_Day.Year. как для доступа к информации компонеты, так и для ее изменения в операторе присваивания. Говоря конкретнее, когда мы используем D.Year в вызове Put_Line, мы читаем информацию, хранящуюся в этом компоненте. Когда мы пишем Some_Day.Year := 2001, то перезаписываем информацию, которая была ранее сохранена в компоненте Year в переменной Some_Day.

Переименование

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

Мы можем переименовать компоненты записи, используя ключевое слово renames в объявлении переменной. Например:

Some_Day : Date;

Y        : Integer renames Some_Day.Year;

Здесь Y является псевдонимом, так что каждый раз, когда мы используем Y, мы в действительности используем компоненту Year переменной Some_Day.

Давайте рассмотрим полный пример:

dates.ads

1 package Dates is

2

3    type Months is

4      (January, February, March, April,

5       May, June, July, August, September,

6       October, November, December);

7

8    type Date is record

9       Day   : Integer range 1 .. 31;

10       Month : Months;

11       Year  : Integer range 1 .. 3000 := 2032;

12    end record;

13

14    procedure Increase_Month (Some_Day : in out Date);

15

16    procedure Display_Month (Some_Day : Date);

17

18 end Dates;

dates.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Dates is

4

5    procedure Increase_Month (Some_Day : in out Date) is

6       --  Renaming components from

7       --  the Date record

8       M : Months  renames Some_Day.Month;

9       Y : Integer renames Some_Day.Year;

10

11       --  Renaming function (for Months

12       --  enumeration)

13       function Next (M : Months) return Months

14         renames Months'Succ;

15    begin

16       if M = December then

17          M := January;

18          Y := Y + 1;

19       else

20          M := Next (M);

21       end if;

22    end Increase_Month;

23

24    procedure Display_Month (Some_Day : Date) is

25       --  Renaming components from

26       --  the Date record

27       M : Months  renames Some_Day.Month;

28       Y : Integer renames Some_Day.Year;

29    begin

30       Put_Line ("Month: "

31                 & Months'Image (M)

32                 & ", Year:"

33                 & Integer'Image (Y));

34    end Display_Month;

35

36 end Dates;

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Dates;       use Dates;

3

4 procedure Main is

5    D : Date := (1, January, 2000);

6 begin

7    Display_Month (D);

8

9    Put_Line ("Increasing month...");

10    Increase_Month (D);

11

12    Display_Month (D);

13 end Main;

Runtime output

Month: JANUARY, Year: 2000

Increasing month...

Month: FEBRUARY, Year: 2000

Мы применяем переименование к двум компонентам записи Date в реализации процедуры Increase_Month. Затем вместо непосредственного использования Some_Day.Month и Some_Day.Year в последующих операциях мы просто используем переименованные версии M и Y.

Обратите внимание, что в приведенном выше примере мы также переименовали Months'Succ - функцию, которая дает нам следующий месяц - в Next.

Footnotes

Массивы

Массивы (или индексируемые типы) предоставляют еще одно фундаментальное семейство составных типов в Аде.

Объявление типа массива

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type My_Int is range 0 .. 1000;

5    type Index is range 1 .. 5;

6

7    type My_Int_Array is

8      array (Index) of My_Int;

9    --                 ^ Type of elements

10    --       ^ Bounds of the array

11    Arr : My_Int_Array := (2, 3, 5, 7, 11);

12    --                    ^ Array literal

13    --                      (aggregate)

14

15    V : My_Int;

16 begin

17    for I in Index loop

18       V := Arr (I);

19       --        ^ Take the Ith element

20       Put (My_Int'Image (V));

21    end loop;

22    New_Line;

23 end Greet;

Build output

greet.adb:11:04: warning: "Arr" is not modified, could be declared constant [-gnatwk]

Runtime output

2 3 5 7 11

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

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

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

Таким образом, когда вы видите выражение, такое как A (B), является ли оно вызовом функции или индексом массива, зависит от того, на что ссылается A.

Наконец, обратите внимание, как мы инициализируем массив выражением (2, 3, 5, 7, 11). Это еще один вид агрегата в Аде, который в некотором смысле является литералом для массива, точно так же, как 3 является литералом для целого числа. Обозначение очень мощное, с рядом свойств, которые мы представим позже. Подробный обзор приводится в главе о агрегатах типов.

Пример также иллюстрирует две процедуры из:ada:Ada.Text_IO, которые не связанны с массивами:

- Put - выводит строку без завершающего конца строки.

- New_Line - вывод конца строки

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

На других языках

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

array_bounds_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Array_Bounds_Example is

4    type My_Int is range 0 .. 1000;

5    type Index is range 11 .. 15;

6    --                  ^ Low bound can be any value

7    type My_Int_Array is array (Index) of My_Int;

8    Tab : constant My_Int_Array := (2, 3, 5, 7, 11);

9 begin

10    for I in Index loop

11       Put (My_Int'Image (Tab (I)));

12    end loop;

13    New_Line;

14 end Array_Bounds_Example;

Runtime output

2 3 5 7 11

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

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

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

for I in 11 .. 15 loop

Tab (I) := Tab (I) * 2;

end loop;

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

month_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Month_Example is

4    type Month_Duration is range 1 .. 31;

5    type Month is (Jan, Feb, Mar, Apr,

6                   May, Jun, Jul, Aug,

7                   Sep, Oct, Nov, Dec);

8

9    type My_Int_Array is

10      array (Month) of Month_Duration;

11    --       ^ Can use an enumeration type

12    --         as the index

13

14    Tab : constant My_Int_Array :=

15    --    ^ constant is like a variable but

16    --      cannot be modified

17      (31, 28, 31, 30, 31, 30,

18       31, 31, 30, 31, 30, 31);

19    --  Maps months to number of days

20    --  (ignoring leap years)

21

22    Feb_Days : Month_Duration := Tab (Feb);

23    --  Number of days in February

24 begin

25    for M in Month loop

26       Put_Line

27         (Month'Image (M) & " has "

28          & Month_Duration'Image (Tab (M))

29          & " days.");

30    --    ^ Concatenation operator

31    end loop;

32 end Month_Example;

Build output

month_example.adb:22:04: warning: variable "Feb_Days" is not referenced [-gnatwu]

Runtime output

JAN has  31 days.

FEB has  28 days.

MAR has  31 days.

APR has  30 days.

MAY has  31 days.

JUN has  30 days.

JUL has  31 days.

AUG has  31 days.

SEP has  30 days.

OCT has  31 days.

NOV has  30 days.

DEC has  31 days.

В приведенном выше примере мы:

- Создание типа массива для отображения месяца в его продолжительность в днях.

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

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

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

Доступ по индексу

Мы уже видели синтаксис обращения к элементам массива. Однако следует отметить еще несколько моментов.

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type My_Int is range 0 .. 1000;

5

6    type My_Index   is range 1 .. 5;

7    type Your_Index is range 1 .. 5;

8

9    type My_Int_Array is array (My_Index) of My_Int;

10    Tab : My_Int_Array := (2, 3, 5, 7, 11);

11 begin

12    for I in Your_Index loop

13       Put (My_Int'Image (Tab (I)));

14    --                         ^ Compile time error

15    end loop;

16    New_Line;

17 end Greet;

Build output

greet.adb:13:31: error: expected type "My_Index" defined at line 6

greet.adb:13:31: error: found type "Your_Index" defined at line 7

gprbuild: *** compilation phase failed

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    type My_Int is range 0 .. 1000;

5    type Index is range 1 .. 5;

6    type My_Int_Array is array (Index) of My_Int;

7    Tab : My_Int_Array := (2, 3, 5, 7, 11);

8 begin

9    for I in Index range 2 .. 6 loop

10       Put (My_Int'Image (Tab (I)));

11       --                      ^ Will raise an

12       --                        exception when

13       --                        I = 6

14    end loop;

15    New_Line;

16 end Greet;

Build output

greet.adb:7:04: warning: "Tab" is not modified, could be declared constant [-gnatwk]

greet.adb:9:30: warning: static value out of range of type "Index" defined at line 5 [enabled by default]

greet.adb:9:30: warning: "Constraint_Error" will be raised at run time [enabled by default]

greet.adb:9:30: warning: suspicious loop bound out of range of loop subtype [enabled by default]

greet.adb:9:30: warning: loop executes zero times or raises Constraint_Error [enabled by default]

Runtime output

raised CONSTRAINT_ERROR : greet.adb:9 range check failed

Более простые объявления массива

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

simple_array_bounds.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Simple_Array_Bounds is

4    type My_Int is range 0 .. 1000;

5    type My_Int_Array is array (1 .. 5) of My_Int;

6    --                          ^ Subtype of Integer

7    Tab : constant My_Int_Array := (2, 3, 5, 7, 11);

8 begin

9    for I in 1 .. 5 loop

10    --       ^ Subtype of Integer

11       Put (My_Int'Image (Tab (I)));

12    end loop;

13    New_Line;

14 end Simple_Array_Bounds;

Runtime output

2 3 5 7 11

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

Это означает, что индекс имеет целочисленный тип Integer. Точно так же, когда вы используете анонимный диапазон в цикле for, как в приведенном выше примере, тип параметра цикла также является Integer, поэтому вы можете использовать I для индексации Tab.

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

Атрибут диапазона

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

range_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Range_Example is

4    type My_Int is range 0 .. 1000;

5    type My_Int_Array is array (1 .. 5) of My_Int;

6    Tab : constant My_Int_Array := (2, 3, 5, 7, 11);

7 begin

8    for I in Tab'Range loop

9    --          ^ Gets the range of Tab

10       Put (My_Int'Image (Tab (I)));

11    end loop;

12    New_Line;

13 end Range_Example;

Runtime output

2 3 5 7 11

Если требуется более точный контроль, можно использовать отдельные атрибуты 'First («Первый») и 'Last («Последний»).

array_attributes_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Array_Attributes_Example is

4    type My_Int is range 0 .. 1000;

5    type My_Int_Array is array (1 .. 5) of My_Int;

6    Tab : My_Int_Array := (2, 3, 5, 7, 11);

7 begin

8    for I in Tab'First .. Tab'Last - 1 loop

9    --          ^ Iterate on every index

10    --            except the last

11       Put (My_Int'Image (Tab (I)));

12    end loop;

13    New_Line;

14 end Array_Attributes_Example;

Build output

array_attributes_example.adb:6:04: warning: "Tab" is not modified, could be declared constant [-gnatwk]

Runtime output

2 3 5 7

Атрибуты 'Range, 'First и 'Last показанные в этих примерах также можно применять к имени типа массива, а не только к экземплярам массива.

Хотя это не демонстрируется в вышеприведенных примерах, другим полезным атрибутом для экземпляра массива A является A'Length, который возвращает количеством элементов в A.

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

Неограниченные массивы

Давайте теперь рассмотрим одину из самых мощных возможностей массивов в языке Ада.

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

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

unconstrained_array_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Unconstrained_Array_Example is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7

8    type Workload_Type is

9      array (Days range <>) of Natural;

10    --  Indefinite array type

11    --       ^ Bounds are of type Days,

12    --         but not known

13

14    Workload : constant

15      Workload_Type (Monday .. Friday) :=

16    --               ^ Specify the bounds

17    --                 when declaring

18       (Friday => 7, others => 8);

19    --               ^ Default value

20    --  ^ Specify element by name of index

21 begin

22    for I in Workload'Range loop

23       Put_Line (Integer'Image (Workload (I)));

24    end loop;

25 end Unconstrained_Array_Example;

Build output

unconstrained_array_example.adb:4:26: warning: literal "Tuesday" is not referenced [-gnatwu]

unconstrained_array_example.adb:4:35: warning: literal "Wednesday" is not referenced [-gnatwu]

unconstrained_array_example.adb:5:18: warning: literal "Thursday" is not referenced [-gnatwu]

unconstrained_array_example.adb:6:18: warning: literal "Saturday" is not referenced [-gnatwu]

unconstrained_array_example.adb:6:28: warning: literal "Sunday" is not referenced [-gnatwu]

Runtime output

8

8

8

8

7

Тот факт, что границы массива неизвестны, указывается синтаксисом Days range <>. Взяв, для примера дискретный тип Discrete_Type, eсли бы мы указали просто Discrete_Type как индекс массива, то для каждого значения из Discrete_Type существовал бы индекс и соответствующий элемент в каждом экземпляре массива.

Но, если мы определим индекс как Discrete_Type range <>, то, хотя Discrete_Type все еще будет типом индекса, но разные экземпляры массива смогут иметь свои границы.

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

В приведенном выше примере также показаны другие формы записи агрегата. Вы можете использовать именованное сопоставление, задав значение индекса слева от стрелки. Таким образом, 1 => 2 означает «присвоить значение 2 элементу с индексом 1 в моем массиве». Запись others => 8 означает «присвоить значение 8 каждому элементу, который ранее не был назначен в этом агрегате».

Attention

Так называемый «бокс» (<>) в Аде обычно используется для обозначение места, где отсутствует некий элемент. Как мы увидим еще не раз, такое обозначение можно читать, как «значение, не заданое явно».

На других языках

В то время как неограниченные массивы в Аде могут казаться похожими на массивы переменной длины в C, в действительности они гораздо более мощные, поскольку работают как настоящие значения. Их можно передавать их как параметры при вызове подпрограммам и возвращать как результат функции, и они неявно содержат границы как часть своего значения. Это означает, что нет нужды явно передавать границы или длину массива вместе с массивом, поскольку эти значения доступны через атрибуты 'First, 'Last, 'Range и 'Length, описанные ранее.

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

Предопределенный тип String

В нашем введении в типы языка Ада было отмечено, что такие важные встроенные типы, как Boolean или Integer, определяются с помощью тех же средств, что доступны пользователю. Это также верно и для строк: тип String в Аде является простым массивом.

Вот как тип строки определяется в Аде:

type String is array (Positive range <>) of Character;

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

Hint

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

string_literals.ads

1 package String_Literals is

2     --  Those two declarations are equivalent

3     A : String (1 .. 11) := "Hello World";

4     B : String (1 .. 11) :=

5         ('H', 'e', 'l', 'l', 'o', ' ',

6          'W', 'o', 'r', 'l', 'd');

7 end String_Literals;

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    Message : String (1 .. 11) := "dlroW olleH";

5    --        ^ Pre-defined array type.

6    --          Component type is Character

7 begin

8    for I in reverse Message'Range loop

9       --    ^ Iterate in reverse order

10       Put (Message (I));

11    end loop;

12    New_Line;

13 end Greet;

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

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

greet.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Greet is

4    Message : constant String := "dlroW olleH";

5    --                 ^ Bounds are automatically

6    --                   computed from

7    --                   initialization value

8 begin

9    for I in reverse Message'Range loop

10       Put (Message (I));

11    end loop;

12    New_Line;

13 end Greet;

Runtime output

Hello World

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

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

5

6    My_Array : constant Integer_Array := (1, 2, 3, 4);

7    --                  ^ Bounds are automatically

8    --                    computed from

9    --                    initialization value

10 begin

11     null;

12 end Main;

Attention

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

Если вам нужно что-то вроде :c++:std::string в C++, вы можете использовать Unbounded Strings из стандартной библиотеки Ада. Этот тип больше похож на изменяемый, автоматически управляемый строковый буфер, в который вы можете добавлять содержимое.

Ограничения

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

declare

A : String;

begin

A := "World";

end;

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

declare

A : String := "Hello";

begin

A := "World";       --  OK: Same size

A := "Hello World"; --  Not OK: Different size

end;

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

Обратите внимание

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

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

indefinite_subtypes.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Indefinite_Subtypes is

4     function Get_Number return Integer is

5     begin

6         return Integer'Value (Get_Line);

7     end Get_Number;

8

9    A : String := "Hello";

10    --  Indefinite subtype

11

12    B : String (1 .. 5) := "Hello";

13    --  Definite subtype

14

15    C : String (1 .. Get_Number);

16    --  Indefinite subtype

17    --  (Get_Number's value is computed at

18    --  run-time)

19 begin

20    null;

21 end Indefinite_Subtypes;

Возврат неограниченных массивов

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

Например, это функция, которая возвращает неограниченную строку:

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4

5    type Days is (Monday, Tuesday, Wednesday,

6                  Thursday, Friday,

7                  Saturday, Sunday);

8

9    function Get_Day_Name (Day : Days := Monday)

10                           return String is

11    begin

12       return

13         (case Day is

14          when Monday    => "Monday",

15          when Tuesday   => "Tuesday",

16          when Wednesday => "Wednesday",

17          when Thursday  => "Thursday",

18          when Friday    => "Friday",

19          when Saturday  => "Saturday",

20          when Sunday    => "Sunday");

21    end Get_Day_Name;

22

23 begin

24    Put_Line ("First day is "

25              & Get_Day_Name (Days'First));

26 end Main;

Runtime output

First day is Monday

Примечание. Этот пример приведен только в иллюстративных целях. Существует встроенный механизм, атрибут 'Image для скалярных типов, который возвращает имя (в виде строки – типа String) любого элемента типа перечисления. Например, Days'Image(Monday) есть "MONDAY".)

На других языках

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

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

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

Объявление массивов (2)

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

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

show_days.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Days is

4    type Days is (Monday, Tuesday, Wednesday,

5                  Thursday, Friday,

6                  Saturday, Sunday);

7

8    subtype Day_Name is String (1 .. 2);

9    --  Subtype of string with known size

10

11    type Days_Name_Type is

12      array (Days) of Day_Name;

13    --       ^ Type of the index

14    --                ^ Type of the element.

15    --                  Must be definite

16

17    Names : constant Days_Name_Type :=

18      ("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su");

19    --  Initial value given by aggregate

20 begin

21    for I in Names'Range loop

22       Put_Line (Names (I));

23    end loop;

24 end Show_Days;

Build output

show_days.adb:4:18: warning: literal "Monday" is not referenced [-gnatwu]

show_days.adb:4:26: warning: literal "Tuesday" is not referenced [-gnatwu]

show_days.adb:4:35: warning: literal "Wednesday" is not referenced [-gnatwu]

show_days.adb:5:18: warning: literal "Thursday" is not referenced [-gnatwu]

show_days.adb:5:28: warning: literal "Friday" is not referenced [-gnatwu]

show_days.adb:6:18: warning: literal "Saturday" is not referenced [-gnatwu]

show_days.adb:6:28: warning: literal "Sunday" is not referenced [-gnatwu]

Runtime output

Mo

Tu

We

Th

Fr

Sa

Su

Отрезки массива

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

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Main is

4     Buf : String := "Hello ...";

5

6     Full_Name : String := "John Smith";

7 begin

8     Buf (7 .. 9) := "Bob";

9     --  Careful! This works because the string

10     --  on the right side is the same length as

11     --  the replaced slice!

12

13     --  Prints "Hello Bob"

14     Put_Line (Buf);

15

16      --  Prints "Hi John"

17     Put_Line ("Hi " & Full_Name (1 .. 4));

18 end Main;

Build output

main.adb:6:05: warning: "Full_Name" is not modified, could be declared constant [-gnatwk]

Runtime output

Hello Bob

Hi John

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

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

Attention

В Ада есть многомерные массивы#1, которые не рассматриваются в этом курсе. Отрезки будут работать только с одномерными массивами.

Переименование

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

measurements.ads

1 package Measurements is

2

3    subtype Degree_Celsius is Float;

4

5    Current_Temperature : Degree_Celsius;

6

7 end Measurements;

main.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Measurements;

3

4 procedure Main is

5    subtype Degrees is Measurements.Degree_Celsius;

6

7    T : Degrees

8      renames Measurements.Current_Temperature;

9 begin

10     T := 5.0;

11

12     Put_Line (Degrees'Image (T));

13     Put_Line (Degrees'Image

14       (Measurements.Current_Temperature));

15

16     T := T + 2.5;

17

18     Put_Line (Degrees'Image (T));

19     Put_Line (Degrees'Image

20       (Measurements.Current_Temperature));

21 end Main;

Runtime output

5.00000E+00

5.00000E+00

7.50000E+00

7.50000E+00

В приведенном выше примере мы объявляем переменную T, переименовывая объект Current_Temperature из пакета Measurements. Как вы можете видеть, запустив этот пример, и Current_Temperature, и его альтернативное имя T имеют одинаковые значения:

- сначала они показывают значение 5.0

- после сложения они показывают значение 7.5.

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

Обратите внимание, что в приведенном выше примере мы используем Degrees как псевдоним Degree_Celsius. Мы обсуждали этот метод переименования ранее в курсе.

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

colors.ads

1 package Colors is

2

3    type Color is (Black, Red, Green, Blue, White);

4

5    type Color_Array is

6      array (Positive range <>) of Color;

7

8    procedure Reverse_It (X : in out Color_Array);

9

10 end Colors;

colors.adb

1 package body Colors is

2

3    procedure Reverse_It (X : in out Color_Array) is

4    begin

5       for I in X'First .. (X'Last + X'First) / 2 loop

6          declare

7             Tmp     : Color;

8             X_Left  : Color

9               renames X (I);

10             X_Right : Color

11               renames X (X'Last + X'First - I);

12          begin

13             Tmp     := X_Left;

14             X_Left  := X_Right;

15             X_Right := Tmp;

16          end;

17       end loop;

18    end Reverse_It;

19

20 end Colors;

test_reverse_colors.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 with Colors; use Colors;

4

5 procedure Test_Reverse_Colors is

6

7    My_Colors : Color_Array (1 .. 5) :=

8      (Black, Red, Green, Blue, White);

9

10 begin

11    for C of My_Colors loop

12       Put_Line ("My_Color: " & Color'Image (C));

13    end loop;

14

15    New_Line;

16    Put_Line ("Reversing My_Color...");

17    New_Line;

18    Reverse_It (My_Colors);

19

20    for C of My_Colors loop

21       Put_Line ("My_Color: " & Color'Image (C));

22    end loop;

23

24 end Test_Reverse_Colors;

Runtime output

My_Color: BLACK

My_Color: RED

My_Color: GREEN

My_Color: BLUE

My_Color: WHITE

Reversing My_Color...

My_Color: WHITE

My_Color: BLUE

My_Color: GREEN

My_Color: RED

My_Color: BLACK

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

begin

Tmp     := X_Left;

X_Left  := X_Right;

X_Right := Tmp;

end;

Сравните это с альтернативной версией без переименования:

begin

Tmp                      := X (I);

X (I)                    := X (X'Last + X'First - I);

X (X'Last + X'First - I) := Tmp;

end;

Footnotes

#1

http://www.adaic.org/resources/add_content/standards/12rm/html/RM-3-6.html

Подробнее о типах

Агрегаты: краткая информация

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

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

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

Это означает, что следующий код неверен:

incorrect.ads

1 package Incorrect is

2    type Point is record

3       X, Y : Integer := 0;

4    end record;

5

6    Origin : Point := (X => 0);

7 end Incorrect;

Build output

incorrect.ads:6:22: error: no value supplied for component "Y"

gprbuild: *** compilation phase failed

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

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

- Символ | можно использовать для присвоения нескольким компонентам одинакового значения.

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

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

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

points.ads

1 package Points is

2    type Point is record

3       X, Y : Integer := 0;

4    end record;

5

6    type Point_Array is

7      array (Positive range <>) of Point;

8

9    --  use the default values

10    Origin   : Point := (X | Y => <>);

11

12    --  likewise, use the defaults

13    Origin_2 : Point := (others => <>);

14

15    Points_1 : Point_Array := ((1, 2), (3, 4));

16    Points_2 : Point_Array := (1       => (1, 2),

17                               2       => (3, 4),

18                               3 .. 20 => <>);

19 end Points;

Cовмещение и квалифицированные выражения

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

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

pkg.ads

1 package Pkg is

2    function F (A : Integer) return Integer;

3    function F (A : Character) return Integer;

4 end Pkg;

Это распространенное понятие в языках программирования, называемое совмещением, или перегрузкой имен#1.

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

pkg.ads

1 package Pkg is

2    type SSID is new Integer;

3

4    function Convert (Self : SSID) return Integer;

5    function Convert (Self : SSID) return String;

6 end Pkg;

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Pkg;         use Pkg;

3

4 procedure Main is

5    S : String := Convert (123_145_299);

6    --            ^ Valid, will choose the

7    --              proper Convert

8 begin

9    Put_Line (S);

10 end Main;

Attention

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

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

pkg.ads

1 package Pkg is

2    type SSID is new Integer;

3

4    function Convert (Self : SSID) return Integer;

5    function Convert (Self : SSID) return String;

6    function Convert (Self : Integer) return String;

7 end Pkg;

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Pkg;         use Pkg;

3

4 procedure Main is

5    S : String := Convert (123_145_299);

6    --            ^ Invalid, which convert

7    --              should we call?

8

9    S2 : String := Convert (SSID'(123_145_299));

10    --                     ^ We specify that the

11    --                       type of the expression

12    --                       is SSID.

13

14    --  We could also have declared a temporary

15

16    I : SSID := 123_145_299;

17

18    S3 : String := Convert (I);

19 begin

20    Put_Line (S);

21 end Main;

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

qual_expr.ads

1 package Qual_Expr is

2    type Point is record

3       A, B : Integer;

4    end record;

5

6    P : Point := Point'(12, 15);

7

8    A : Integer := Integer'(12);

9 end Qual_Expr;

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

Attention

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

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

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

X : Integer := Natural'(1);

Символьные типы

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

character_example.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Character_Example is

4    type My_Char is ('a', 'b', 'c');

5    --  Our custom character type, an

6    --  enumeration type with 3 valid values.

7

8    C : Character;

9    --  ^ Built-in character type

10    --    (it's an enumeration type)

11

12    M : My_Char;

13 begin

14    C := '?';

15    --   ^ Character literal

16    --     (enumeration literal)

17

18    M := 'a';

19

20    C := 65;

21    --   ^ Invalid: 65 is not a

22    --     Character value

23

24    C := Character'Val (65);

25    --  Assign the character at

26    --  position 65 in the

27    --  enumeration (which is 'A')

28

29    M := C;

30    --   ^ Invalid: C is of type Character,

31    --     and M is a My_Char

32

33    M := 'd';

34    --   ^ Invalid: 'd' is not a valid

35    --     literal for type My_Char

36 end Character_Example;

Build output

character_example.adb:14:04: warning: useless assignment to "C", value overwritten at line 20 [-gnatwm]

character_example.adb:18:04: warning: useless assignment to "M", value overwritten at line 29 [-gnatwm]

character_example.adb:20:04: warning: useless assignment to "C", value overwritten at line 24 [-gnatwm]

character_example.adb:20:09: error: expected type "Standard.Character"

character_example.adb:20:09: error: found type universal integer

character_example.adb:29:04: warning: useless assignment to "M", value overwritten at line 33 [-gnatwm]

character_example.adb:29:09: error: expected type "My_Char" defined at line 4

character_example.adb:29:09: error: found type "Standard.Character"

character_example.adb:33:04: warning: possibly useless assignment to "M", value might not be referenced [-gnatwm]

character_example.adb:33:09: error: character not defined for type "My_Char" defined at line 4

gprbuild: *** compilation phase failed

Footnotes

#1

https://ru.m.wikipedia.org/wiki/Перегрузка_процедур_и_функций

Ссылочные типы (указатели)

Введение

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

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

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

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

Вот как в Аде объявляется простой тип указателя, ссылочный тип:

dates.ads

1 package Dates is

2    type Months is

3      (January, February, March, April,

4       May, June, July, August, September,

5       October, November, December);

6

7    type Date is record

8       Day   : Integer range 1 .. 31;

9       Month : Months;

10       Year  : Integer;

11    end record;

12 end Dates;

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4     --  Declare an access type

5     type Date_Acc is access Date;

6     --                      ^ "Designated type"

7     --                      ^ Date_Acc values point

8     --                        to Date objects

9

10     D : Date_Acc := null;

11     --              ^ Literal for

12     --                "access to nothing"

13     --  ^ Access to date

14 end Access_Types;

На этом примере показано, как:

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

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

- Присвоить ему значение null

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

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4     --  Declare an access type

5     type Date_Acc   is access Date;

6     type Date_Acc_2 is access Date;

7

8     D  : Date_Acc   := null;

9     D2 : Date_Acc_2 := D;

10     --                 ^ Invalid! Different types

11 end Access_Types;

Build output

access_types.ads:9:24: error: expected type "Date_Acc_2" defined at line 6

access_types.ads:9:24: error: found type "Date_Acc" defined at line 5

gprbuild: *** compilation phase failed

На других языках

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

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

package Access_Types is

type Point is record

X, Y : Natural;

end record;

type Point_Access is access Point;

end Access_Types;

Выделение (allocation) памяти

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

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4     type Date_Acc is access Date;

5

6     D : Date_Acc := new Date;

7     --              ^ Allocate a new Date record

8 end Access_Types;

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

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4    type String_Acc is access String;

5    --                        ^

6    --  Access to unconstrained array type

7    Msg : String_Acc;

8    --    ^ Default value is null

9

10    Buffer : String_Acc :=

11      new String (1 .. 10);

12    --            ^ Constraint required

13 end Access_Types;

Build output

access_types.ads:1:06: warning: no entities of "Dates" are referenced [-gnatwu]

access_types.ads:1:13: warning: use clause for package "Dates" has no effect [-gnatwu]

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

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4    type Date_Acc is access Date;

5    type String_Acc is access String;

6

7    D   : Date_Acc   := new Date'(30, November, 2011);

8    Msg : String_Acc := new String'("Hello");

9 end Access_Types;

Извлечение по ссылке

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

access_types.ads

1 with Dates; use Dates;

2

3 package Access_Types is

4    type Date_Acc is access Date;

5

6    D     : Date_Acc := new Date'(30, November, 2011);

7

8    Today : Date := D.all;

9    --              ^ Access value dereference

10    J     : Integer := D.Day;

11    --                 ^ Implicit dereference for

12    --                   record and array components

13    --                   Equivalent to D.all.day

14 end Access_Types;

Другие особенности

Как вы, возможно, заметили, если пользовались указатели в C или C++, мы не показали некоторых функций, которые считаются основополагающими при использования указателей, таких как:

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

- Освобождение памяти вручную - то, что в C делается с помощью free или delete. Это потенциально опасная операция. Чтобы оставаться в безопасном пространстве Ады, вам лучше не освобождать память вручную.

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

Attention

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

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

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

2. Контейнер, который следует отметить в этом контексте, является Indefinite holder#1. Этот контейнер позволяет хранить значение неопределенного типа, например String.

3. GNATCOLL🤢 имеет библиотеку для интеллектуальных указателей, называемую Refcount#2, память этих указателей автоматически управляется, так что, когда у выделенного объекта больше нет ссылок на него, память автоматически освобождается.

Взаимно рекурсивные типы

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

simple_list.ads

1 package Simple_List is

2    type Node;

3    --  This is an incomplete type declaration,

4    --  which is completed in the same

5    --  declarative region.

6

7    type Node_Acc is access Node;

8

9    type Node is record

10       Content    : Natural;

11       Prev, Next : Node_Acc;

12    end record;

13 end Simple_List;

Footnotes

#1

http://www.ada-auth.org/standards/12rat/html/Rat12-8-5.html

#2

https://github.com/AdaCore/gnatcoll-core/blob/master/src/gnatcoll-refcount.ads

Подробнее о записях

Типы записей динамически изменяемого размера

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

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

runtime_length.ads

1 package Runtime_Length is

2    function Compute_Max_Len return Natural;

3 end Runtime_Length;

var_size_record.ads

1 with Runtime_Length; use Runtime_Length;

2

3 package Var_Size_Record is

4     Max_Len : constant Natural

5       := Compute_Max_Len;

6     --   ^ Not known at compile time

7

8     type Items_Array is array (Positive range <>)

9       of Integer;

10

11     type Growable_Stack is record

12        Items : Items_Array (1 .. Max_Len);

13        Len   : Natural;

14     end record;

15     --  Growable_Stack is a definite type, but

16     --  size is not known at compile time.

17

18     G : Growable_Stack;

19 end Var_Size_Record;

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

Записи с дискриминантом

В приведенном выше примере размер поля Items определяется один раз во время выполнения, но размер всех экземпляров Growable_Stack будет совпадать. И, возможно, это не то, что вы хотите получить. Мы видели, что массивы в целом имеют такую гибкость: для неограниченного типа массива разные объекты могут иметь разные размеры.

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

var_size_record_2.ads

1 package Var_Size_Record_2 is

2     type Items_Array is array (Positive range <>)

3       of Integer;

4

5     type Growable_Stack (Max_Len : Natural) is

6     record

7     --                   ^ Discriminant. Cannot be

8     --                     modified once initialized.

9        Items : Items_Array (1 .. Max_Len);

10        Len   : Natural := 0;

11     end record;

12     --  Growable_Stack is an indefinite type

13     --  (like an array)

14 end Var_Size_Record_2;

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

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

test_discriminants.ads

1 package Test_Discriminants is

2    type Point (X, Y : Natural) is record

3       null;

4    end record;

5

6    P : Point;

7    --  ERROR: Point is indefinite, so you

8    --  need to specify the discriminants

9    --  or give a default value

10

11    P2 : Point (1, 2);

12    P3 : Point := (1, 2);

13    --  Those two declarations are equivalent.

14

15 end Test_Discriminants;

Build output

test_discriminants.ads:6:08: error: unconstrained subtype not allowed (need initialization)

test_discriminants.ads:6:08: error: provide initial value or explicit discriminant values

test_discriminants.ads:6:08: error: or give default discriminant values for type "Point"

gprbuild: *** compilation phase failed

Это также означает, что в приведенном выше примере вы не можете объявить массив значений Point, потому что размер Point неизвестен.

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

test_discriminants.ads

1 package Test_Discriminants is

2    type Point (X, Y : Natural := 0) is record

3       null;

4    end record;

5

6    P : Point;

7    --  We can now simply declare a "Point"

8    --  without further ado. In this case,

9    --  we're using the default values (0)

10    --  for X and Y.

11

12    P2 : Point (1, 2);

13    P3 : Point := (1, 2);

14    --  We can still specify discriminants.

15

16 end Test_Discriminants;

Также обратите внимание, что, хотя тип Point теперь имеет дискриминанты по умолчанию, это не мешает нам указывать дискриминанты, как мы это делаем в объявлениях P2 и P3.

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

main.adb

1 with Var_Size_Record_2; use Var_Size_Record_2;

2 with Ada.Text_IO; use Ada.Text_IO;

3

4 procedure Main is

5    procedure Print_Stack (G : Growable_Stack) is

6    begin

7       Put ("<Stack, items: [");

8       for I in G.Items'Range loop

9          exit when I > G.Len;

10          Put (" " & Integer'Image (G.Items (I)));

11       end loop;

12       Put_Line ("]>");

13    end Print_Stack;

14

15    S : Growable_Stack :=

16      (Max_Len => 128,

17       Items   => (1, 2, 3, 4, others => <>),

18       Len     => 4);

19 begin

20    Print_Stack (S);

21 end Main;

Build output

main.adb:15:04: warning: "S" is not modified, could be declared constant [-gnatwk]

Runtime output

<Stack, items: [  1  2  3  4]>

Note

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

Записи c вариантами

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

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

variant_record.ads

1 package Variant_Record is

2    --  Forward declaration of Expr

3    type Expr;

4

5    --  Access to a Expr

6    type Expr_Access is access Expr;

7

8    type Expr_Kind_Type is (Bin_Op_Plus,

9                            Bin_Op_Minus,

10                            Num);

11    --  A regular enumeration type

12

13    type Expr (Kind : Expr_Kind_Type) is record

14       --      ^ The discriminant is an

15       --        enumeration value

16       case Kind is

17          when Bin_Op_Plus | Bin_Op_Minus =>

18             Left, Right : Expr_Access;

19          when Num =>

20             Val : Integer;

21       end case;

22       --  Variant part. Only one, at the end of

23       --  the record definition, but can be

24       --  nested

25    end record;

26 end Variant_Record;

Поля, которые находятся в варианте when, будут доступны только тогда, когда значение дискриминанта совпадает с указанным. В приведенном выше примере вы сможете обращаться к полям Left и Right, только если Kind равен Bin_Op_Plus или Bin_Op_Minus.

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

main.adb

1 with Variant_Record; use Variant_Record;

2

3 procedure Main is

4    E : Expr := (Num, 12);

5 begin

6    E.Left := new Expr'(Num, 15);

7    --  Will compile but fail at runtime

8 end Main;

Build output

main.adb:4:04: warning: variable "E" is not referenced [-gnatwu]

main.adb:6:05: warning: component not present in subtype of "Expr" defined at line 4 [enabled by default]

main.adb:6:05: warning: "Constraint_Error" will be raised at run time [enabled by default]

Runtime output

raised CONSTRAINT_ERROR : main.adb:6 discriminant check failed

А вот как можно написать вычислитель выражений:

main.adb

1 with Variant_Record; use Variant_Record;

2 with Ada.Text_IO; use Ada.Text_IO;

3

4 procedure Main is

5    function Eval_Expr (E : Expr) return Integer is

6      (case E.Kind is

7       when Bin_Op_Plus  => Eval_Expr (E.Left.all)

8                            + Eval_Expr (E.Right.all),

9       when Bin_Op_Minus => Eval_Expr (E.Left.all)

10                            - Eval_Expr (E.Right.all),

11       when Num => E.Val);

12

13    E : Expr := (Bin_Op_Plus,

14                 new Expr'(Bin_Op_Minus,

15                           new Expr'(Num, 12),

16                           new Expr'(Num, 15)),

17                 new Expr'(Num, 3));

18 begin

19    Put_Line (Integer'Image (Eval_Expr (E)));

20 end Main;

Build output

main.adb:13:04: warning: "E" is not modified, could be declared constant [-gnatwk]

Runtime output

0

На других языках

Записи вариантов Аде очень похожи на Sum типы в функциональных языках, таких как OCaml или Haskell. Основное отличие состоит в том, что дискриминант является отдельным полем в Аде, тогда как «тег» Sum типа является встроенным и доступен только при сопаставлении шаблонов.

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

По сравнению с объединениями C/C++ записи с вариантами Аде более мощны, а также благодаря проверкам во время выполнения, более безопасны.

Footnotes

Типы с фиксированной запятой

Десятичные типы с фиксированной запятой

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

Синтаксис простого десятичного типа с фиксированной запятой:

type <type-name> is delta <delta-value> digits <digits-value>;

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

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

Имя атрибута

Значение

First

Наименьшее значение типа

Last

Наибольшее значение типа

Delta

Значение минимального шага типа

В приведенном ниже примере мы объявляем два типа данных: T3_D3 и T6_D3. Для обоих типов значение дельты одинаково: 0.001.

decimal_fixed_point_types.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Decimal_Fixed_Point_Types is

4    type T3_D3 is delta 10.0 ** (-3) digits 3;

5    type T6_D3 is delta 10.0 ** (-3) digits 6;

6 begin

7    Put_Line ("The delta    value of T3_D3 is "

8              & T3_D3'Image (T3_D3'Delta));

9    Put_Line ("The minimum  value of T3_D3 is "

10              & T3_D3'Image (T3_D3'First));

11    Put_Line ("The maximum  value of T3_D3 is "

12              & T3_D3'Image (T3_D3'Last));

13    New_Line;

14

15    Put_Line ("The delta    value of T6_D3 is "

16              & T6_D3'Image (T6_D3'Delta));

17    Put_Line ("The minimum  value of T6_D3 is "

18              & T6_D3'Image (T6_D3'First));

19    Put_Line ("The maximum  value of T6_D3 is "

20              & T6_D3'Image (T6_D3'Last));

21 end Decimal_Fixed_Point_Types;

Runtime output

The delta    value of T3_D3 is  0.001

The minimum  value of T3_D3 is -0.999

The maximum  value of T3_D3 is  0.999

The delta    value of T6_D3 is  0.001

The minimum  value of T6_D3 is -999.999

The maximum  value of T6_D3 is  999.999

При запуске приложения мы видим, что значение дельты обоих типов действительно одинаково: 0.001. Однако, поскольку T3_D3 ограничен 3 цифрами, его диапазон составляет от -0,999 до 0,999. Для T6_D3 мы определили точность 6 цифр, поэтому диапазон от -999,999 до 999,999.

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

decimal_fixed_point_smaller.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Decimal_Fixed_Point_Smaller is

4    type T3_D3 is delta 10.0 ** (-3) digits 3;

5    type T6_D6 is delta 10.0 ** (-6) digits 6;

6    A : T3_D3 := T3_D3'Delta;

7    B : T3_D3 := 0.5;

8    C : T6_D6;

9 begin

10    Put_Line ("The value of A     is "

11              & T3_D3'Image (A));

12

13    A := A * B;

14    Put_Line ("The value of A * B is "

15              & T3_D3'Image (A));

16

17    A := T3_D3'Delta;

18    C := A * B;

19    Put_Line ("The value of A * B is "

20              & T6_D6'Image (C));

21 end Decimal_Fixed_Point_Smaller;

Build output

decimal_fixed_point_smaller.adb:7:04: warning: "B" is not modified, could be declared constant [-gnatwk]

Runtime output

The value of A     is  0.001

The value of A * B is  0.000

The value of A * B is  0.000500

В этом примере, результат операции 0.001 * 0.5 будет 0.0005. Ввиду того, что это значение не может быть представленно типом T3_D3 ведь его дельта равна 0.001, реальное значение которое получит переменная A будет равно нулю. Однако, если тип имеет большую точность, то точности арифметических операций будет достаточно, и значение C будет равно 0.000500.

Обычные типы с фиксированной запятой

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

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

Note

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

Синтаксис обычного типа с фиксированной запятой:

type <type-name> is

delta <delta-value>

range <lower-bound> .. <upper-bound>;

По умолчанию компилятор выберет коэффициент масштабирования, или small, то есть степень 2, не превышающую <delta-value>.

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

normalized_fixed_point_type.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Normalized_Fixed_Point_Type is

4    D : constant := 2.0 ** (-31);

5    type TQ31 is delta D range -1.0 .. 1.0 - D;

6 begin

7    Put_Line ("TQ31 requires "

8              & Integer'Image (TQ31'Size)

9              & " bits");

10    Put_Line ("The delta    value of TQ31 is "

11              & TQ31'Image (TQ31'Delta));

12    Put_Line ("The minimum  value of TQ31 is "

13              & TQ31'Image (TQ31'First));

14    Put_Line ("The maximum  value of TQ31 is "

15              & TQ31'Image (TQ31'Last));

16 end Normalized_Fixed_Point_Type;

Runtime output

TQ31 requires  32 bits

The delta    value of TQ31 is  0.0000000005

The minimum  value of TQ31 is -1.0000000000

The maximum  value of TQ31 is  0.9999999995

В этом примере мы определяем 32-разрядный тип данных с фиксированной запятой для нормализованного диапазона. При запуске приложения мы замечаем, что верхняя граница близка к единице, но не ровна. Это типичный эффект типов данных с фиксированной запятой - более подробную информацию можно найти в этом обсуждении Q формата#1. Мы также можем переписать этот код определения типа:

normalized_adapted_fixed_point_type.adb

1 procedure Normalized_Adapted_Fixed_Point_Type is

2    type TQ31 is

3      delta 2.0 ** (-31)

4      range -1.0 .. 1.0 - 2.0 ** (-31);

5 begin

6    null;

7 end Normalized_Adapted_Fixed_Point_Type;

Build output

normalized_adapted_fixed_point_type.adb:2:09: warning: type "TQ31" is not referenced [-gnatwu]

Мы также можем использовать любой другой диапазон. Например:

custom_fixed_point_range.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics; use Ada.Numerics;

3

4 procedure Custom_Fixed_Point_Range is

5    type T_Inv_Trig is

6      delta 2.0 ** (-15) * Pi

7      range -Pi / 2.0 .. Pi / 2.0;

8 begin

9    Put_Line ("T_Inv_Trig requires "

10              & Integer'Image (T_Inv_Trig'Size)

11              & " bits");

12    Put_Line ("The delta    value of T_Inv_Trig is "

13              & T_Inv_Trig'Image (T_Inv_Trig'Delta));

14    Put_Line ("The minimum  value of T_Inv_Trig is "

15              & T_Inv_Trig'Image (T_Inv_Trig'First));

16    Put_Line ("The maximum  value of T_Inv_Trig is "

17              & T_Inv_Trig'Image (T_Inv_Trig'Last));

18 end Custom_Fixed_Point_Range;

Build output

custom_fixed_point_range.adb:13:44: warning: static fixed-point value is not a multiple of Small [-gnatwb]

Runtime output

T_Inv_Trig requires  16 bits

The delta    value of T_Inv_Trig is  0.00006

The minimum  value of T_Inv_Trig is -1.57080

The maximum  value of T_Inv_Trig is  1.57080

В этом примере мы определяем 16-разрядный тип с именем T_Inv_Trig, который имеет диапазон от −𝜋/2 до 𝜋/2.

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

fixed_point_op.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Fixed_Point_Op is

4    type TQ31 is

5      delta 2.0 ** (-31)

6      range -1.0 .. 1.0 - 2.0 ** (-31);

7

8    A, B, R : TQ31;

9 begin

10    A := 0.25;

11    B := 0.50;

12    R := A + B;

13    Put_Line ("R is " & TQ31'Image (R));

14 end Fixed_Point_Op;

Runtime output

R is  0.7500000000

Как и ожидалось, R содержит 0,75 после сложения A и B.

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

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

type Angle is

delta 1.0/3600.0

range 0.0 .. 360.0 - 1.0 / 3600.0;

for Angle'Small use Angle'Delta;

Footnotes

#1

https://en.wikipedia.org/wiki/Q_(number_format)

Изоляция

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

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

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

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

Простейшая инкапсуляция

encapsulate.ads

1 package Encapsulate is

2    procedure Hello;

3

4 private

5

6    procedure Hello2;

7    --  Not visible from external units

8 end Encapsulate;

encapsulate.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Encapsulate is

4

5    procedure Hello is

6    begin

7       Put_Line ("Hello");

8    end Hello;

9

10    procedure Hello2 is

11    begin

12       Put_Line ("Hello #2");

13    end Hello2;

14

15 end Encapsulate;

main.adb

1 with Encapsulate;

2

3 procedure Main is

4 begin

5    Encapsulate.Hello;

6    Encapsulate.Hello2;

7    --  Invalid: Hello2 is not visible

8 end Main;

Build output

main.adb:6:15: error: "Hello2" is not a visible entity of "Encapsulate"

gprbuild: *** compilation phase failed

Абстрактные типы данных

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

stacks.ads

1 package Stacks is

2    type Stack is private;

3    --  Declare a private type: You cannot depend

4    --  on its implementation. You can only assign

5    --  and test for equality.

6

7    procedure Push (S   : in out Stack;

8                    Val :        Integer);

9    procedure Pop (S   : in out Stack;

10                   Val :    out Integer);

11 private

12

13    subtype Stack_Index is Natural range 1 .. 10;

14    type Content_Type is array (Stack_Index)

15      of Natural;

16

17    type Stack is record

18       Top     : Stack_Index;

19       Content : Content_Type;

20    end record;

21 end Stacks;

stacks.adb

1 package body Stacks is

2

3    procedure Push (S   : in out Stack;

4                    Val :        Integer) is

5    begin

6       --  Missing implementation!

7       null;

8    end Push;

9

10    procedure Pop (S   : in out Stack;

11                   Val :    out Integer) is

12    begin

13       --  Dummy implementation!

14       Val := 0;

15    end Pop;

16

17 end Stacks;

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

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

Несколько слов о терминологии:

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

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

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

--  No need to read the private part to use the package

package Stacks is

type Stack is private;

procedure Push (S   : in out Stack;

Val :        Integer);

procedure Pop (S   : in out Stack;

Val :    out Integer);

private

...

end Stacks;

А вот как будет использоваться пакет Stacks:

--  Example of use

with Stacks; use Stacks;

procedure Test_Stack is

S : Stack;

Res : Integer;

begin

Push (S, 5);

Push (S, 7);

Pop (S, Res);

end Test_Stack;

Лимитируемые типы

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

stacks.ads

1 package Stacks is

2    type Stack is limited private;

3    --  Limited type. Cannot assign nor compare.

4

5    procedure Push (S   : in out Stack;

6                    Val :        Integer);

7    procedure Pop (S   : in out Stack;

8                   Val :    out Integer);

9 private

10    subtype Stack_Index is Natural range 1 .. 10;

11    type Content_Type is

12      array (Stack_Index) of Natural;

13

14    type Stack is limited record

15       Top     : Stack_Index;

16       Content : Content_Type;

17    end record;

18 end Stacks;

stacks.adb

1 package body Stacks is

2

3    procedure Push (S   : in out Stack;

4                    Val :        Integer) is

5    begin

6       --  Missing implementation!

7       null;

8    end Push;

9

10    procedure Pop (S   : in out Stack;

11                   Val :    out Integer) is

12    begin

13       --  Dummy implementation!

14       Val := 0;

15    end Pop;

16

17 end Stacks;

main.adb

1 with Stacks; use Stacks;

2

3 procedure Main is

4    S, S2 : Stack;

5 begin

6    S := S2;

7    --  Illegal: S is limited.

8 end Main;

Build output

stacks.adb:10:19: warning: formal parameter "S" is not referenced [-gnatwf]

main.adb:6:04: error: left hand of assignment must not be limited type

gprbuild: *** compilation phase failed

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

Ада позволяет вам определить операторы сравнения = и /= для лимитируемых типов (или переопределить встроенные объявления для нелимитируемых).

Ада также позволяет вам предоставить собственную реализацию присваивания используя контролируемые типы#2. Однако в некоторых случаях операция присваивания просто не имеет смысла; примером может служить File_Type из пакета Ada.Text_IO, который объявлен как лимитируемый тип, и поэтому все попытки присвоить один файл другому будут отклонены как незаконные.

Дочерние пакеты и изоляция

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

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

Часть дочернего пакета

Доступ к личному разделу родительской спецификации

Спецификация: видимый раздел

Нет

Спецификация: личный раздел

Да

Тело

Да

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

Давайте сначала рассмотрим пример, в котором тело дочернего пакета P.C имеет доступ к личному разделу спецификации его родителя P. В предыдущем примере исходного кода мы видели, что процедуру Hello2, объявленную в личном разделе пакета Encapsulate нельзя использовать в процедуре Main, поскольку ее там не видно. Однако это ограничение не распространяется на некоторые части дочерних пакетов. Фактически, тело дочернего пакета Encapsulate.Child имеет доступ к процедуре Hello2 и она может быть вызыванна оттуда, как вы можете видеть в реализации процедуры Hello3 пакета Child:

encapsulate.ads

1 package Encapsulate is

2    procedure Hello;

3

4 private

5

6    procedure Hello2;

7    --  Not visible from external units

8    --  But visible in child packages

9 end Encapsulate;

encapsulate.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Encapsulate is

4

5    procedure Hello is

6    begin

7       Put_Line ("Hello");

8    end Hello;

9

10    procedure Hello2 is

11    begin

12       Put_Line ("Hello #2");

13    end Hello2;

14

15 end Encapsulate;

encapsulate-child.ads

1 package Encapsulate.Child is

2

3    procedure Hello3;

4

5 end Encapsulate.Child;

encapsulate-child.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Encapsulate.Child is

4

5    procedure Hello3 is

6    begin

7       --  Using private procedure Hello2

8       --  from the parent package

9       Hello2;

10       Put_Line ("Hello #3");

11    end Hello3;

12

13 end Encapsulate.Child;

main.adb

1 with Encapsulate.Child;

2

3 procedure Main is

4 begin

5    Encapsulate.Child.Hello3;

6 end Main;

Runtime output

Hello #2

Hello #3

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

my_types.ads

1 package My_Types is

2

3    type Priv_Rec is private;

4

5 private

6

7    type Priv_Rec is record

8      Number : Integer := 42;

9    end record;

10

11 end My_Types;

my_types-ops.ads

1 package My_Types.Ops is

2

3    procedure Display (E : Priv_Rec);

4

5 end My_Types.Ops;

my_types-ops.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body My_Types.Ops is

4

5    procedure Display (E : Priv_Rec) is

6    begin

7       Put_Line ("Priv_Rec.Number: "

8                 & Integer'Image (E.Number));

9    end Display;

10

11 end My_Types.Ops;

main.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2

3 with My_Types;     use My_Types;

4 with My_Types.Ops; use My_Types.Ops;

5

6 procedure Main is

7    E : Priv_Rec;

8 begin

9    Put_Line ("Presenting information:");

10

11    --  The following code would trigger a

12    --  compilation error here:

13    --

14    --  Put_Line ("Priv_Rec.Number: "

15    --            & Integer'Image (E.Number));

16

17    Display (E);

18 end Main;

Runtime output

Presenting information:

Priv_Rec.Number:  42

В этом примере у нас нет доступа к компоненте Number типа записи Priv_Rec в процедуре Main. Вы можете увидеть это в вызове Put_Line, который был закомментирован в реализации Main. Попытка получить доступ к компоненте Number вызовет ошибку компиляции. Но у нас есть доступ к этой компоненте в теле пакета My_Types.Ops, поскольку это дочерний пакет пакета My_Types. Следовательно, тело Ops имеет доступ к объявлению типа Priv_Rec, которое находится в личном разделе его родительского пакета My_Types. По этой причине тот же вызов Put_Line, который вызовет ошибку компиляции в процедуре Main, отлично работает в процедуре Display пакета My_Types.Ops.

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

Как мы упоминали ранее, в дополнение к телу пакета личный раздел спецификации дочернего пакета P.C также имеет доступ к личному разделу спецификации его родителя P. Давайте посмотрим на пример, в котором мы объявляем объект личного типа Priv_Rec в личном разделе дочернего пакета My_Types.Child и напрямую инициализируем компоненту Number записи Priv_Rec:

package My_Types.Child is

private

E : Priv_Rec := (Number => 99);

end My_Types.Ops;

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

package My_Types.Child is

E : Priv_Rec := (Number => 99);

end My_Types.Ops;

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

Footnotes

#1

https://ru.wikipedia.org/wiki/Инкапсуляция_(программирование)

#2

https://www.adaic.org/resources/add_content/standards/12rm/html/RM-7-6.html

Настраиваемые модули

Введение

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

Настраиваемыми могут быть лишь подпрограммы или пакеты. Объявление настраиваемого модуля начинается с ключевого слова generic. Например:

operator.ads

1 generic

2    type T is private;

3    --  Declaration of formal types and objects

4 --  Below, we could use one of the following:

5 --  <procedure | function | package>

6 procedure Operator (Dummy : in out T);

operator.adb

1 procedure Operator (Dummy : in out T) is

2 begin

3    null;

4 end Operator;

Объявление формального типа

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

set.ads

1 generic

2    type T is private;

3    --  T is a formal type that indicates that

4    --  any type can be used, possibly a numeric

5    --  type or possibly even a record type.

6 procedure Set (Dummy : T);

set.adb

1 procedure Set (Dummy : T) is

2 begin

3    null;

4 end Set;

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

Формальный тип

Формат

Любой тип

type T is private;

Любой дискретный тип

type T is (<>);

Любой тип с плавающей запятой

type T is digits <>;

Объявление формального объекта

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

set.ads

1 generic

2    type T is private;

3    X : in out T;

4    --  X can be used in the Set procedure

5 procedure Set (E : T);

set.adb

1 procedure Set (E : T) is

2    pragma Unreferenced (E, X);

3 begin

4    null;

5 end Set;

Формальные объекты могут быть либо входными параметрами, либо иметь вид in out.

Определение тела настраиваемого модуля

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

set.ads

1 generic

2    type T is private;

3    X : in out T;

4 procedure Set (E : T);

set.adb

1 procedure Set (E : T) is

2 --  Body definition: "generic" keyword

3 --  is not used

4 begin

5    X := E;

6 end Set;

Конкретизация настройки

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

set.ads

1 generic

2    type T is private;

3    X : in out T;

4    --  X can be used in the Set procedure

5 procedure Set (E : T);

set.adb

1 procedure Set (E : T) is

2 begin

3    X := E;

4 end Set;

show_generic_instantiation.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Set;

3

4 procedure Show_Generic_Instantiation is

5

6    Main    : Integer := 0;

7    Current : Integer;

8

9    procedure Set_Main is new Set (T => Integer,

10                                   X => Main);

11    --  Here, we map the formal parameters to

12    --  actual types and objects.

13    --

14    --  The same approach can be used to

15    --  instantiate functions or packages, e.g.:

16    --

17    --  function Get_Main is new ...

18    --  package Integer_Queue is new ...

19

20 begin

21    Current := 10;

22

23    Set_Main (Current);

24    Put_Line ("Value of Main is "

25              & Integer'Image (Main));

26 end Show_Generic_Instantiation;

Runtime output

Value of Main is  10

В приведенном выше примере мы создаем экземпляр настраиваемой процедуры Set, сопоставляя формальные параметры T и X с фактическими, уже существующими, элементами, в данном случае типом Integer и переменной Main.

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

Предыдущие примеры мы сосредочились на настраиваемых подпрограммах. В этом разделе мы рассмотрим настраиваемые пакеты. Их синтаксис аналогичен: мы начинаем с ключевого слова generic, а далее следуют формальныме объявления. Единственное отличие состоит в том, что вместо ключевого слова подпрограммы указывается package.

Вот пример:

element.ads

1 generic

2    type T is private;

3 package Element is

4

5    procedure Set (E : T);

6    procedure Reset;

7    function Get return T;

8    function Is_Valid return Boolean;

9

10    Invalid_Element : exception;

11

12 private

13    Value : T;

14    Valid : Boolean := False;

15 end Element;

element.adb

1 package body Element is

2

3    procedure Set (E : T) is

4    begin

5       Value := E;

6       Valid := True;

7    end Set;

8

9    procedure Reset is

10    begin

11       Valid := False;

12    end Reset;

13

14    function Get return T is

15    begin

16       if not Valid then

17          raise Invalid_Element;

18       end if;

19       return Value;

20    end Get;

21

22    function Is_Valid return Boolean is (Valid);

23 end Element;

show_generic_package.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Element;

3

4 procedure Show_Generic_Package is

5

6    package I is new Element (T => Integer);

7

8    procedure Display_Initialized is

9    begin

10       if I.Is_Valid then

11          Put_Line ("Value is initialized");

12       else

13          Put_Line ("Value is not initialized");

14       end if;

15    end Display_Initialized;

16

17 begin

18    Display_Initialized;

19

20    Put_Line ("Initializing...");

21    I.Set (5);

22    Display_Initialized;

23    Put_Line ("Value is now set to "

24              & Integer'Image (I.Get));

25

26    Put_Line ("Reseting...");

27    I.Reset;

28    Display_Initialized;

29

30 end Show_Generic_Package;

Runtime output

Value is not initialized

Initializing...

Value is initialized

Value is now set to  5

Reseting...

Value is not initialized

В приведенном выше примере мы создали простой контейнер с именем Element, содержащий всего один элемент. Этот контейнер отслеживает, был ли элемент инициализирован или нет.

После написания определения пакета мы создаем экземпляр I пакета Element. Мы используем экземпляр, вызывая подпрограммы пакета (Set, Reset и Get).

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

В дополнение к формальным типам и объектам мы также можем объявлять формальные подпрограммы или пакеты. Этот курс описывает только формальные подпрограммы; формальные пакеты обсуждаются в продвинутом курсе.

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

check.ads

1 generic

2    Description : String;

3    type T is private;

4    with function Comparison (X, Y : T) return Boolean;

5 procedure Check (X, Y : T);

check.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Check (X, Y : T) is

4    Result : Boolean;

5 begin

6    Result := Comparison (X, Y);

7    if Result then

8       Put_Line ("Comparison ("

9                 & Description

10                 & ") between arguments is OK!");

11    else

12       Put_Line ("Comparison ("

13                 & Description

14                 & ") between arguments is not OK!");

15    end if;

16 end Check;

show_formal_subprogram.adb

1 with Check;

2

3 procedure Show_Formal_Subprogram is

4

5    A, B : Integer;

6

7    procedure Check_Is_Equal is new

8      Check (Description => "equality",

9             T           => Integer,

10             Comparison  => Standard."=");

11    --  Here, we are mapping the standard

12    --  equality operator for Integer types to

13    --  the Comparison formal function

14 begin

15    A := 0;

16    B := 1;

17    Check_Is_Equal (A, B);

18 end Show_Formal_Subprogram;

Runtime output

Comparison (equality) between arguments is not OK!

Пример: конкретизация ввода/вывода

Ада предлагает настраиваемые пакеты ввода-вывода, которые могут быть конкретизированы для стандартных и произвольных типов. Одним из примеров является настраиваемый пакет Float_IO, который предоставляет такие процедуры, как Put и Get. Фактически, Float_Text_IO - доступный в стандартной библиотеке - является конкретизацией пакета Float_IO и определяется как:

with Ada.Text_IO;

package Ada.Float_Text_IO is new Ada.Text_IO.Float_IO (Float);

Его можно использовать непосредственно с любым объектом типа Float. Например:

show_float_text_io.adb

1 with Ada.Float_Text_IO;

2

3 procedure Show_Float_Text_IO is

4    X : constant Float := 2.5;

5

6    use Ada.Float_Text_IO;

7 begin

8    Put (X);

9 end Show_Float_Text_IO;

Runtime output

2.50000E+00

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

show_float_io_inst.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Float_IO_Inst is

4

5    type Price is digits 3;

6

7    package Price_IO is new

8      Ada.Text_IO.Float_IO (Price);

9

10    P : Price;

11 begin

12    --  Set to zero => don't display exponent

13    Price_IO.Default_Exp  := 0;

14

15    P := 2.5;

16    Price_IO.Put (P);

17    New_Line;

18

19    P := 5.75;

20    Price_IO.Put (P);

21    New_Line;

22 end Show_Float_IO_Inst;

Runtime output

2.50

5.75

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

-- [...]

type Price is new Float;

package Price_IO is new

Ada.Text_IO.Float_IO (Price);

begin

Price_IO.Default_Aft  := 2;

Price_IO.Default_Exp  := 0;

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

В дополнение к настраиваемому пакету Float_IO в Ada.Text_IO доступны следующие настраиваемые пакеты:

- Enumeration_IO для перечислимых типов;

- Integer_IO для целочисленных типов;

- Modular_IO для модульных типов;

- Fixed_IO для типов с фиксированной запятой;

- Decimal_IO для десятичных типов.

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

show_decimal_io_inst.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Decimal_IO_Inst is

4

5    type Price is delta 10.0 ** (-2) digits 12;

6

7    package Price_IO is new

8      Ada.Text_IO.Decimal_IO (Price);

9

10    P : Price;

11 begin

12    Price_IO.Default_Exp  := 0;

13

14    P := 2.5;

15    Price_IO.Put (P);

16    New_Line;

17

18    P := 5.75;

19    Price_IO.Put (P);

20    New_Line;

21 end Show_Decimal_IO_Inst;

Runtime output

2.50

5.75

Пример: АТД

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

Типичным примером АТД является стек:

stacks.ads

1 generic

2    Max : Positive;

3    type T is private;

4 package Stacks is

5

6    type Stack is limited private;

7

8    Stack_Underflow, Stack_Overflow : exception;

9

10    function Is_Empty (S : Stack) return Boolean;

11

12    function Pop (S : in out Stack) return T;

13

14    procedure Push (S : in out Stack;

15                    V :        T);

16

17 private

18

19    type Stack_Array is

20      array (Natural range <>) of T;

21

22    Min : constant := 1;

23

24    type Stack is record

25       Container : Stack_Array (Min .. Max);

26       Top       : Natural := Min - 1;

27    end record;

28

29 end Stacks;

stacks.adb

1 package body Stacks is

2

3    function Is_Empty (S : Stack) return Boolean is

4      (S.Top < S.Container'First);

5

6    function Is_Full (S : Stack) return Boolean is

7      (S.Top >= S.Container'Last);

8

9    function Pop (S : in out Stack) return T is

10    begin

11       if Is_Empty (S) then

12          raise Stack_Underflow;

13       else

14          return X : T do

15             X     := S.Container (S.Top);

16             S.Top := S.Top - 1;

17          end return;

18       end if;

19    end Pop;

20

21    procedure Push (S : in out Stack;

22                    V :        T) is

23    begin

24       if Is_Full (S) then

25          raise Stack_Overflow;

26       else

27          S.Top               := S.Top + 1;

28          S.Container (S.Top) := V;

29       end if;

30    end Push;

31

32 end Stacks;

show_stack.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Stacks;

3

4 procedure Show_Stack is

5

6    package Integer_Stacks is new

7      Stacks (Max => 10,

8              T   => Integer);

9    use Integer_Stacks;

10

11    Values : Integer_Stacks.Stack;

12

13 begin

14    Push (Values, 10);

15    Push (Values, 20);

16

17    Put_Line ("Last value was "

18              & Integer'Image (Pop (Values)));

19 end Show_Stack;

Runtime output

Last value was  20

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

Пример: Обмен

Давайте рассмотрим простую процедуру, которая меняет местами переменные типа Color:

colors.ads

1 package Colors is

2    type Color is (Black, Red, Green,

3                   Blue, White);

4

5    procedure Swap_Colors (X, Y : in out Color);

6 end Colors;

colors.adb

1 package body Colors is

2

3    procedure Swap_Colors (X, Y : in out Color) is

4       Tmp : constant Color := X;

5    begin

6       X := Y;

7       Y := Tmp;

8    end Swap_Colors;

9

10 end Colors;

test_non_generic_swap_colors.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Colors;      use Colors;

3

4 procedure Test_Non_Generic_Swap_Colors is

5    A, B, C : Color;

6 begin

7    A := Blue;

8    B := White;

9    C := Red;

10

11    Put_Line ("Value of A is "

12              & Color'Image (A));

13    Put_Line ("Value of B is "

14              & Color'Image (B));

15    Put_Line ("Value of C is "

16              & Color'Image (C));

17

18    New_Line;

19    Put_Line ("Swapping A and C...");

20    New_Line;

21    Swap_Colors (A, C);

22

23    Put_Line ("Value of A is "

24              & Color'Image (A));

25    Put_Line ("Value of B is "

26              & Color'Image (B));

27    Put_Line ("Value of C is "

28              & Color'Image (C));

29 end Test_Non_Generic_Swap_Colors;

Runtime output

Value of A is BLUE

Value of B is WHITE

Value of C is RED

Swapping A and C...

Value of A is RED

Value of B is WHITE

Value of C is BLUE

В этом примере Swap_Colors можно использовать только для типа Color. Однако этот алгоритм теоретически можно использовать для любого типа, будь то перечислимый тип или составной тип записи с множеством элементов. Сам алгоритм такой же: отличается только тип. Если, например, мы хотим поменять местами переменные типа Integer, мы не хотим дублировать реализацию. Следовательно, такой алгоритм - идеальный кандидат для абстракции с использованием настраиваемых модулей.

В приведенном ниже примере мы создадим настраиваемую версию Swap_Colors и назовем ее Generic_Swap. Эта настраиваемая версия может работать с любым типом благодаря объявлению формального типа T.

generic_swap.ads

1 generic

2    type T is private;

3 procedure Generic_Swap (X, Y : in out T);

generic_swap.adb

1 procedure Generic_Swap (X, Y : in out T) is

2    Tmp : constant T := X;

3 begin

4    X := Y;

5    Y := Tmp;

6 end Generic_Swap;

colors.ads

1 with Generic_Swap;

2

3 package Colors is

4

5    type Color is (Black, Red, Green,

6                   Blue, White);

7

8    procedure Swap_Colors is new

9      Generic_Swap (T => Color);

10

11 end Colors;

test_swap_colors.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Colors;       use Colors;

3

4 procedure Test_Swap_Colors is

5    A, B, C : Color;

6 begin

7    A := Blue;

8    B := White;

9    C := Red;

10

11    Put_Line ("Value of A is "

12              & Color'Image (A));

13    Put_Line ("Value of B is "

14              & Color'Image (B));

15    Put_Line ("Value of C is "

16              & Color'Image (C));

17

18    New_Line;

19    Put_Line ("Swapping A and C...");

20    New_Line;

21    Swap_Colors (A, C);

22

23    Put_Line ("Value of A is "

24              & Color'Image (A));

25    Put_Line ("Value of B is "

26              & Color'Image (B));

27    Put_Line ("Value of C is "

28              & Color'Image (C));

29 end Test_Swap_Colors;

Runtime output

Value of A is BLUE

Value of B is WHITE

Value of C is RED

Swapping A and C...

Value of A is RED

Value of B is WHITE

Value of C is BLUE

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

Пример: Обратный порядок элементов

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

colors.ads

1 package Colors is

2

3    type Color is (Black, Red, Green,

4                   Blue, White);

5

6    type Color_Array is

7      array (Integer range <>) of Color;

8

9    procedure Reverse_It (X : in out Color_Array);

10

11 end Colors;

colors.adb

1 package body Colors is

2

3    procedure Reverse_It (X : in out Color_Array) is

4    begin

5       for I in X'First ..

6                (X'Last + X'First) / 2 loop

7          declare

8             Tmp     : Color;

9             X_Left  : Color

10               renames X (I);

11             X_Right : Color

12               renames X (X'Last + X'First - I);

13          begin

14             Tmp     := X_Left;

15             X_Left  := X_Right;

16             X_Right := Tmp;

17          end;

18       end loop;

19    end Reverse_It;

20

21 end Colors;

test_non_generic_reverse_colors.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Colors;      use Colors;

3

4 procedure Test_Non_Generic_Reverse_Colors is

5

6    My_Colors : Color_Array (1 .. 5) :=

7      (Black, Red, Green, Blue, White);

8

9 begin

10    for C of My_Colors loop

11       Put_Line ("My_Color: " & Color'Image (C));

12    end loop;

13

14    New_Line;

15    Put_Line ("Reversing My_Color...");

16    New_Line;

17    Reverse_It (My_Colors);

18

19    for C of My_Colors loop

20       Put_Line ("My_Color: " & Color'Image (C));

21    end loop;

22

23 end Test_Non_Generic_Reverse_Colors;

Runtime output

My_Color: BLACK

My_Color: RED

My_Color: GREEN

My_Color: BLUE

My_Color: WHITE

Reversing My_Color...

My_Color: WHITE

My_Color: BLUE

My_Color: GREEN

My_Color: RED

My_Color: BLACK

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

Чтобы абстрагироваться от этой процедуры, мы объявляем формальные типы для трех элементов алгоритма:

- тип компоненты массива (в примере - тип Color)

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

- фактический тип массива (в примере - тип Color_Array)

Это настраиваемая версия алгоритма:

generic_reverse.ads

1 generic

2    type T is private;

3    type Index is range <>;

4    type Array_T is

5      array (Index range <>) of T;

6 procedure Generic_Reverse (X : in out Array_T);

generic_reverse.adb

1 procedure Generic_Reverse (X : in out Array_T) is

2 begin

3    for I in X'First ..

4             (X'Last + X'First) / 2 loop

5       declare

6          Tmp     : T;

7          X_Left  : T

8            renames X (I);

9          X_Right : T

10            renames X (X'Last + X'First - I);

11       begin

12          Tmp     := X_Left;

13          X_Left  := X_Right;

14          X_Right := Tmp;

15       end;

16    end loop;

17 end Generic_Reverse;

colors.ads

1 with Generic_Reverse;

2

3 package Colors is

4

5    type Color is (Black, Red, Green,

6                   Blue, White);

7

8    type Color_Array is

9      array (Integer range <>) of Color;

10

11    procedure Reverse_It is new

12      Generic_Reverse (T       => Color,

13                       Index   => Integer,

14                       Array_T => Color_Array);

15

16 end Colors;

test_reverse_colors.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Colors;      use Colors;

3

4 procedure Test_Reverse_Colors is

5

6    My_Colors : Color_Array (1 .. 5) :=

7      (Black, Red, Green, Blue, White);

8

9 begin

10    for C of My_Colors loop

11       Put_Line ("My_Color: "

12                 & Color'Image (C));

13    end loop;

14

15    New_Line;

16    Put_Line ("Reversing My_Color...");

17    New_Line;

18    Reverse_It (My_Colors);

19

20    for C of My_Colors loop

21       Put_Line ("My_Color: "

22                 & Color'Image (C));

23    end loop;

24

25 end Test_Reverse_Colors;

Runtime output

My_Color: BLACK

My_Color: RED

My_Color: GREEN

My_Color: BLUE

My_Color: WHITE

Reversing My_Color...

My_Color: WHITE

My_Color: BLUE

My_Color: GREEN

My_Color: RED

My_Color: BLACK

Как упоминалось выше, мы выделили три параметра алгоритма:

- тип T абстрагирует элементы массива

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

- тип Array_T абстрагирует тип массива и использует формальные объявления типов T и Index.

Пример: Тестовое приложение

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

Чтобы сделать это, мы снова должны выбрать элементы для абстрагирования. Поэтому мы объевляем следующие формальные параметры:

- S: строка, содержащая имя массива

- функция Image, преобразующая элемент типа T в строку

- процедура Test, которая выполняет некоторую операцию с массивом

Обратите внимание, что Image и Test являются примерами формальных подпрограмм, а S - примером формального объекта.

Вот версия тестового приложения, использующего общую процедуру Perform_Test:

generic_reverse.ads

1 generic

2    type T is private;

3    type Index is range <>;

4    type Array_T is

5      array (Index range <>) of T;

6 procedure Generic_Reverse (X : in out Array_T);

generic_reverse.adb

1 procedure Generic_Reverse (X : in out Array_T) is

2 begin

3    for I in X'First ..

4             (X'Last + X'First) / 2 loop

5       declare

6          Tmp     : T;

7          X_Left  : T

8            renames X (I);

9          X_Right : T

10            renames X (X'Last + X'First - I);

11       begin

12          Tmp     := X_Left;

13          X_Left  := X_Right;

14          X_Right := Tmp;

15       end;

16    end loop;

17 end Generic_Reverse;

perform_test.ads

1 generic

2    type T is private;

3    type Index is range <>;

4    type Array_T is

5      array (Index range <>) of T;

6    S : String;

7    with function Image (E : T) return String is <>;

8    with procedure Test (X : in out Array_T);

9 procedure Perform_Test (X : in out Array_T);

perform_test.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2

3 procedure Perform_Test (X : in out Array_T) is

4 begin

5    for C of X loop

6       Put_Line (S & ": " & Image (C));

7    end loop;

8

9    New_Line;

10    Put_Line ("Testing " & S & "...");

11    New_Line;

12    Test (X);

13

14    for C of X loop

15       Put_Line (S & ": " & Image (C));

16    end loop;

17 end Perform_Test;

colors.ads

1 with Generic_Reverse;

2

3 package Colors is

4

5    type Color is (Black, Red, Green,

6                   Blue, White);

7

8    type Color_Array is

9      array (Integer range <>) of Color;

10

11    procedure Reverse_It is new

12      Generic_Reverse (T       => Color,

13                       Index   => Integer,

14                       Array_T => Color_Array);

15

16 end Colors;

test_reverse_colors.adb

1 with Colors;       use Colors;

2 with Perform_Test;

3

4 procedure Test_Reverse_Colors is

5

6    procedure Perform_Test_Reverse_It is new

7      Perform_Test (T       => Color,

8                    Index   => Integer,

9                    Array_T => Color_Array,

10                    S       => "My_Color",

11                    Image   => Color'Image,

12                    Test    => Reverse_It);

13

14    My_Colors : Color_Array (1 .. 5) :=

15      (Black, Red, Green, Blue, White);

16

17 begin

18    Perform_Test_Reverse_It (My_Colors);

19 end Test_Reverse_Colors;

Runtime output

My_Color: BLACK

My_Color: RED

My_Color: GREEN

My_Color: BLUE

My_Color: WHITE

Testing My_Color...

My_Color: WHITE

My_Color: BLUE

My_Color: GREEN

My_Color: RED

My_Color: BLACK

В этом примере создается процедура, Perform_Test_Reverse_It как экземпляр настраиваемой процедуры (Perform_Test). Обратите внимание, что:

- Для формальной функции Image мы используем атрибут 'Image типа Color

- Для формальной процедуры тестирования Test мы ссылаемся на процедуру Reverse_Array из пакета.

Footnotes

Исключения

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

Объявление исключения

Исключения в Аде - это не типы, а объекты, что может показаться вам необычным, если вы привыкли к тому, как работают исключения в Java или Python. Вот как вы объявляете исключение:

exceptions.ads

1 package Exceptions is

2     My_Except : exception;

3     --  Like an object. *NOT* a type !

4 end Exceptions;

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

Возбуждение исключения

Чтобы возбудить исключение нашего только что объявленного класса исключения, сделайте следующее:

main.adb

1 with Exceptions; use Exceptions;

2

3 procedure Main is

4 begin

5    raise My_Except;

6    --  Execution of current control flow

7    --  abandoned; an exception of kind

8    --  "My_Except" will bubble up until it

9    --  is caught.

10

11    raise My_Except with "My exception message";

12    --  Execution of current control flow

13    --  abandoned; an exception of kind

14    --  "My_Except" with associated string will

15    --  bubble up until it is caught.

16 end Main;

Build output

main.adb:11:04: warning: unreachable code [enabled by default]

Runtime output

raised EXCEPTIONS.MY_EXCEPT : main.adb:5

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

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

open_file.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Exceptions;  use Ada.Exceptions;

3

4 procedure Open_File is

5    File : File_Type;

6 begin

7    --  Block (sequence of statements)

8    begin

9       Open (File, In_File, "input.txt");

10    exception

11       when E : Name_Error =>

12       --       ^ Exception to be handled

13          Put ("Cannot open input file : ");

14          Put_Line (Exception_Message (E));

15          raise;

16          --  Reraise current occurence

17    end;

18 end Open_File;

Runtime output

Cannot open input file : input.txt: No such file or directory

raised ADA.IO_EXCEPTIONS.NAME_ERROR : input.txt: No such file or directory

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

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

open_file.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Exceptions;  use Ada.Exceptions;

3

4 procedure Open_File is

5    File : File_Type;

6 begin

7    Open (File, In_File, "input.txt");

8 --  Exception block can be added to any block

9 exception

10    when Name_Error =>

11       Put ("Cannot open input file");

12 end Open_File;

Build output

open_file.adb:2:09: warning: no entities of "Ada.Exceptions" are referenced [-gnatwu]

open_file.adb:2:23: warning: use clause for package "Exceptions" has no effect [-gnatwu]

Runtime output

Cannot open input file

Обратите внимание

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

be_careful.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with Ada.Exceptions;  use Ada.Exceptions;

3

4 procedure Be_Careful is

5    function Dangerous return Integer is

6    begin

7       raise Constraint_Error;

8       return 42;

9    end Dangerous;

10

11 begin

12    declare

13       A : Integer := Dangerous;

14    begin

15       Put_Line (Integer'Image (A));

16    exception

17       when Constraint_Error =>

18          Put_Line ("error!");

19    end;

20 end Be_Careful;

Build output

be_careful.adb:2:09: warning: no entities of "Ada.Exceptions" are referenced [-gnatwu]

be_careful.adb:2:23: warning: use clause for package "Exceptions" has no effect [-gnatwu]

be_careful.adb:13:07: warning: "A" is not modified, could be declared constant [-gnatwk]

Runtime output

raised CONSTRAINT_ERROR : be_careful.adb:7 explicit raise

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

Предопределенные исключения

Ада имеет очень небольшое количество предопределенных исключений:

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

- Когда происходит выход за границы массива или, в общем, любое нарушение ограничений

- В случае переполнения

- В случае обращения по пустой ссылке

- В случае деления на 0

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

- Storage_Error произойдет из-за проблем с памятью, таких как:

- Недостаточно памяти (при распределении)

- Недостаточно стека

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

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

Footnotes

Управление задачами

Задачи и защищенные объекты позволяют реализовать параллельное исполнение в Аде. В следующих разделах эти концепции объясняются более подробно.

Задачи

Задачу можно рассматривать как приложение, которое выполняется одновременно с основным приложением. В других языках программирования задачи могут назваться потоками#1, а управление задачами можно назвать многопоточностью#2.

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

Простая задача

Задачи объявляются с использованием ключевого слова task. Реализация задачи определяется в теле задачи (task body). Например:

show_simple_task.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Task is

4    task T;

5

6    task body T is

7    begin

8       Put_Line ("In task T");

9    end T;

10 begin

11    Put_Line ("In main");

12 end Show_Simple_Task;

Runtime output

In main

In task T

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

Обратите внимание, что:

- Основное приложение само по себе является задачей (основной задачей).

- В этом примере подпрограмма Show_Simple_Task является основной задачей приложения.

- Задача T - это подзадача.

- Каждая подзадача имеет задачу-родителя.

- Поэтому основная задача - это также задача-родитель задачи T.

- Количество задач не ограничено одной: мы могли бы включить задачу T2 в приведенный выше пример.

- Эта задача также запускается автоматически и выполняется одновременно как с задачей T, так и с основной задачей. Например:

show_simple_tasks.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3       procedure Show_Simple_Tasks is

4          task T;

5          task T2;

6

7          task body T is

8          begin

9             Put_Line ("In task T");

10          end T;

11

12          task body T2 is

13          begin

14             Put_Line ("In task T2");

15          end T2;

16

17       begin

18          Put_Line ("In main");

19       end Show_Simple_Tasks;

Runtime output

In task T

In main

In task T2

Простая синхронизация

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

show_simple_sync.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Sync is

4    task T;

5    task body T is

6    begin

7       for I in 1 .. 10 loop

8          Put_Line ("hello");

9       end loop;

10    end T;

11 begin

12    null;

13    --  Will wait here until all tasks

14    --  have terminated

15 end Show_Simple_Sync;

Runtime output

hello

hello

hello

hello

hello

hello

hello

hello

hello

hello

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

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

simple_sync_pkg.ads

1 package Simple_Sync_Pkg is

2    task T;

3 end Simple_Sync_Pkg;

Это соответствующее тело пакета:

simple_sync_pkg.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Simple_Sync_Pkg is

4    task body T is

5    begin

6       for I in 1 .. 10 loop

7          Put_Line ("hello");

8       end loop;

9    end T;

10 end Simple_Sync_Pkg;

Поскольку пакет указан в with основной процедуры, задача T, определенная в пакете, является частью основной задачи. Например:

test_simple_sync_pkg.adb

1 with Simple_Sync_Pkg;

2

3 procedure Test_Simple_Sync_Pkg is

4 begin

5    null;

6    --  Will wait here until all tasks

7    --  have terminated

8 end Test_Simple_Sync_Pkg;

Build output

test_simple_sync_pkg.adb:1:06: warning: unit "Simple_Sync_Pkg" is not referenced [-gnatwu]

Runtime output

hello

hello

hello

hello

hello

hello

hello

hello

hello

hello

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

Оператор задержки

Мы можем ввести задержку, используя ключевое слово delay. Это переводит задачу в спящий режим на время, указанное (в секундах) в операторе delay. Например:

show_delay.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Delay is

4

5    task T;

6

7    task body T is

8    begin

9       for I in 1 .. 5 loop

10          Put_Line ("hello from task T");

11          delay 1.0;

12          --    ^ Wait 1.0 seconds

13       end loop;

14    end T;

15 begin

16    delay 1.5;

17    Put_Line ("hello from main");

18 end Show_Delay;

Runtime output

hello from task T

hello from task T

hello from main

hello from task T

hello from task T

hello from task T

В этом примере мы заставляем задачу T ждать одну секунду после каждого вывода сообщения "hello". Кроме того, основная задача ожидает 1,5 секунды перед выводом своего сообщения "hello".

Синхронизация: рандеву

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

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

- вызываемая задача ожидает в этот момент (в операторе принятия accept) и готова принять вызов соответствующей входа из вызывающей задачи;

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

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

show_rendezvous.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Rendezvous is

4

5    task T is

6       entry Start;

7    end T;

8

9    task body T is

10    begin

11       accept Start;

12       --     ^ Waiting for somebody

13       --       to call the entry

14

15       Put_Line ("In T");

16    end T;

17

18 begin

19    Put_Line ("In Main");

20

21    --  Calling T's entry:

22    T.Start;

23 end Show_Rendezvous;

Runtime output

In Main

In T

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

Вход может использоваться для выполнения чего-то большего, чем простая синхронизация задач: он также может выполнять несколько операторов в течение времени синхронизации обеих задач. Мы делаем это с помощью блока do ... end. Для предыдущего примера мы бы просто написали accept Start do <операторы>; end;. Мы используем эту конструкцию в следующем примере.

Обрабатывающий цикл

Задача может исполнять оператор принятия входа не ограниченое число раз. Мы могли бы даже создать бесконечный цикл в задаче и принимать вызовы одного и того же входа снова и снова. Однако бесконечный цикл препятствует завершению задачи-родителя и заблокирует ее, когда она достигает своего конца. Поэтому цикл, содержащий оператор принятия accept в теле задачи, обычно используется в сочетании с оператором select ... or terminate (выбрать или завершить). Говоря упрощенно, этот оператор позволяет родительской задаче автоматически завершать выполнение подзадачи по достижении своего конца. Например:

show_rendezvous_loop.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Rendezvous_Loop is

4

5    task T is

6       entry Reset;

7       entry Increment;

8    end T;

9

10    task body T is

11       Cnt : Integer := 0;

12    begin

13       loop

14          select

15             accept Reset do

16                Cnt := 0;

17             end Reset;

18             Put_Line ("Reset");

19          or

20             accept Increment do

21                Cnt := Cnt + 1;

22             end Increment;

23             Put_Line ("In T's loop ("

24                       & Integer'Image (Cnt)

25                       & ")");

26          or

27             terminate;

28          end select;

29       end loop;

30    end T;

31

32 begin

33    Put_Line ("In Main");

34

35    for I in 1 .. 4 loop

36       --  Calling T's entry multiple times

37       T.Increment;

38    end loop;

39

40    T.Reset;

41    for I in 1 .. 4 loop

42       --  Calling T's entry multiple times

43       T.Increment;

44    end loop;

45

46 end Show_Rendezvous_Loop;

Runtime output

In Main

In T's loop ( 1)

In T's loop ( 2)

In T's loop ( 3)

In T's loop ( 4)

Reset

In T's loop ( 1)

In T's loop ( 2)

In T's loop ( 3)

In T's loop ( 4)

В этом примере тело задачи содержит бесконечный цикл, который принимает вызовы входов Reset и Increment. Нам стоит отметить следуещее:

- Блок accept E do ... end используется для увеличения счетчика.

- До тех пор, пока задача T выполняет блок do ... end, основная задача ожидает завершения блока.

- Основная задача выполняет вызов входа Increment несколько раз в цикле от 1 .. 4. Она также вызывает вход Reset перед вторым циклом.

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

- Когда основная задача достигает конца, она проверяет статус задачи T. Несмотря на то, что задача T может принимать новые вызовы входов Reset или Increment, главная задача может завершить задачу T введу наличия or terminate ветви оператора select.

Циклические задачи

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

while True loop

delay 1.0;

--    ^ Wait 1.0 seconds

Computational_Intensive_App;

end loop;

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

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

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

delay_aux_pkg.ads

1 with Ada.Real_Time; use Ada.Real_Time;

2

3 package Delay_Aux_Pkg is

4

5    function Get_Start_Time return Time

6      with Inline;

7

8    procedure Show_Elapsed_Time

9      with Inline;

10

11    procedure Computational_Intensive_App;

12 private

13    Start_Time   : Time := Clock;

14

15    function Get_Start_Time return Time is (Start_Time);

16

17 end Delay_Aux_Pkg;

delay_aux_pkg.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body Delay_Aux_Pkg is

4

5    procedure Show_Elapsed_Time is

6       Now_Time     : Time;

7       Elapsed_Time : Time_Span;

8    begin

9       Now_Time     := Clock;

10       Elapsed_Time := Now_Time - Start_Time;

11       Put_Line ("Elapsed time "

12                 & Duration'Image (To_Duration (Elapsed_Time))

13                 & " seconds");

14    end Show_Elapsed_Time;

15

16    procedure Computational_Intensive_App is

17    begin

18       delay 0.5;

19    end Computational_Intensive_App;

20

21 end Delay_Aux_Pkg;

Используя этот вспомогательный пакет, теперь мы готовы написать наше приложение с дрейфом по времени:

show_time_task.adb

1 with Ada.Text_IO;   use Ada.Text_IO;

2 with Ada.Real_Time; use Ada.Real_Time;

3

4 with Delay_Aux_Pkg;

5

6 procedure Show_Time_Task is

7    package Aux renames Delay_Aux_Pkg;

8

9    task T;

10

11    task body T is

12       Cnt   : Integer := 1;

13    begin

14       for I in 1 .. 5 loop

15          delay 1.0;

16

17          Aux.Show_Elapsed_Time;

18          Aux.Computational_Intensive_App;

19

20          Put_Line ("Cycle # "

21                    & Integer'Image (Cnt));

22          Cnt  := Cnt + 1;

23       end loop;

24       Put_Line ("Finished time-drifting loop");

25    end T;

26

27 begin

28    null;

29 end Show_Time_Task;

Build output

show_time_task.adb:2:09: warning: no entities of "Ada.Real_Time" are referenced [-gnatwu]

show_time_task.adb:2:21: warning: use clause for package "Real_Time" has no effect [-gnatwu]

Runtime output

Elapsed time  1.035948169 seconds

Cycle #  1

Elapsed time  2.545618496 seconds

Cycle #  2

Elapsed time  4.050430835 seconds

Cycle #  3

Elapsed time  5.552350350 seconds

Cycle #  4

Elapsed time  7.082408585 seconds

Cycle #  5

Finished time-drifting loop

Запустив приложение, мы видим, что у нас уже есть разница во времени примерно в четыре секунды после трех итераций цикла из-за дрейфа, вызванного Computational_Intensive_Applications. Однако, используя оператор delay until, мы можем избежать этого дрейфа и получить регулярный интервал ровно итерации в одну секунду:

show_time_task.adb

1 with Ada.Text_IO;   use Ada.Text_IO;

2 with Ada.Real_Time; use Ada.Real_Time;

3

4 with Delay_Aux_Pkg;

5

6 procedure Show_Time_Task is

7    package Aux renames Delay_Aux_Pkg;

8

9    task T;

10

11    task body T is

12       Cycle : constant Time_Span :=

13         Milliseconds (1000);

14       Next  : Time := Aux.Get_Start_Time

15                       + Cycle;

16

17       Cnt   : Integer := 1;

18    begin

19       for I in 1 .. 5 loop

20          delay until Next;

21

22          Aux.Show_Elapsed_Time;

23          Aux.Computational_Intensive_App;

24

25          --  Calculate next execution time

26          --  using a cycle of one second

27          Next := Next + Cycle;

28

29          Put_Line ("Cycle # "

30                    & Integer'Image (Cnt));

31          Cnt  := Cnt + 1;

32       end loop;

33       Put_Line ("Finished cycling");

34    end T;

35

36 begin

37    null;

38 end Show_Time_Task;

Runtime output

Elapsed time  1.004152324 seconds

Cycle #  1

Elapsed time  2.003440248 seconds

Cycle #  2

Elapsed time  3.030813293 seconds

Cycle #  3

Elapsed time  4.001581985 seconds

Cycle #  4

Elapsed time  5.032210132 seconds

Cycle #  5

Finished cycling

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

Защищенные объекты

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

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

Важное замечание.

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

Простой объект

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

show_protected_objects.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Protected_Objects is

4

5    protected Obj is

6       --  Operations go here (only subprograms)

7       procedure Set (V : Integer);

8       function Get return Integer;

9    private

10       --  Data goes here

11       Local : Integer := 0;

12    end Obj;

13

14    protected body Obj is

15       --  procedures can modify the data

16       procedure Set (V : Integer) is

17       begin

18          Local := V;

19       end Set;

20

21       --  functions cannot modify the data

22       function Get return Integer is

23       begin

24          return Local;

25       end Get;

26    end Obj;

27

28 begin

29    Obj.Set (5);

30    Put_Line ("Number is: "

31              & Integer'Image (Obj.Get));

32 end Show_Protected_Objects;

Runtime output

Number is:  5

В этом примере мы определяем две операции для Obj: Set и Get. Реализация этих операций находится в теле объекта Obj. Синтаксис, используемый для записи этих операций, такой же, как и для обычных процедур и функций. Реализация защищенных объектов проста - мы просто читаем и переписываем значение Local в этих подпрограммах. Для вызова этих операций в основном приложении мы используем точечную нотацию, например, Obj.Get.

Входы

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

В предыдущем примере использовались процедуры и функции для определения операций с защищенными объектами. Однако при этом можно считать защищенную информацию (через Obj.Get) до того, как она будет установлена (через Obj.Set). Чтобы код имел детерминированное поведение, мы указали значение по умолчанию (0). Если, вместо этого, переписать функцию Obj.Get как вход, мы сможем установить барьер, гарантирующий, что ни одна задача не сможет прочитать информацию до того, как она будет записана.

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

show_protected_objects_entries.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Protected_Objects_Entries is

4

5    protected Obj is

6       procedure Set (V : Integer);

7       entry Get (V : out Integer);

8    private

9       Local  : Integer;

10       Is_Set : Boolean := False;

11    end Obj;

12

13    protected body Obj is

14       procedure Set (V : Integer) is

15       begin

16          Local := V;

17          Is_Set := True;

18       end Set;

19

20       entry Get (V : out Integer)

21         when Is_Set is

22          --  Entry is blocked until the

23          --  condition is true. The barrier

24          --  is evaluated at call of entries

25          --  and at exits of procedures and

26          --  entries. The calling task sleeps

27          --  until the barrier is released.

28       begin

29          V := Local;

30          Is_Set := False;

31       end Get;

32    end Obj;

33

34    N : Integer := 0;

35

36    task T;

37

38    task body T is

39    begin

40       Put_Line ("Task T will delay for 4 seconds...");

41       delay 4.0;

42       Put_Line ("Task T will set Obj...");

43       Obj.Set (5);

44       Put_Line ("Task T has just set Obj...");

45    end T;

46 begin

47    Put_Line ("Main application will get Obj...");

48    Obj.Get (N);

49    Put_Line ("Main application has just retrieved Obj...");

50    Put_Line ("Number is: " & Integer'Image (N));

51

52 end Show_Protected_Objects_Entries;

Runtime output

Task T will delay for 4 seconds...

Main application will get Obj...

Task T will set Obj...

Task T has just set Obj...

Main application has just retrieved Obj...

Number is:  5

Как видим, запустив пример, основное приложение ждет, пока не произойдет запись в защищенный объект (по вызову Obj.Set в задаче T), прежде чем прочитать информацию (через Obj.Get). Поскольку в задаче T добавлена 4-секундная задержка, основное приложение также задерживается на 4 секунды. Только после этой задержки задача T записывает данные в объект и снимает барьер в Obj.Get, чтобы главное приложение могло затем возобновить обработку (после извлечения информации из защищенного объекта).

Задачные и защищенные типы

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

Задачные типы

Задачный типй - это обобщение задачи. Его объявление аналогично единичным задачам: вы заменяете task на task type. Разница между единичными задачами и задачными заключается в том, что задачные типы не создают фактические задачи и не запускают их автоматически. Вместо этого требуется объявление задачи. Именно так работают обычные переменные и типы: объекты создаются только с помощью определений переменных, но не определений типов.

Чтобы проиллюстрировать это, мы повторим наш первый пример:

show_simple_task.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Task is

4    task T;

5

6    task body T is

7    begin

8       Put_Line ("In task T");

9    end T;

10 begin

11    Put_Line ("In main");

12 end Show_Simple_Task;

Runtime output

In task T

In main

Теперь мы переписываем его, заменив задачу task T задачным типом task type TT. Объявляем задачу (A_Task) на основе задачного типа TT после её определения:

show_simple_task_type.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Task_Type is

4    task type TT;

5

6    task body TT is

7    begin

8       Put_Line ("In task type TT");

9    end TT;

10

11    A_Task : TT;

12 begin

13    Put_Line ("In main");

14 end Show_Simple_Task_Type;

Runtime output

In task type TT

In main

Мы можем расширить этот пример и создать массив задач. Поскольку мы используем тот же синтаксис, что и для объявлений переменных, мы используем аналогичный синтаксис для задачного типа: array (<>) of Task_Type. Кроме того, мы можем передавать информацию отдельным задачам, определив начальный вход Start. Вот обновленный пример:

show_task_type_array.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Task_Type_Array is

4    task type TT is

5       entry Start (N : Integer);

6    end TT;

7

8    task body TT is

9       Task_N : Integer;

10    begin

11       accept Start (N : Integer) do

12          Task_N := N;

13       end Start;

14       Put_Line ("In task T: "

15                 & Integer'Image (Task_N));

16    end TT;

17

18    My_Tasks : array (1 .. 5) of TT;

19 begin

20    Put_Line ("In main");

21

22    for I in My_Tasks'Range loop

23       My_Tasks (I).Start (I);

24    end loop;

25 end Show_Task_Type_Array;

Runtime output

In main

In task T:  1

In task T:  2

In task T:  3

In task T:  4

In task T:  5

В этом примере мы объявляем пять задач в массиве My_Tasks. Мы передаем индекс в массиве отдельной задаче вызвав вход (Start). После синхронизации между отдельными подзадачами и основной задачей каждая подзадача одновременно вызывает Put_Line.

Защищенные типы

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

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

show_protected_object_type.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Protected_Object_Type is

4

5    protected type Obj_Type is

6       procedure Set (V : Integer);

7       function Get return Integer;

8    private

9       Local : Integer := 0;

10    end Obj_Type;

11

12    protected body Obj_Type is

13       procedure Set (V : Integer) is

14       begin

15          Local := V;

16       end Set;

17

18       function Get return Integer is

19       begin

20          return Local;

21       end Get;

22    end Obj_Type;

23

24    Obj : Obj_Type;

25 begin

26    Obj.Set (5);

27    Put_Line ("Number is: "

28              & Integer'Image (Obj.Get));

29 end Show_Protected_Object_Type;

Runtime output

Number is:  5

В этом примере вместо непосредственного определения объекта Obj мы сначала определяем защищенный тип Obj_Type а затем объявляем Obj как объект этого защищенного типа. Обратите внимание, что основное приложение не изменилось: мы по-прежнему используем Obj.Set и Obj.Get для доступа к защищенному объекту, как в исходном примере.

Footnotes

#1

https://eru.wikipedia.org/wiki/Поток_выполнения

#2

https://eru.wikipedia.org/wiki/Поток_выполнения#Многопоточность

Контрактное проектирование

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

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

Пред- и постусловия

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

Пред- и постусловия указываются с помощью спецификации аспекта в объявлении подпрограммы. Спуцификация with Pre => <условие> определяет предварительное условие, а спецификация with Post => <условие> определяет постусловие.

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

show_simple_precondition.adb

1 procedure Show_Simple_Precondition is

2

3    procedure DB_Entry (Name : String;

4                        Age  : Natural)

5      with Pre => Name'Length > 0

6    is

7    begin

8       --  Missing implementation

9       null;

10    end DB_Entry;

11 begin

12    DB_Entry ("John", 30);

13

14    --  Precondition will fail!

15    DB_Entry ("",     21);

16 end Show_Simple_Precondition;

Runtime output

raised ADA.ASSERTIONS.ASSERTION_ERROR : failed precondition from show_simple_precondition.adb:5

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

В наборе инструментов GNAT

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

Прежде чем мы перейдем к следующему примеру, давайте кратко обсудим кванторные выражения, которые весьма полезны для краткого написания предварительных и постусловий. Кванторные выражения возвращают логическое значение, указывающее, соответствуют ли элементы массива или контейнера ожидаемому условию. Они имеют форму: (for all I in A'Range => <условие для A(I)>, где A - это массив, а I - индекс. Кванторные выражения, использующие for all, проверяют, выполняется ли условие для каждого элемент. Например:

(for all I in A'Range => A (I) = 0)

Это кванторные выражение истинно только тогда, когда все элементы массива A имеют нулевое значение.

Другой вид кванторных выражений использует for some. Он выглядит примерно так: (for some I in A'Range => <условие для A(I)>. И в этом случае кванторное выражение проверяет, есть ли некоторые (some) элементы (отсюда и название) для которых условие истино.

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

show_simple_postcondition.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Postcondition is

4

5    type Int_8 is range -2 ** 7 .. 2 ** 7 - 1;

6

7    type Int_8_Array is

8      array (Integer range <>) of Int_8;

9

10    function Square (A : Int_8) return Int_8 is

11      (A * A)

12      with Post => (if abs A in 0 | 1

13                    then Square'Result = abs A

14                    else Square'Result > A);

15

16    procedure Square (A : in out Int_8_Array)

17      with Post => (for all I in A'Range =>

18                      A (I) = A'Old (I) * A'Old (I))

19    is

20    begin

21       for V of A loop

22          V := Square (V);

23       end loop;

24    end Square;

25

26    V : Int_8_Array := (-2, -1, 0, 1, 10, 11);

27 begin

28    for E of V loop

29       Put_Line ("Original: "

30                 & Int_8'Image (E));

31    end loop;

32    New_Line;

33

34    Square (V);

35    for E of V loop

36       Put_Line ("Square:   "

37                 & Int_8'Image (E));

38    end loop;

39 end Show_Simple_Postcondition;

Runtime output

Original: -2

Original: -1

Original:  0

Original:  1

Original:  10

Original:  11

Square:    4

Square:    1

Square:    0

Square:    1

Square:    100

Square:    121

Мы объявляем 8-битный тип со знаком Int_8 и массив элементов этого типа (Int_8_Array). Мы хотим убедиться, что после вызова процедуры Square каждый элемент массива Int_8_Array возведен в квадрат. Мы делаем это с помощью постусловия, используя выражение for all. Это постусловие использует атрибут 'Old чтобы сослаться на исходное (до вызова) значение параметра.

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

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

show_simple_contract.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Contract is

4

5    type Int_8 is range -2 ** 7 .. 2 ** 7 - 1;

6

7    function Square (A : Int_8) return Int_8 is

8      (A * A)

9      with

10           Pre  => (Integer'Size >= Int_8'Size * 2

11                    and Integer (A) * Integer (A) <=

12                        Integer (Int_8'Last)),

13           Post => (if abs A in 0 | 1

14                    then Square'Result = abs A

15                    else Square'Result > A);

16

17    V : Int_8;

18 begin

19    V := Square (11);

20    Put_Line ("Square of 11 is " & Int_8'Image (V));

21

22    --  Precondition will fail...

23    V := Square (12);

24    Put_Line ("Square of 12 is " & Int_8'Image (V));

25 end Show_Simple_Contract;

Runtime output

Square of 11 is  121

raised ADA.ASSERTIONS.ASSERTION_ERROR : failed precondition from show_simple_contract.adb:10

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

Предикаты

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

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

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

- with Static_Predicate => <свойство>

- with Dynamic_Predicate => <свойство>

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

show_dynamic_predicate_courses.adb

1 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

2 with Ada.Calendar;          use Ada.Calendar;

3 with Ada.Containers.Vectors;

4

5 procedure Show_Dynamic_Predicate_Courses is

6

7    package Courses is

8       type Course_Container is private;

9

10       type Course is record

11          Name       : Unbounded_String;

12          Start_Date : Time;

13          End_Date   : Time;

14       end record

15         with Dynamic_Predicate =>

16           Course.Start_Date <= Course.End_Date;

17

18       procedure Add (CC : in out Course_Container;

19                      C  :        Course);

20    private

21       package Course_Vectors is new

22         Ada.Containers.Vectors

23           (Index_Type   => Natural,

24            Element_Type => Course);

25

26       type Course_Container is record

27          V : Course_Vectors.Vector;

28       end record;

29    end Courses;

30

31    package body Courses is

32       procedure Add (CC : in out Course_Container;

33                      C  :        Course) is

34       begin

35          CC.V.Append (C);

36       end Add;

37    end Courses;

38

39    use Courses;

40

41    CC : Course_Container;

42 begin

43    Add (CC,

44         Course'(

45           Name       => To_Unbounded_String

46                          ("Intro to Photography"),

47           Start_Date => Time_Of (2018, 5, 1),

48           End_Date   => Time_Of (2018, 5, 10)));

49

50    --  This should trigger an error in the

51    --  dynamic predicate check

52    Add (CC,

53         Course'(

54           Name       => To_Unbounded_String

55                          ("Intro to Video Recording"),

56           Start_Date => Time_Of (2019, 5, 1),

57           End_Date   => Time_Of (2018, 5, 10)));

58

59 end Show_Dynamic_Predicate_Courses;

Runtime output

raised ADA.ASSERTIONS.ASSERTION_ERROR : Dynamic_Predicate failed at show_dynamic_predicate_courses.adb:53

В этом примере пакет Courses определяет тип Course и тип Course_Container, объект которого содержит все курсы. Мы хотим обеспечить согласованность дат каждого курса, в частности, чтобы дата начала была не позже даты окончания. Чтобы обеспечить соблюдение этого правила, мы объявляем динамический предикат для типа Course, который будет действовать для каждого объекта. Предикат использует имя типа, так как будь-то это обычная переменная данного типа: такое имя обозначает экземпляр проверяемого объекта.

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

- о типе неограниченной строки (Unbounded_String): раздел Неограниченные строки;

- о дате и времени: Раздел Даты и время.

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

type Week is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

Мы можем легко создать подсписок рабочих дней в неделе, указав подтип (subtype) с диапазоном на основе Week. Например:

subtype Work_Week is Week range Mon .. Fri;

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

subtype Check_Days is Work_Week

with Static_Predicate => Check_Days in Mon | Wed | Fri;

Давайте посмотрим на полный пример:

show_predicates.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Predicates is

4

5    type Week is (Mon, Tue, Wed, Thu,

6                  Fri, Sat, Sun);

7

8    subtype Work_Week is Week range Mon .. Fri;

9

10    subtype Test_Days is Work_Week

11      with Static_Predicate =>

12        Test_Days in Mon | Wed | Fri;

13

14    type Tests_Week is array (Week) of Natural

15      with Dynamic_Predicate =>

16        (for all I in Tests_Week'Range =>

17           (case I is

18                when Test_Days =>

19                   Tests_Week (I) > 0,

20                when others    =>

21                   Tests_Week (I) = 0));

22

23    Num_Tests : Tests_Week :=

24                  (Mon => 3, Tue => 0,

25                   Wed => 4, Thu => 0,

26                   Fri => 2, Sat => 0,

27                   Sun => 0);

28

29    procedure Display_Tests (N : Tests_Week) is

30    begin

31       for I in Test_Days loop

32          Put_Line ("# tests on "

33                    & Test_Days'Image (I)

34                    & " => "

35                    & Integer'Image (N (I)));

36       end loop;

37    end Display_Tests;

38

39 begin

40    Display_Tests (Num_Tests);

41

42    --  Assigning non-conformant values to

43    --  individual elements of the Tests_Week

44    --  type does not trigger a predicate

45    --  check:

46    Num_Tests (Tue) := 2;

47

48    --  However, assignments with the "complete"

49    --  Tests_Week type trigger a predicate

50    --  check. For example:

51    --

52    --  Num_Tests := (others => 0);

53

54    --  Also, calling any subprogram with

55    --  parameters of Tests_Week type

56    --  triggers a predicate check. Therefore,

57    --  the following line will fail:

58    Display_Tests (Num_Tests);

59 end Show_Predicates;

Runtime output

# tests on MON =>  3

# tests on WED =>  4

# tests on FRI =>  2

raised ADA.ASSERTIONS.ASSERTION_ERROR : Dynamic_Predicate failed at show_predicates.adb:58

Здесь у нас есть приложение, которое хочет проводить тесты только три дня в рабочей неделе. Эти дни указаны в подтипе Test_Days. Мы хотим отслеживать количество тестов, которые проводятся каждый день. Мы объявляем тип Tests_Week как массив, объект которого будет содержать количество тестов, выполняемых каждый день. Согласно нашим требованиям, эти тесты должны проводиться только в вышеупомянутые три дня; в другие дни никаких анализов проводить не следует. Это требование реализовано с помощью динамического предиката типа Tests_Week. Наконец, фактическая информация об этих тестах хранится в массиве Num_Tests, который является экземпляром типа Tests_Week.

Динамический предикат типа Tests_Week проверяется во время инициализации Num_Tests. Если у нас там будет несоответствующее значение, проверка не удастся. Однако, как мы видим в нашем примере, изменения отдельных элементов массива не приводят к выполнению проверки. Так происходит потому, что инициализация сложной структуры данных (например, массивов или записей) может требовать выполнения нескольких операций присваивания. Однако, как только объект передается в качестве аргумента подпрограмме, динамический предикат проверяется, потому что подпрограмма требует, чтобы объект был согласован. Это происходит при вызове Display_Tests в последнем операторе нашего примера. Здесь проверка предиката вызовет ошибку потому, что предыдущее изменение приводит к несогласованному значению.

Инварианты типа

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

Инварианты типов указываются с помощью предложения with Type_Invariant => <свойство>. Подобно предикатам, свойство определяет условие, которое позволяет нам контролировать, соответствует ли объект типа T нашим требованиям. В этом смысле инварианты типов можно рассматривать как своего рода предикаты для личных типов. Однако есть некоторые отличия касающиеся проверок. Эти отличия приведены в следующей таблице:

Элемент

Проверки параметров подпрограммы

Проверки присвоения

Предикаты

По всем входящим  in и выходящим out параметрам

При присваивании и явных инициализациях

Инварианты типов

По параметрам  out, возвращенным из подпрограмм, объявленных в том же видимом разделе

При всех инициализациях

Мы могли бы переписать наш предыдущий пример и заменить динамические предикаты инвариантами типов. Это выглядело бы так:

show_type_invariant.adb

1 with Ada.Text_IO;           use Ada.Text_IO;

2 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

3 with Ada.Calendar;          use Ada.Calendar;

4 with Ada.Containers.Vectors;

5

6 procedure Show_Type_Invariant is

7

8    package Courses is

9       type Course is private

10         with Type_Invariant => Check (Course);

11

12       type Course_Container is private;

13

14       procedure Add (CC : in out Course_Container;

15                      C  :        Course);

16

17       function Init

18         (Name                 : String;

19          Start_Date, End_Date : Time) return Course;

20

21       function Check (C : Course) return Boolean;

22

23    private

24       type Course is record

25          Name       : Unbounded_String;

26          Start_Date : Time;

27          End_Date   : Time;

28       end record;

29

30       function Check (C : Course) return Boolean is

31         (C.Start_Date <= C.End_Date);

32

33       package Course_Vectors is new

34         Ada.Containers.Vectors

35           (Index_Type   => Natural,

36            Element_Type => Course);

37

38       type Course_Container is record

39          V : Course_Vectors.Vector;

40       end record;

41    end Courses;

42

43    package body Courses is

44       procedure Add (CC : in out Course_Container;

45                      C  :        Course) is

46       begin

47          CC.V.Append (C);

48       end Add;

49

50       function Init

51         (Name                 : String;

52          Start_Date, End_Date : Time) return Course is

53       begin

54          return

55            Course'(Name       => To_Unbounded_String (Name),

56                    Start_Date => Start_Date,

57                    End_Date   => End_Date);

58       end Init;

59    end Courses;

60

61    use Courses;

62

63    CC : Course_Container;

64 begin

65    Add (CC,

66         Init (Name       => "Intro to Photography",

67               Start_Date => Time_Of (2018, 5, 1),

68               End_Date   => Time_Of (2018, 5, 10)));

69

70    --  This should trigger an error in the

71    --  type-invariant check

72    Add (CC,

73         Init (Name       => "Intro to Video Recording",

74               Start_Date => Time_Of (2019, 5, 1),

75               End_Date   => Time_Of (2018, 5, 10)));

76 end Show_Type_Invariant;

Build output

show_type_invariant.adb:1:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

show_type_invariant.adb:1:29: warning: use clause for package "Text_IO" has no effect [-gnatwu]

Runtime output

raised ADA.ASSERTIONS.ASSERTION_ERROR : failed invariant from show_type_invariant.adb:10

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

Footnotes

Взаимодействие с языком C

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

Многоязычный проект

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

project Multilang is

for Languages use ("ada", "c");

for Source_Dirs use ("src");

for Main use ("main.adb");

for Object_Dir use "obj";

end Multilang;

Соглашение о типах

Для взаимодействия с типами данных, объявленными в приложении на C, необходимо указать аспект Convention в соответствующем объявлении типа Ада. В следующем примере вводим перечисленимый тип C_Enum, для соответствующего типа в исходном файле C:

show_c_enum.adb

1 procedure Show_C_Enum is

2

3    type C_Enum is (A, B, C)

4      with Convention => C;

5    --  Use C convention for C_Enum

6 begin

7    null;

8 end Show_C_Enum;

Для взаимодействия со встроенными типами C используется пакет Interfaces.C, содержащий большинство необходимых определений типов. Например:

show_c_struct.adb

1 with Interfaces.C; use Interfaces.C;

2

3 procedure Show_C_Struct is

4

5    type c_struct is record

6       a : int;

7       b : long;

8       c : unsigned;

9       d : double;

10    end record

11      with Convention => C;

12

13 begin

14    null;

15 end Show_C_Struct;

Здесь мы взаимодействуем со структурой C (C_Struct) и используем соответствующие типы данных C (int, long, unsigned и double). А вот объявление в C:

c_struct.h

1 struct c_struct

2 {

3     int         a;

4     long        b;

5     unsigned    c;

6     double      d;

7 };

Подпрограммы на других языках

Вызов подпрограмм C из Ады

Мы используем аналогичный подход при взаимодействии с подпрограммами, написанными на C. Рассмотрим следующее объявление в заголовочном файле C:

my_func.h

1 int my_func (int a);

Вот соответствующее определение функции на C:

my_func.c

1 #include "my_func.h"

2

3 int my_func (int a)

4 {

5     return a * 2;

6 }

Мы можем связать этот код с кодом на Аде, указывая аспект Import. Например:

show_c_func.adb

1 with Interfaces.C; use Interfaces.C;

2 with Ada.Text_IO;  use Ada.Text_IO;

3

4 procedure Show_C_Func is

5

6    function my_func (a : int) return int

7      with

8        Import        => True,

9        Convention    => C;

10

11    --  Imports function 'my_func' from C.

12    --  You can now call it from Ada.

13

14    V : int;

15 begin

16    V := my_func (2);

17    Put_Line ("Result is " & int'Image (V));

18 end Show_C_Func;

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

show_c_func.adb

1 with Interfaces.C; use Interfaces.C;

2 with Ada.Text_IO;  use Ada.Text_IO;

3

4 procedure Show_C_Func is

5

6    function Get_Value (a : int) return int

7      with

8        Import        => True,

9        Convention    => C,

10        External_Name => "my_func";

11

12    --  Imports function 'my_func' from C and

13    --  rename it to 'Get_Value'

14

15    V : int;

16 begin

17    V := Get_Value (2);

18    Put_Line ("Result is " & int'Image (V));

19 end Show_C_Func;

Вызов Ада подпрограмм из C

Вы также можете вызывать Ада подпрограммы из C приложений. Это делается с помощью аспекта Export. Например:

c_api.ads

1 with Interfaces.C; use Interfaces.C;

2

3 package C_API is

4

5    function My_Func (a : int) return int

6      with

7        Export        => True,

8        Convention    => C,

9        External_Name => "my_func";

10

11 end C_API;

Вот соответствующее тело пакета с реализфцией этой функции:

c_api.adb

1 package body C_API is

2

3    function My_Func (a : int) return int is

4    begin

5       return a * 2;

6    end My_Func;

7

8 end C_API;

На стороне C мы делаем так, как если бы функция была написана на C: просто объявляем ее с помощью ключевого слова extern. Например:

main.c

1 #include <stdio.h>

2

3 extern int my_func (int a);

4

5 int main (int argc, char **argv) {

6

7   int v = my_func(2);

8

9   printf("Result is %d\n", v);

10

11   return 0;

12 }

Внешние переменные

Использование глобальных переменных C в Аде

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

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

test.h

1 extern int func_cnt;

2

3 int my_func (int a);

Переменная объявлена в файле C и увеличивается в my_func:

test.c

1 #include "test.h"

2

3 int func_cnt = 0;

4

5 int my_func (int a)

6 {

7   func_cnt++;

8

9   return a * 2;

10 }

В коде на Аде мы просто ссылаемся на внешнюю переменную:

show_c_func.adb

1 with Interfaces.C; use Interfaces.C;

2 with Ada.Text_IO;  use Ada.Text_IO;

3

4 procedure Show_C_Func is

5

6    function my_func (a : int) return int

7      with

8        Import     => True,

9        Convention => C;

10

11    V : int;

12

13    func_cnt : int

14      with

15        Import        => True,

16        Convention    => C;

17    --  We can access the func_cnt variable

18    --  from test.c

19

20 begin

21    V := my_func (1);

22    V := my_func (2);

23    V := my_func (3);

24    Put_Line ("Result is " & int'Image (V));

25

26    Put_Line ("Function was called "

27              & int'Image (func_cnt)

28              & " times");

29 end Show_C_Func;

Как мы видим, запустив приложение, значение счетчика - это количество вызовов my_func.

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

Использование переменных Ада в C

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

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

c_api.ads

1 with Interfaces.C; use Interfaces.C;

2

3 package C_API is

4

5    func_cnt : int := 0

6      with

7        Export     => True,

8        Convention => C;

9

10    function My_Func (a : int) return int

11      with

12        Export        => True,

13        Convention    => C,

14        External_Name => "my_func";

15

16 end C_API;

Затем переменная увеличивается в My_Func:

c_api.adb

1 package body C_API is

2

3    function My_Func (a : int) return int is

4    begin

5       func_cnt := func_cnt + 1;

6       return a * 2;

7    end My_Func;

8

9 end C_API;

В приложении C нам просто нужно объявить переменную и использовать ее:

main.c

1 #include <stdio.h>

2

3 extern int my_func (int a);

4

5 extern int func_cnt;

6

7 int main (int argc, char **argv) {

8

9   int v;

10

11   v = my_func(1);

12   v = my_func(2);

13   v = my_func(3);

14

15   printf("Result is %d\n", v);

16

17   printf("Function was called %d times\n", func_cnt);

18

19   return 0;

20 }

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

Автоматическое создание связок

В приведенных выше примерах мы вручную добавили аспекты в наш код Ада, чтобы они соответствовали исходному коду C, с которым мы взаимодействуем. Это называется созданием cвязки. Мы можем автоматизировать этот процесс, используя особый ключ компилятора для дампа спецификации Ада: -fdump-ada-spec. Мы проиллюстрируем это, вернувшись к нашему предыдущему примеру.

Это был наш заголовочный файл C:

my_func.c

1 extern int func_cnt;

2

3 int my_func (int a);

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

gcc -c -fdump-ada-spec -C ./test.h

Результатом является файл спецификации Ада с именем test_h.ads:

test_h.ads

1 pragma Ada_2005;

2 pragma Style_Checks (Off);

3

4 with Interfaces.C; use Interfaces.C;

5

6 package test_h is

7

8    func_cnt : aliased int;  -- ./test.h:3

9    pragma Import (C, func_cnt, "func_cnt");

10

11    function my_func (arg1 : int) return int;  -- ./test.h:5

12    pragma Import (C, my_func, "my_func");

13

14 end test_h;

Теперь мы просто ссылаемся на этот пакет test_h в нашем приложении Ада:

show_c_func.adb

1 with Interfaces.C; use Interfaces.C;

2 with Ada.Text_IO;  use Ada.Text_IO;

3 with test_h;       use test_h;

4

5 procedure Show_C_Func is

6    V : int;

7 begin

8    V := my_func (1);

9    V := my_func (2);

10    V := my_func (3);

11    Put_Line ("Result is " & int'Image (V));

12

13    Put_Line ("Function was called "

14              & int'Image (func_cnt)

15              & " times");

16 end Show_C_Func;

Вы можете указать имя родительского модуля создаваемых связок в качестве операнда для fdump-ada-spec:

gcc -c -fdump-ada-spec -fada-spec-parent=Ext_C_Code -C ./test.h

И получим файл ext_c_code-test_h.ads:

ext_c_code-test_h.ads

1 package Ext_C_Code.test_h is

2

3    --  automatic generated bindings...

4

5 end Ext_C_Code.test_h;

Адаптация связок

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

Начнем с такого заголовочного файла C:

test.h

1 struct test;

2

3 struct test * test_create(void);

4

5 void test_destroy(struct test *t);

6

7 void test_reset(struct test *t);

8

9 void test_set_name(struct test *t, char *name);

10

11 void test_set_address(struct test *t, char *address);

12

13 void test_display(const struct test *t);

И соответствующей реализация на C:

test.c

1 #include <stdlib.h>

2 #include <string.h>

3 #include <stdio.h>

4

5 #include "test.h"

6

7 struct test {

8   char name[80];

9   char address[120];

10 };

11

12 static size_t

13 strlcpy(char *dst, const char *src, size_t dstsize)

14 {

15   size_t len = strlen(src);

16   if (dstsize) {

17     size_t bl = (len < dstsize-1 ? len : dstsize-1);

18     ((char*)memcpy(dst, src, bl))[bl] = 0;

19   }

20   return len;

21 }

22

23 struct test * test_create(void)

24 {

25   return malloc (sizeof (struct test));

26 }

27

28 void test_destroy(struct test *t)

29 {

30   if (t != NULL) {

31     free(t);

32   }

33 }

34

35 void test_reset(struct test *t)

36 {

37   t->name[0]    = '\0';

38   t->address[0] = '\0';

39 }

40

41 void test_set_name(struct test *t, char *name)

42 {

43   strlcpy(t->name, name, sizeof(t->name));

44 }

45

46 void test_set_address(struct test *t, char *address)

47 {

48   strlcpy(t->address, address, sizeof(t->address));

49 }

50

51 void test_display(const struct test *t)

52 {

53   printf("Name:    %s\n", t->name);

54   printf("Address: %s\n", t->address);

55 }

Далее мы создадим наши связки:

gcc -c -fdump-ada-spec -C ./test.h

Это создает следующую спецификацию в test_h.ads:

test_h.ads

1 pragma Ada_2005;

2 pragma Style_Checks (Off);

3

4 with Interfaces.C; use Interfaces.C;

5 with System;

6 with Interfaces.C.Strings;

7

8 package test_h is

9

10    --  skipped empty struct test

11

12    function test_create return System.Address;  -- ./test.h:5

13    pragma Import (C, test_create, "test_create");

14

15    procedure test_destroy (arg1 : System.Address);  -- ./test.h:7

16    pragma Import (C, test_destroy, "test_destroy");

17

18    procedure test_reset (arg1 : System.Address);  -- ./test.h:9

19    pragma Import (C, test_reset, "test_reset");

20

21    procedure test_set_name (arg1 : System.Address; arg2 : Interfaces.C.Strings.chars_ptr);  -- ./test.h:11

22    pragma Import (C, test_set_name, "test_set_name");

23

24    procedure test_set_address (arg1 : System.Address; arg2 : Interfaces.C.Strings.chars_ptr);  -- ./test.h:13

25    pragma Import (C, test_set_address, "test_set_address");

26

27    procedure test_display (arg1 : System.Address);  -- ./test.h:15

28    pragma Import (C, test_display, "test_display");

29

30 end test_h;

Как мы видим, генератор связки полностью игнорирует объявление struct test, и все ссылки на структуру test заменяются адресами (System.Address). Тем не менее, эти связки достаточно хороши, чтобы позволить нам создать тестовое приложение на Ада:

show_automatic_c_struct_bindings.adb

1 with Interfaces.C;         use Interfaces.C;

2 with Interfaces.C.Strings; use Interfaces.C.Strings;

3 with Ada.Text_IO;          use Ada.Text_IO;

4 with test_h;               use test_h;

5

6 with System;

7

8 procedure Show_Automatic_C_Struct_Bindings is

9

10    Name    : constant chars_ptr :=

11      New_String ("John Doe");

12    Address : constant chars_ptr :=

13      New_String ("Small Town");

14

15    T : System.Address := test_create;

16

17 begin

18    test_reset (T);

19    test_set_name (T, Name);

20    test_set_address (T, Address);

21

22    test_display (T);

23    test_destroy (T);

24 end Show_Automatic_C_Struct_Bindings;

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

1. Определить тип Test на основе System.Address и использовать его во всех соответствующих функциях.

2. Удалить префикс test_ во всех операциях с типом Test.

Вот итоговая версия спецификации:

adapted_test_h.ads

1 with Interfaces.C; use Interfaces.C;

2 with System;

3 with Interfaces.C.Strings;

4

5 package adapted_test_h is

6

7    type Test is new System.Address;

8

9    function Create return Test;

10    pragma Import (C, Create, "test_create");

11

12    procedure Destroy (T : Test);

13    pragma Import (C, Destroy, "test_destroy");

14

15    procedure Reset (T : Test);

16    pragma Import (C, Reset, "test_reset");

17

18    procedure Set_Name (T    : Test;

19                        Name : Interfaces.C.Strings.chars_ptr);  -- ./test.h:11

20    pragma Import (C, Set_Name, "test_set_name");

21

22    procedure Set_Address (T       : Test;

23                           Address : Interfaces.C.Strings.chars_ptr);

24    pragma Import (C, Set_Address, "test_set_address");

25

26    procedure Display (T : Test);  -- ./test.h:15

27    pragma Import (C, Display, "test_display");

28

29 end adapted_test_h;

И это соответствующее тело на Аде:

show_adapted_c_struct_bindings.adb

1 with Interfaces.C;         use Interfaces.C;

2 with Interfaces.C.Strings; use Interfaces.C.Strings;

3 with adapted_test_h;       use  adapted_test_h;

4

5 with System;

6

7 procedure Show_Adapted_C_Struct_Bindings is

8

9    Name    : constant chars_ptr :=

10      New_String ("John Doe");

11    Address : constant chars_ptr :=

12      New_String ("Small Town");

13

14    T : Test := Create;

15

16 begin

17    Reset (T);

18    Set_Name (T, Name);

19    Set_Address (T, Address);

20

21    Display (T);

22    Destroy (T);

23 end Show_Adapted_C_Struct_Bindings;

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

Footnotes

Объектно-ориентированное программирование

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

Но одна из моделей, можно сказать, «выиграла» битву за звания "истинного" объектно-ориентированного подхода, хотя бы, если судить только по популярности. Это модель, используется в языке программирования Java, и она очень похожа на модель, используемую в C ++. Вот некоторые важные характеристики:

- Производные типы и расширение типов: Большинство объектно-ориентированных языков позволяют пользователю добавлять новые поля в производные типы.

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

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

- Инкапсуляция: Объекты могут скрывать некоторые свои данные.

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

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

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

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

- Полиморфизм времени выполнения может быть реализован с использованием записей с вариантами.

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

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

Note

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

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

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

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

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

newtypes.ads

1 package Newtypes is

2    type Point is record

3        X, Y : Integer;

4    end record;

5

6    type New_Point is new Point;

7 end Newtypes;

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

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

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

Attention

Обратите внимание: подпрограмма станет примитивом этого типа только в том случае, если:

1. Подпрограмма объявляется в той же области, что и тип и

2. Тип и подпрограмма объявляются в пакете.

primitives.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Primitives is

4   package Week is

5     type Days is (Monday, Tuesday, Wednesday,

6                   Thursday, Friday,

7                   Saturday, Sunday);

8

9      --  Print_Day is a primitive

10      --  of the type Days

11     procedure Print_Day (D : Days);

12   end Week;

13

14   package body Week is

15     procedure Print_Day (D : Days) is

16     begin

17        Put_Line (Days'Image (D));

18     end Print_Day;

19   end Week;

20

21   use Week;

22   type Weekend_Days is new

23     Days range Saturday .. Sunday;

24

25   --  A procedure Print_Day is automatically

26   --  inherited here. It is as if the procedure

27   --

28   --  procedure Print_Day (D : Weekend_Days);

29   --

30   --  has been declared with the same body

31

32   Sat : Weekend_Days := Saturday;

33 begin

34    Print_Day (Sat);

35 end Primitives;

Build output

primitives.adb:11:15: warning: procedure "Print_Day" is not referenced [-gnatwu]

primitives.adb:32:03: warning: "Sat" is not modified, could be declared constant [-gnatwk]

Runtime output

SATURDAY

Этот вид наследования может быть очень полезным и не ограничивается типами записей (вы можете использовать его и для дискретных типов, как в примере выше), но он лишь внешне похож на объектно-ориентированное наследование:

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

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

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

Теговые типы

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

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

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

- Для примитивов может приминяться диспечеризация. Примитив тегового типа - это то, что вы бы назвали методом в Java или C++. Если у вас есть тип, производный от базового типа с переопределенным примитивом, то при вызове примитива для объекта, какой примитив вызовется будет зависить от точного типа объекта в момент исполнения.

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

Давайте посмотрим на наши первые объявления тегового типа:

p.ads

1 package P is

2    type My_Class is tagged null record;

3    --  Just like a regular record, but

4    --  with tagged qualifier

5

6    --  Methods are outside of the type

7    --  definition:

8

9    procedure Foo (Self : in out My_Class);

10    --  If you define a procedure taking a

11    --  My_Class argument in the same package,

12    --  it will be a method.

13

14    --  Here's how you derive a tagged type:

15

16    type Derived is new My_Class with record

17        A : Integer;

18        --  You can add fields in derived types.

19    end record;

20

21    overriding procedure Foo (Self : in out Derived);

22    --  The "overriding" qualifier is optional,

23    --  but if it is present, it must be valid.

24 end P;

p.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body P is

4    procedure Foo (Self : in out My_Class) is

5    begin

6       Put_Line ("In My_Class.Foo");

7    end Foo;

8

9    procedure Foo (Self : in out Derived) is

10    begin

11       Put_Line ("In Derived.Foo, A = "

12                 & Integer'Image (Self.A));

13    end Foo;

14 end P;

Надклассовые типы

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

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

main.adb

1 with P; use P;

2

3 procedure Main is

4

5    O1 : My_Class;

6    --  Declaring an object of type My_Class

7

8    O2 : Derived := (A => 12);

9    --  Declaring an object of type Derived

10

11    O3 : My_Class := O2;

12    --  INVALID: Trying to assign a value

13    --  of type derived to a variable of

14    --  type My_Class.

15 begin

16    null;

17 end Main;

Build output

main.adb:11:21: error: expected type "My_Class" defined at p.ads:2

main.adb:11:21: error: found type "Derived" defined at p.ads:16

gprbuild: *** compilation phase failed

Это связано с тем, что объект типа T имеет в точности тип T, независимо от того, является T теговым или нет. То, что программист пытается тут сказать, это «Я хочу, чтобы O3 содержал объект типа My_Class или любого производного от My_Class типа». Вот как вы это делаете:

main.adb

1 with P; use P;

2

3 procedure Main is

4    O1 : My_Class;

5    --  Declare an object of type My_Class

6

7    O2 : Derived := (A => 12);

8    --  Declare an object of type Derived

9

10    O3 : My_Class'Class := O2;

11    --  Now valid: My_Class'Class designates

12    --  the classwide type for My_Class,

13    --  which is the set of all types

14    --  descending from My_Class (including

15    --  My_Class).

16 begin

17    null;

18 end Main;

Build output

main.adb:4:04: warning: variable "O1" is not referenced [-gnatwu]

main.adb:7:04: warning: "O2" is not modified, could be declared constant [-gnatwk]

main.adb:10:04: warning: variable "O3" is not referenced [-gnatwu]

Attention

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

- Он не может быть использован для поля/компоненты записи

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

Операции диспетчеризации

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

Но если задуматься, переменная типа My_Class всегда содержит объект именно данного типа. Если требуется переменная, которая может содержать My_Class или любой производный тип, она должна иметь тип My_Class'Class.

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

main.adb

1 with P; use P;

2

3 procedure Main is

4    O1 : My_Class;

5    --  Declare an object of type My_Class

6

7    O2 : Derived := (A => 12);

8    --  Declare an object of type Derived

9

10    O3 : My_Class'Class := O2;

11

12    O4 : My_Class'Class := O1;

13 begin

14    Foo (O1);

15    --  Non dispatching: Calls My_Class.Foo

16    Foo (O2);

17    --  Non dispatching: Calls Derived.Foo

18    Foo (O3);

19    --  Dispatching: Calls Derived.Foo

20    Foo (O4);

21    --  Dispatching: Calls My_Class.Foo

22 end Main;

Runtime output

In My_Class.Foo

In Derived.Foo, A =  12

In Derived.Foo, A =  12

In My_Class.Foo

Внимание

Вы можете преобразовать объект типа Derived в объект типа My_Class. В Аде это называется преобразованием представления и полезно, например, если вы хотите вызвать родительский метод.

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

main.adb

1 with P; use P;

2

3 procedure Main is

4    O1 : Derived := (A => 12);

5    --  Declare an object of type Derived

6

7    O2 : My_Class := My_Class (O1);

8

9    O3 : My_Class'Class := O2;

10 begin

11    Foo (O1);

12    --  Non dispatching: Calls Derived.Foo

13    Foo (O2);

14    --  Non dispatching: Calls My_Class.Foo

15

16    Foo (O3);

17    --  Dispatching: Calls My_Class.Foo

18 end Main;

Runtime output

In Derived.Foo, A =  12

In My_Class.Foo

In My_Class.Foo

Точечная нотация

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

main.adb

1 with P; use P;

2

3 procedure Main is

4    O1 : My_Class;

5    --  Declare an object of type My_Class

6

7    O2 : Derived := (A => 12);

8    --  Declare an object of type Derived

9

10    O3 : My_Class'Class := O2;

11

12    O4 : My_Class'Class := O1;

13 begin

14    O1.Foo;

15    --  Non dispatching: Calls My_Class.Foo

16    O2.Foo;

17    --  Non dispatching: Calls Derived.Foo

18    O3.Foo;

19    --  Dispatching: Calls Derived.Foo

20    O4.Foo;

21    --  Dispatching: Calls My_Class.Foo

22 end Main;

Runtime output

In My_Class.Foo

In Derived.Foo, A =  12

In Derived.Foo, A =  12

In My_Class.Foo

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

main.adb

1 with P; use P;

2

3 procedure Main is

4    package Extend is

5       type D2 is new Derived with null record;

6

7       procedure Bar (Self : in out D2;

8                      Val  :        Integer);

9    end Extend;

10

11    package body Extend is

12       procedure Bar (Self : in out D2;

13                      Val  :        Integer) is

14       begin

15          Self.A := Self.A + Val;

16       end Bar;

17    end Extend;

18

19    use Extend;

20

21    Obj : D2 := (A => 15);

22 begin

23    Obj.Bar (2);

24    Obj.Foo;

25 end Main;

Runtime output

In Derived.Foo, A =  17

Личные и лимитируемые типы с тегами

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

Это пример личного тегового типа:

p.ads

1 package P is

2    type T is tagged private;

3 private

4    type T is tagged record

5        E : Integer;

6    end record;

7 end P;

Это пример лимитируемого тегового типа:

p.ads

1 package P is

2    type T is tagged limited record

3        E : Integer;

4    end record;

5 end P;

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

p.ads

1 package P is

2    type T is tagged limited private;

3

4    procedure Init (A : in out T);

5 private

6    type T is tagged limited record

7        E : Integer;

8    end record;

9 end P;

p.adb

1 package body P is

2

3    procedure Init (A : in out T) is

4    begin

5       A.E := 0;

6    end Init;

7

8 end P;

main.adb

1 with P; use P;

2

3 procedure Main is

4   T1, T2 : T;

5 begin

6   T1.Init;

7   T2.Init;

8

9   --  The following line doesn't work

10   --  because type T is private:

11   --

12   --  T1.E := 0;

13

14   --  The following line doesn't work

15   --  because type T is limited:

16   --

17   --  T2 := T1;

18 end Main;

Обратите внимание, что код в процедуре Main имеет два оператора присваивания, которые вызывают ошибки компиляции, потому что тип T является лимитируемым личным. Фактически, вы не можете:

- присваивать T1.E непосредственно, потому что тип T является личным;

- присваивать T1 в T2, потому что тип T ограничен.

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

Надклассовые ссылочные типы

В этом разделе мы обсудим полезный шаблон для объектно-ориентированного программирования в Аде: надклассовые ссылочные типы. Начнем с примера, в котором мы объявляем теговый тип T и производный тип T_New:

p.ads

1 package P is

2    type T is tagged null record;

3

4    procedure Show (Dummy : T);

5

6    type T_New is new T with null record;

7

8    procedure Show (Dummy : T_New);

9 end P;

p.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body P is

4

5    procedure Show (Dummy : T) is

6    begin

7       Put_Line ("Using type "

8                 & T'External_Tag);

9    end Show;

10

11    procedure Show (Dummy : T_New) is

12    begin

13       Put_Line ("Using type "

14                 & T_New'External_Tag);

15    end Show;

16

17 end P;

Обратите внимание, как мы используем пустые записи для типов T и T_New. Хотя эти типы на самом деле не имеют каких-либо компонент, мы по-прежнему можем использовать их для демонстрации диспетчеризации. Также обратите внимание, что в приведенном выше примере используется атрибут 'External_Tag в реализации процедуры Show для получения строки с названием соответствующего тегового типа.

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

dispatching_example.adb

1 with P; use P;

2

3 procedure Dispatching_Example is

4   T2         :          T_New;

5   T_Dispatch : constant T'Class := T2;

6 begin

7   T_Dispatch.Show;

8 end Dispatching_Example;

Runtime output

Using type P.T_NEW

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

for I in T_Arr'Range loop

T_Arr (I).Show;

--  Call Show procedure according

--  to actual type of T_Arr (I)

end loop;

Однако непосредственно объявить массив с элементами T'Class невозможно:

classwide_compilation_error.adb

1 with P; use P;

2

3 procedure Classwide_Compilation_Error is

4   T_Arr  : array (1 .. 2) of T'Class;

5   --                         ^

6   --               Compilation Error!

7 begin

8   for I in T_Arr'Range loop

9      T_Arr (I).Show;

10   end loop;

11 end Classwide_Compilation_Error;

Build output

classwide_compilation_error.adb:4:31: error: unconstrained element type in array declaration

gprbuild: *** compilation phase failed

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

type T_Class is access T'Class;

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

p.ads

1 package P is

2    type T is tagged record

3        E : Integer;

4    end record;

5

6    type T_Class is access T'Class;

7

8    procedure Init (A : in out T);

9

10    procedure Show (Dummy : T);

11

12    type T_New is new T with null record;

13

14    procedure Show (Dummy : T_New);

15

16 end P;

p.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 package body P is

4

5    procedure Init (A : in out T) is

6    begin

7       Put_Line ("Initializing type T...");

8       A.E := 0;

9    end Init;

10

11    procedure Show (Dummy : T) is

12    begin

13       Put_Line ("Using type "

14                 & T'External_Tag);

15    end Show;

16

17    procedure Show (Dummy : T_New) is

18    begin

19       Put_Line ("Using type "

20                 & T_New'External_Tag);

21    end Show;

22

23 end P;

main.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2 with P;           use P;

3

4 procedure Main is

5   T_Arr  : array (1 .. 2) of T_Class;

6 begin

7   T_Arr (1) := new T;

8   T_Arr (2) := new T_New;

9

10   for I in T_Arr'Range loop

11      Put_Line ("Element # "

12                & Integer'Image (I));

13

14      T_Arr (I).Init;

15      T_Arr (I).Show;

16

17      Put_Line ("-----------");

18   end loop;

19 end Main;

Runtime output

Element #  1

Initializing type T...

Using type P.T

-----------

Element #  2

Initializing type T...

Using type P.T_NEW

-----------

В этом примере первый элемент (T_Arr (1)) имеет тип T, а второй элемент - тип T_New. При запуске примера процедура Init типа T вызывается для обоих элементов массива T_Arr, в то время, как вызов процедуры Show выберет нужную процедуру в соответствии с типом каждого элемента T_Arr.

Footnotes

#1

https://ru.wikipedia.org/wiki/Динамическая_идентификация_типа_данных

Стандартная библиотека: Контейнеры

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

Здесь мы представляем введение в контейнеры. Список всех контейнеров, имеющихся в Аде, см. в Приложении B.

Векторы

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

Создание экземпляра

Вот пример, показывающий настройку и объявление вектора V:

show_vector_inst.adb

1 with Ada.Containers.Vectors;

2

3 procedure Show_Vector_Inst is

4

5    package Integer_Vectors is new

6      Ada.Containers.Vectors

7        (Index_Type   => Natural,

8         Element_Type => Integer);

9

10    V : Integer_Vectors.Vector;

11 begin

12    null;

13 end Show_Vector_Inst;

Build output

show_vector_inst.adb:10:04: warning: variable "V" is not referenced [-gnatwu]

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

A : array (1 .. 10) of Integer;

Вместо этого нам сначала нужно создать экземпляр одного из этих пакетов. Мы используем пакет контейнера (в данном случае Ada.Containers.Vectors) и настраиваем его, чтобы создать экземпляр настраиваемого пакета для желаемого типа. Только затем мы сможем объявить вектор, используя тип из созданного пакета. Такая настройка необходима для любого типа контейнера стандартной библиотеки.

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

Инициализация

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

show_vector_init.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_Init is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    V : Vector := 20 & 10 & 0 & 13;

16 begin

17    Put_Line ("Vector has "

18              & Count_Type'Image (V.Length)

19              & " elements");

20 end Show_Vector_Init;

Build output

show_vector_init.adb:15:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Vector has  4 elements

Мы указываем use Integer_Vectors, чтобы получить прямой доступ к типам и операциям из созданного пакета. Кроме того, пример знакомит нас с еще одной операцией вектора: Length, она возвращает количество элементов в векторе. Мы можем использовать точечную нотацию, потому что Vector - это теговый тип, и это позволяет нам писать, как V.Length, так и Length (V).

Добавление элементов

Вы добавляете элементы в вектор с помощью операций Prepend и Append. Как следует из названий, эти операции добавляют элементы в начало или конец вектора соответственно. Например:

show_vector_append.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_Append is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    V : Vector;

16 begin

17    Put_Line ("Appending some elements to the vector...");

18    V.Append (20);

19    V.Append (10);

20    V.Append (0);

21    V.Append (13);

22    Put_Line ("Finished appending.");

23

24    Put_Line ("Prepending some elements to the vector...");

25    V.Prepend (30);

26    V.Prepend (40);

27    V.Prepend (100);

28    Put_Line ("Finished prepending.");

29

30    Put_Line ("Vector has "

31              & Count_Type'Image (V.Length)

32              & " elements");

33 end Show_Vector_Append;

Runtime output

Appending some elements to the vector...

Finished appending.

Prepending some elements to the vector...

Finished prepending.

Vector has  7 elements

В этом примере элементы помещаются в вектор в следующей последовательности: (100, 40, 30, 20, 10, 0, 13).

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

- O(log N) для операции Append и

- O(N log N) для операции Prepend.

Доступ к первому и последнему элементам

Мы получаем доступ к первому и последнему элементам вектора с помощью функций First_Element и Last_Element. Например:

show_vector_first_last_element.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_First_Last_Element is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    function Img (I : Integer)    return String

16      renames Integer'Image;

17    function Img (I : Count_Type) return String

18      renames Count_Type'Image;

19

20    V : Vector := 20 & 10 & 0 & 13;

21 begin

22    Put_Line ("Vector has "

23              & Img (V.Length)

24              & " elements");

25

26    --  Using V.First_Element to

27    --  retrieve first element

28    Put_Line ("First element is "

29              & Img (V.First_Element));

30

31    --  Using V.Last_Element to

32    --  retrieve last element

33    Put_Line ("Last element is "

34              & Img (V.Last_Element));

35 end Show_Vector_First_Last_Element;

Build output

show_vector_first_last_element.adb:20:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Vector has  4 elements

First element is  20

Last element is  13

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

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

show_vector_first_last_element.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_First_Last_Element is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    function Img (I : Integer) return String

16      renames Integer'Image;

17

18    V : Vector := 20 & 10 & 0 & 13;

19 begin

20    --  We use V.First and V.Last to retrieve

21    --  cursor for first and last elements.

22    --  We use V.Swap to swap elements.

23    V.Swap (V.First, V.Last);

24

25    Put_Line ("First element is now "

26              & Img (V.First_Element));

27    Put_Line ("Last element is now "

28              & Img (V.Last_Element));

29 end Show_Vector_First_Last_Element;

Runtime output

First element is now  13

Last element is now  20

Итерация

Самый простой способ перебрать элементы контейнера - использовать цикл for E of Our_Container. Это дает нам ссылку (E) на элемент в текущей позиции. Затем мы можем использовать E непосредственно. Например:

show_vector_iteration.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Vector_Iteration is

6

7    package Integer_Vectors is new

8      Ada.Containers.Vectors

9        (Index_Type   => Natural,

10         Element_Type => Integer);

11

12    use Integer_Vectors;

13

14    function Img (I : Integer) return String

15      renames Integer'Image;

16

17    V : Vector := 20 & 10 & 0 & 13;

18 begin

19    Put_Line ("Vector elements are: ");

20

21    --

22    --  Using for ... of loop to iterate:

23    --

24    for E of V loop

25       Put_Line ("- " & Img (E));

26    end loop;

27

28 end Show_Vector_Iteration;

Build output

show_vector_iteration.adb:17:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Vector elements are:

-  20

-  10

-  0

-  13

Этот код отображает каждый элемент вектора V.

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

for E of V loop

E := E + 1;

end loop;

Мы также можем использовать индексы для доступа к элементам вектора. Формат аналогичен циклу по элементам массива: мы используем в цикле for I in <range>. Диапазон строим из V.First_Index и V.Last_Index. Мы можем обратиться к текущему элементу, используя операцию индексирования массива: V (I). Например:

show_vector_index_iteration.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Vector_Index_Iteration is

6

7    package Integer_Vectors is new

8      Ada.Containers.Vectors

9        (Index_Type   => Natural,

10         Element_Type => Integer);

11

12    use Integer_Vectors;

13

14    V : Vector := 20 & 10 & 0 & 13;

15 begin

16    Put_Line ("Vector elements are: ");

17

18    --

19    --  Using indices in a "for I in ..." loop

20    --  to iterate:

21    --

22    for I in V.First_Index .. V.Last_Index loop

23       --  Displaying current index I

24       Put ("- ["

25            & Extended_Index'Image (I)

26            & "] ");

27

28       Put (Integer'Image (V (I)));

29

30       --  We could also use the V.Element (I)

31       --  function to retrieve the element at

32       --  the current index I

33

34       New_Line;

35    end loop;

36

37 end Show_Vector_Index_Iteration;

Build output

show_vector_index_iteration.adb:14:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Vector elements are:

- [ 0]  20

- [ 1]  10

- [ 2]  0

- [ 3]  13

Здесь, помимо вывода элементов вектора, мы также печатаем каждый индекс I, точно так же, как то, что мы можем сделать для индексов массива. Получить элемент можено, как с помощью краткой формы V (I), так и с помощью вызова функции V.Element (I), но не как V.I.

Как упоминалось в предыдущем разделе, курсоры можно использовать для итерации по контейнерам. Для этого используйте функцию Iterate, которая выдает курсоры для каждой позиции в векторе. Соответствующий цикл имеет формат for C in V.Iterate loop. Как и в предыдущем примере с использованием индексов, можно снова получить доступ к текущему элементу, используя курсор в качестве индекса массива V (C). Например:

show_vector_cursor_iteration.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Vector_Cursor_Iteration is

6

7    package Integer_Vectors is new

8      Ada.Containers.Vectors

9        (Index_Type   => Natural,

10         Element_Type => Integer);

11

12    use Integer_Vectors;

13

14    V : Vector := 20 & 10 & 0 & 13;

15 begin

16    Put_Line ("Vector elements are: ");

17

18    --

19    --  Use a cursor to iterate in a loop:

20    --

21    for C in V.Iterate loop

22       --  Using To_Index function to retrieve

23       --  the index for the cursor position

24       Put ("- ["

25            & Extended_Index'Image (To_Index (C))

26            & "] ");

27

28       Put (Integer'Image (V (C)));

29

30       --  We could use Element (C) to retrieve

31       --  the vector element for the cursor

32       --  position

33

34       New_Line;

35    end loop;

36

37    --  Alternatively, we could iterate with a

38    --  while-loop:

39    --

40    --  declare

41    --     C : Cursor := V.First;

42    --  begin

43    --     while C /= No_Element loop

44    --        some processing here...

45    --

46    --        C := Next (C);

47    --     end loop;

48    --  end;

49

50 end Show_Vector_Cursor_Iteration;

Build output

show_vector_cursor_iteration.adb:14:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Vector elements are:

- [ 0]  20

- [ 1]  10

- [ 2]  0

- [ 3]  13

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

Как показано в комментариях после цикла, мы также можем использовать цикл while ... loop для прохода по вектору. В этом случае мы должны начать с курсора первого элемента (полученного с помощью вызова V.First), а затем вызывать Next (C), чтобы получить курсор для последующих элементов. Функция Next (C) возвращает No_Element, когда курсор достигает конца вектора. Используя курсор вы можете изменять элементы вектора непосредственно.

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

--  Modify vector elements using index

for I in V.First_Index .. V.Last_Index loop

V (I) := V (I) + 1;

end loop;

--  Modify vector elements using cursor

for C in V.Iterate loop

V (C) := V (C) + 1;

end loop;

Справочное руководство требует, чтобы сложность доступа к элементу в наихудшем случае составляла O (log N).

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

show_vector_update.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Vector_Update is

6

7    package Integer_Vectors is new

8      Ada.Containers.Vectors

9        (Index_Type   => Natural,

10         Element_Type => Integer);

11

12    use Integer_Vectors;

13

14    procedure Add_One (I : in out Integer) is

15    begin

16       I := I + 1;

17    end Add_One;

18

19    V : Vector := 20 & 10 & 12;

20 begin

21    --

22    --  Use V.Update_Element to process elements

23    --

24    for C in V.Iterate loop

25       V.Update_Element (C, Add_One'Access);

26    end loop;

27

28 end Show_Vector_Update;

Build output

show_vector_update.adb:3:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

show_vector_update.adb:3:19: warning: use clause for package "Text_IO" has no effect [-gnatwu]

Поиск и изменение элементов

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

show_find_vector_element.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Find_Vector_Element is

6

7    package Integer_Vectors is new

8      Ada.Containers.Vectors

9        (Index_Type   => Natural,

10         Element_Type => Integer);

11

12    use Integer_Vectors;

13

14    V : Vector := 20 & 10 & 0 & 13;

15    Idx : Extended_Index;

16    C   : Cursor;

17 begin

18    --  Using Find_Index to retrieve the index

19    --  of element with value 10

20    Idx := V.Find_Index (10);

21    Put_Line ("Index of element with value 10 is "

22              & Extended_Index'Image (Idx));

23

24    --  Using Find to retrieve the cursor for

25    --  the element with value 13

26    C   := V.Find (13);

27    Idx := To_Index (C);

28    Put_Line ("Index of element with value 13 is "

29              & Extended_Index'Image (Idx));

30 end Show_Find_Vector_Element;

Build output

show_find_vector_element.adb:14:04: warning: "V" is not modified, could be declared constant [-gnatwk]

Runtime output

Index of element with value 10 is  1

Index of element with value 13 is  3

Как мы видели в предыдущем разделе, мы можем осуществлять прямой доступ к элементам вектора используя индекс или курсор. Но, если мы пытаемся получить доступ к элементу с недопустимым индексом или курсором, будет возбуждено исключение, поэтому мы сначала должны проверить, действителен ли индекс или курсор, прежде чем использовать его для доступа к элементу. В нашем примере Find_Index или Find могли не найти элемент в векторе. Мы проверяем эту ситуацию, сравнивая индекс с No_Index или курсора с No_Element. Например:

--  Modify vector element using index

if Idx /= No_Index then

V (Idx) := 11;

end if;

--  Modify vector element using cursor

if C /= No_Element then

V (C) := 14;

end if;

Вместо того, чтобы писать V (C) := 14, мы могли бы использовать более длинную форму V.Replace_Element (C, 14).

Вставка элементов

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

- с помощью оператора конкатенации (&) при объявлении вектора, или

- вызвав процедуры Prepend и Append.

Вам может потребоваться вставить элемент в определенное место, например, перед определенным элементом в векторе. Вы делаете это, вызывая Insert. Например:

show_vector_insert.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_Insert is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    procedure Show_Elements (V : Vector) is

16    begin

17       New_Line;

18       Put_Line ("Vector has "

19                 & Count_Type'Image (V.Length)

20                 & " elements");

21

22       if not V.Is_Empty then

23          Put_Line ("Vector elements are: ");

24          for E of V loop

25             Put_Line ("- " & Integer'Image (E));

26          end loop;

27       end if;

28    end Show_Elements;

29

30    V : Vector := 20 & 10 & 12;

31    C : Cursor;

32 begin

33    Show_Elements (V);

34

35    New_Line;

36    Put_Line ("Adding element with value 9 (before 10)...");

37

38    --

39    --  Using V.Insert to insert the element

40    --  into the vector

41    --

42    C := V.Find (10);

43    if C /= No_Element then

44       V.Insert (C, 9);

45    end if;

46

47    Show_Elements (V);

48

49 end Show_Vector_Insert;

Runtime output

Vector has  3 elements

Vector elements are:

-  20

-  10

-  12

Adding element with value 9 (before 10)...

Vector has  4 elements

Vector elements are:

-  20

-  9

-  10

-  12

В этом примере мы ищем элемент со значением 10. Если он найден, перед ним вставляется элемент со значением 9.

Удаление элементов

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

show_remove_vector_element.adb

1 with Ada.Containers.Vectors;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Remove_Vector_Element is

6    package Integer_Vectors is new

7      Ada.Containers.Vectors

8        (Index_Type   => Natural,

9         Element_Type => Integer);

10

11    use Integer_Vectors;

12

13    V : Vector := 20 & 10 & 0 & 13 & 10 & 13;

14    Idx : Extended_Index;

15    C   : Cursor;

16 begin

17    --  Use Find_Index to retrieve index of

18    --  the element with value 10

19    Idx := V.Find_Index (10);

20

21    --  Checking whether index is valid

22    if Idx /= No_Index then

23       --  Removing element using V.Delete

24       V.Delete (Idx);

25    end if;

26

27    --  Use Find to retrieve cursor for

28    --  the element with value 13

29    C := V.Find (13);

30

31    --  Check whether index is valid

32    if C /= No_Element then

33       --  Remove element using V.Delete

34       V.Delete (C);

35    end if;

36

37 end Show_Remove_Vector_Element;

Build output

show_remove_vector_element.adb:3:09: warning: no entities of "Ada.Text_IO" are referenced [-gnatwu]

show_remove_vector_element.adb:3:19: warning: use clause for package "Text_IO" has no effect [-gnatwu]

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

show_remove_vector_elements.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Remove_Vector_Elements is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    use Integer_Vectors;

14

15    procedure Show_Elements (V : Vector) is

16    begin

17       New_Line;

18       Put_Line ("Vector has "

19                 & Count_Type'Image (V.Length)

20                 & " elements");

21

22       if not V.Is_Empty then

23          Put_Line ("Vector elements are: ");

24          for E of V loop

25             Put_Line ("- " & Integer'Image (E));

26          end loop;

27       end if;

28    end Show_Elements;

29

30    V : Vector := 20 & 10 & 0 & 13 & 10 & 14 & 13;

31 begin

32    Show_Elements (V);

33

34    --

35    --  Remove elements using an index

36    --

37    declare

38       E : constant Integer := 10;

39       I : Extended_Index;

40    begin

41       New_Line;

42       Put_Line ("Removing all elements with value of "

43                 & Integer'Image (E) & "...");

44       loop

45          I := V.Find_Index (E);

46          exit when I = No_Index;

47          V.Delete (I);

48       end loop;

49    end;

50

51    --

52    --  Remove elements using a cursor

53    --

54    declare

55       E : constant Integer := 13;

56       C : Cursor;

57    begin

58       New_Line;

59       Put_Line ("Removing all elements with value of "

60                 & Integer'Image (E) & "...");

61       loop

62          C := V.Find (E);

63          exit when C = No_Element;

64          V.Delete (C);

65       end loop;

66    end;

67

68    Show_Elements (V);

69 end Show_Remove_Vector_Elements;

Runtime output

Vector has  7 elements

Vector elements are:

-  20

-  10

-  0

-  13

-  10

-  14

-  13

Removing all elements with value of  10...

Removing all elements with value of  13...

Vector has  3 elements

Vector elements are:

-  20

-  0

-  14

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

Другие операции

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

Мы выполняем конкатенацию векторов с помощью оператора & для векторов. Рассмотрим два вектора V1 и V2. Мы можем объединить их, выполнив V := V1 & V2. Результирующий вектор содержится в V.

Настраиваемый пакет Generic_Sorting является дочерним пакетом Ada.Containers.Vectors. Он содержит операции сортировки и объединения. Поскольку это настраиваемый пакет, вы не можете использовать его непосредственно, но должны сначала настроить его. Чтобы использовать эти операции с вектором целочисленных значений (Integer_Vectors в нашем примере), вам необходимо настроить пакет дочерний к Integer_Vectors. Следующий пример поясняет, как это сделать.

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

В следующем примере представлен код, который работает тремя векторами (V1, V2, V3) и использует операций конкатенации, сортировки и слияния:

show_vector_ops.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Vectors;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Vector_Ops is

7

8    package Integer_Vectors is new

9      Ada.Containers.Vectors

10        (Index_Type   => Natural,

11         Element_Type => Integer);

12

13    package Integer_Vectors_Sorting is new Integer_Vectors.Generic_Sorting;

14

15    use Integer_Vectors;

16    use Integer_Vectors_Sorting;

17

18    procedure Show_Elements (V : Vector) is

19    begin

20       New_Line;

21       Put_Line ("Vector has "

22                 & Count_Type'Image (V.Length)

23                 & " elements");

24

25       if not V.Is_Empty then

26          Put_Line ("Vector elements are: ");

27          for E of V loop

28             Put_Line ("- " & Integer'Image (E));

29          end loop;

30       end if;

31    end Show_Elements;

32

33    V, V1, V2, V3 : Vector;

34 begin

35    V1 := 10 & 12 & 18;

36    V2 := 11 & 13 & 19;

37    V3 := 15 & 19;

38

39    New_Line;

40    Put_Line ("---- V1 ----");

41    Show_Elements (V1);

42

43    New_Line;

44    Put_Line ("---- V2 ----");

45    Show_Elements (V2);

46

47    New_Line;

48    Put_Line ("---- V3 ----");

49    Show_Elements (V3);

50

51    New_Line;

52    Put_Line ("Concatenating V1, V2 and V3 into V:");

53

54    V := V1 & V2 & V3;

55

56    Show_Elements (V);

57

58    New_Line;

59    Put_Line ("Sorting V:");

60

61    Sort (V);

62

63    Show_Elements (V);

64

65    New_Line;

66    Put_Line ("Merging V2 into V1:");

67

68    Merge (V1, V2);

69

70    Show_Elements (V1);

71

72 end Show_Vector_Ops;

Runtime output

---- V1 ----

Vector has  3 elements

Vector elements are:

-  10

-  12

-  18

---- V2 ----

Vector has  3 elements

Vector elements are:

-  11

-  13

-  19

---- V3 ----

Vector has  2 elements

Vector elements are:

-  15

-  19

Concatenating V1, V2 and V3 into V:

Vector has  8 elements

Vector elements are:

-  10

-  12

-  18

-  11

-  13

-  19

-  15

-  19

Sorting V:

Vector has  8 elements

Vector elements are:

-  10

-  11

-  12

-  13

-  15

-  18

-  19

-  19

Merging V2 into V1:

Vector has  6 elements

Vector elements are:

-  10

-  11

-  12

-  13

-  18

-  19

Справочное руководство требует, чтобы худшей сложностью вызова для сортировки Sort было O(Nsup:2) и средняя сложность должна быть лучше, чем O(Nsup:2).

Множества

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

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

Инициализация и итерация

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

- версия Insert, которая возвращает логическое значение, указывающее, была ли вставка успешной;

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

Чтобы перебрать множество, вы можете использовать цикл for E of S, аналогично векторам. Вы получаете ссылку на элемент в множестве.

Посмотрим на пример:

show_set_init.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Ordered_Sets;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Set_Init is

7

8    package Integer_Sets is new

9      Ada.Containers.Ordered_Sets

10        (Element_Type => Integer);

11

12    use Integer_Sets;

13

14    S : Set;

15    --  Same as:  S : Integer_Sets.Set;

16    C : Cursor;

17    Ins : Boolean;

18 begin

19    S.Insert (20);

20    S.Insert (10);

21    S.Insert (0);

22    S.Insert (13);

23

24    --  Calling S.Insert(0) now would raise

25    --  Constraint_Error because this element

26    --  is already in the set. We instead call a

27    --  version of Insert that doesn't raise an

28    --  exception but instead returns a Boolean

29    --  indicating the status

30

31    S.Insert (0, C, Ins);

32    if not Ins then

33       Put_Line ("Inserting 0 into set was not successful");

34    end if;

35

36    --  We can also call S.Include instead

37    --  If the element is already present,

38    --  the set remains unchanged

39    S.Include (0);

40    S.Include (13);

41    S.Include (14);

42

43    Put_Line ("Set has "

44              & Count_Type'Image (S.Length)

45              & " elements");

46

47    --

48    --  Iterate over set using for .. of loop

49    --

50    Put_Line ("Elements:");

51    for E of S loop

52        Put_Line ("- " & Integer'Image (E));

53    end loop;

54 end Show_Set_Init;

Runtime output

Inserting 0 into set was not successful

Set has  5 elements

Elements:

-  0

-  10

-  13

-  14

-  20

Операции с элементами

В этом разделе мы кратко рассмотрим следующие операции над множествами:

- Delete и Exclude, чтобы удалить элементы;

- Contains и Find, чтобы проверить наличие элементов.

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

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

Давайте рассмотрим пример, в котором используются эти операции:

show_set_element_ops.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Ordered_Sets;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Set_Element_Ops is

7

8    package Integer_Sets is new

9      Ada.Containers.Ordered_Sets

10        (Element_Type => Integer);

11

12    use Integer_Sets;

13

14    procedure Show_Elements (S : Set) is

15    begin

16       New_Line;

17       Put_Line ("Set has "

18                 & Count_Type'Image (S.Length)

19                 & " elements");

20       Put_Line ("Elements:");

21       for E of S loop

22          Put_Line ("- " & Integer'Image (E));

23       end loop;

24    end Show_Elements;

25

26    S : Set;

27 begin

28    S.Insert (20);

29    S.Insert (10);

30    S.Insert (0);

31    S.Insert (13);

32

33    S.Delete (13);

34

35    --  Calling S.Delete (13) again raises

36    --  Constraint_Error because the element

37    --  is no longer present in the set, so

38    --  it can't be deleted. We can call

39    --  V.Exclude instead:

40    S.Exclude (13);

41

42    if S.Contains (20) then

43       Put_Line ("Found element 20 in set");

44    end if;

45

46    --  Alternatively, we could use S.Find

47    --  instead of S.Contains

48    if S.Find (0) /= No_Element then

49       Put_Line ("Found element 0 in set");

50    end if;

51

52    Show_Elements (S);

53 end Show_Set_Element_Ops;

Runtime output

Found element 20 in set

Found element 0 in set

Set has  3 elements

Elements:

-  0

-  10

-  20

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

Операции

Ordered_Sets

Hashed_Sets

- Insert

- Include

- Replace

- Delete

- Exclude

- Find

O((log N) 2) или лучше

O(log N)

Подпрограмма с использованием курсора

O(1)

O(1)

Другие операции

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

Операции над множеством

Оператор

Объединение

or

Пересечение

and

Разность

-

Симметричная разность

xor

В следующем примере используются эти операторы:

show_set_ops.adb

1 with Ada.Containers; use Ada.Containers;

2 with Ada.Containers.Ordered_Sets;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Set_Ops is

7

8    package Integer_Sets is new

9      Ada.Containers.Ordered_Sets

10        (Element_Type => Integer);

11

12    use Integer_Sets;

13

14    procedure Show_Elements (S : Set) is

15    begin

16       Put_Line ("Elements:");

17       for E of S loop

18          Put_Line ("- " & Integer'Image (E));

19       end loop;

20    end Show_Elements;

21

22    procedure Show_Op (S       : Set;

23                       Op_Name : String) is

24    begin

25       New_Line;

26       Put_Line (Op_Name

27                 & "(set #1, set #2) has "

28                 & Count_Type'Image (S.Length)

29                 & " elements");

30    end Show_Op;

31

32    S1, S2, S3 : Set;

33 begin

34    S1.Insert (0);

35    S1.Insert (10);

36    S1.Insert (13);

37

38    S2.Insert (0);

39    S2.Insert (10);

40    S2.Insert (14);

41

42    S3.Insert (0);

43    S3.Insert (10);

44

45    New_Line;

46    Put_Line ("---- Set #1 ----");

47    Show_Elements (S1);

48

49    New_Line;

50    Put_Line ("---- Set #2 ----");

51    Show_Elements (S2);

52

53    New_Line;

54    Put_Line ("---- Set #3 ----");

55    Show_Elements (S3);

56

57    New_Line;

58    if S3.Is_Subset (S1) then

59       Put_Line ("S3 is a subset of S1");

60    else

61       Put_Line ("S3 is not a subset of S1");

62    end if;

63

64    S3 := S1 and S2;

65    Show_Op (S3, "Intersection");

66    Show_Elements (S3);

67

68    S3 := S1 or S2;

69    Show_Op (S3, "Union");

70    Show_Elements (S3);

71

72    S3 := S1 - S2;

73    Show_Op (S3, "Difference");

74    Show_Elements (S3);

75

76    S3 := S1 xor S2;

77    Show_Op (S3, "Symmetric difference");

78    Show_Elements (S3);

79

80 end Show_Set_Ops;

Runtime output

---- Set #1 ----

Elements:

-  0

-  10

-  13

---- Set #2 ----

Elements:

-  0

-  10

-  14

---- Set #3 ----

Elements:

-  0

-  10

S3 is a subset of S1

Intersection(set #1, set #2) has  2 elements

Elements:

-  0

-  10

Union(set #1, set #2) has  4 elements

Elements:

-  0

-  10

-  13

-  14

Difference(set #1, set #2) has  1 elements

Elements:

-  13

Symmetric difference(set #1, set #2) has  2 elements

Elements:

-  13

-  14

Отображения для неопределенных типов

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

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

Хэшированные отображения

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

На других языках

Хэшированные отображения похожи на словари в Python и хэши в Perl. Одно из основных отличий заключается в том, что эти скриптовые языки позволяют использовать разные типы для значений, содержащихся в одном отображении, в то время как в Аде, тип ключа и тип значение указываются в настройке пакета и остаются постоянными для этого конкретного отображения. У вас не может быть отображения, содержащего два элемента или два ключа разног типов. Если вы хотите использовать несколько типов, вы должны создать разные отображения для каждого и использовать только одино из них.

При создании настройке хешированного отображения Ada.Containers.Indefinite_Hashed_Maps мы указываем следующие элементы:

- Key_Type: тип ключа

- Element_Type: тип элемента

- HashKey_Type

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

- Если тип, указанный в Key_Type, имеет стандартный оператор, вы можете использовать его. В примере мы так и делаем. Мы указываем этот оператор как значение Equivalent_Keys.

В следующем примере мы будем использовать строку в качестве типа ключа. Мы будем использовать функцию Hash, предоставляемую стандартной библиотекой для строк (в пакете Ada.Strings), и стандартный оператор равенства.

Вы добавляете элементы в хешированное отображение, вызывая Insert. Если элемент уже содержится в отображении M, вы можете получить к нему доступ непосредственно, используя его ключ. Например, вы можете изменить значение элемента, написав M ("My_Key") := 10. Если ключ не найден, возбуждается исключение. Чтобы проверить, доступен ли ключ, используйте функцию Contains (как мы видели выше в разделе о множествоах).

Посмотрим на пример:

show_hashed_map.adb

1 with Ada.Containers.Indefinite_Hashed_Maps;

2 with Ada.Strings.Hash;

3

4 with Ada.Text_IO; use Ada.Text_IO;

5

6 procedure Show_Hashed_Map is

7

8    package Integer_Hashed_Maps is new

9      Ada.Containers.Indefinite_Hashed_Maps

10        (Key_Type        => String,

11         Element_Type    => Integer,

12         Hash            => Ada.Strings.Hash,

13         Equivalent_Keys => "=");

14

15    use Integer_Hashed_Maps;

16

17    M : Map;

18    --  Same as:

19    --

20    --  M : Integer_Hashed_Maps.Map;

21 begin

22    M.Include ("Alice", 24);

23    M.Include ("John",  40);

24    M.Include ("Bob",   28);

25

26    if M.Contains ("Alice") then

27       Put_Line ("Alice's age is "

28                 & Integer'Image (M ("Alice")));

29    end if;

30

31    --  Update Alice's age

32    --  Key must already exist in M.

33    --  Otherwise an exception is raised.

34    M ("Alice") := 25;

35

36    New_Line; Put_Line ("Name & Age:");

37    for C in M.Iterate loop

38       Put_Line (Key (C) & ": "

39                 & Integer'Image (M (C)));

40    end loop;

41

42 end Show_Hashed_Map;

Runtime output

Alice's age is  24

Name & Age:

John:  40

Bob:  28

Alice:  25

Упорядоченные отображения

Упорядоченные отображения имеют много общих черт с хэшированными отображениями. Основными отличиями являются:

- Хэш-функция не нужна. Вместо этого вы должны предоставить функцию сравнения (< operator), которую упорядоченное отображение будет использовать для сравнения элементов и обеспечения быстрого доступа (сложность O(log N)), используя двоичный поиск.

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

Давайте посмотрим на пример:

show_ordered_map.adb

1 with Ada.Containers.Indefinite_Ordered_Maps;

2

3 with Ada.Text_IO; use Ada.Text_IO;

4

5 procedure Show_Ordered_Map is

6

7    package Integer_Ordered_Maps is new

8      Ada.Containers.Indefinite_Ordered_Maps

9        (Key_Type        => String,

10         Element_Type    => Integer);

11

12    use Integer_Ordered_Maps;

13

14    M : Map;

15 begin

16    M.Include ("Alice", 24);

17    M.Include ("John",  40);

18    M.Include ("Bob",   28);

19

20    if M.Contains ("Alice") then

21       Put_Line ("Alice's age is "

22                 & Integer'Image (M ("Alice")));

23    end if;

24

25    --  Update Alice's age

26    --  Key must already exist in M

27    M ("Alice") := 25;

28

29    New_Line; Put_Line ("Name & Age:");

30    for C in M.Iterate loop

31       Put_Line (Key (C) & ": "

32                 & Integer'Image (M (C)));

33    end loop;

34

35 end Show_Ordered_Map;

Runtime output

Alice's age is  24

Name & Age:

Alice:  25

Bob:  28

John:  40

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

Сложность

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

Справочное руководство требует следующей средней сложности операций:

Операции

Ordered_Maps

Hashed_Maps

- Insert

- Include

- Replace

- Delete

- Exclude

- Find

O((log N) 2) or better

O(log N)

Подпрограмма с использованием курсора

O(1)

O(1)

Footnotes

Стандартная библиотека: Дата и время

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

- Календарный подход, подходящий для обработки дат и времени в целом;

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

Эти два подхода представлены в следующих разделах.

Обработка даты и времени

Пакет Ada.Calendar поддерживает обработку дат и времени. Рассмотрим простой пример:

display_current_time.adb

1 with Ada.Calendar;            use Ada.Calendar;

2 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;

3 with Ada.Text_IO;             use Ada.Text_IO;

4

5 procedure Display_Current_Time is

6    Now : Time := Clock;

7 begin

8    Put_Line ("Current time: " & Image (Now));

9 end Display_Current_Time;

Build output

display_current_time.adb:6:04: warning: "Now" is not modified, could be declared constant [-gnatwk]

Runtime output

Current time: 2022-08-22 21:14:24

В этом примере отображаются текущая дата и время, которые извлекаются при вызове функции Clock. Мы вызываем функцию Image из пакета Ada.Calendar.Formatting, чтобы получить строку (String) для текущей даты и времени. Вместо этого мы могли бы получить каждую компоненту с помощью функции Split. Например:

display_current_year.adb

1 with Ada.Calendar;            use Ada.Calendar;

2 with Ada.Text_IO;             use Ada.Text_IO;

3

4 procedure Display_Current_Year is

5    Now         : Time := Clock;

6

7    Now_Year    : Year_Number;

8    Now_Month   : Month_Number;

9    Now_Day     : Day_Number;

10    Now_Seconds : Day_Duration;

11 begin

12    Split (Now,

13           Now_Year,

14           Now_Month,

15           Now_Day,

16           Now_Seconds);

17

18    Put_Line ("Current year  is: "

19              & Year_Number'Image (Now_Year));

20    Put_Line ("Current month is: "

21              & Month_Number'Image (Now_Month));

22    Put_Line ("Current day   is: "

23              & Day_Number'Image (Now_Day));

24 end Display_Current_Year;

Build output

display_current_year.adb:5:04: warning: "Now" is not modified, could be declared constant [-gnatwk]

Runtime output

Current year  is:  2022

Current month is:  8

Current day   is:  22

Здесь мы получаем каждый элемент и отображаем его отдельно.

Задержка с использованием даты

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

display_delay_next_specific_time.adb

1 with Ada.Calendar;            use Ada.Calendar;

2 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;

3 with Ada.Calendar.Time_Zones; use Ada.Calendar.Time_Zones;

4 with Ada.Text_IO;             use Ada.Text_IO;

5

6 procedure Display_Delay_Next_Specific_Time is

7    TZ   : Time_Offset := UTC_Time_Offset;

8    Next : Time        :=

9      Ada.Calendar.Formatting.Time_Of

10        (Year        => 2018,

11         Month       => 5,

12         Day         => 1,

13         Hour        => 15,

14         Minute      => 0,

15         Second      => 0,

16         Sub_Second  => 0.0,

17         Leap_Second => False,

18         Time_Zone   => TZ);

19

20    --  Next = 2018-05-01 15:00:00.00

21    --         (local time-zone)

22 begin

23    Put_Line ("Let's wait until...");

24    Put_Line (Image (Next, True, TZ));

25

26    delay until Next;

27

28    Put_Line ("Enough waiting!");

29 end Display_Delay_Next_Specific_Time;

Build output

display_delay_next_specific_time.adb:7:04: warning: "TZ" is not modified, could be declared constant [-gnatwk]

display_delay_next_specific_time.adb:8:04: warning: "Next" is not modified, could be declared constant [-gnatwk]

Runtime output

Let's wait until...

2018-05-01 15:00:00.00

Enough waiting!

В этом примере мы указываем дату и время, инициализируя Next с помощью вызова Time_Of, функции, принимающей различные компоненты даты (год, месяц и т. д.) и возвращающей элемент типа Time. Поскольку указанная дата находится в прошлом, задержка delay until не даст заметного эффекта. Если мы укажем дату в будущем, программа будет ждать наступления этой конкретной даты и времени.

Здесь мы переводим время в местный часовой пояс. Если мы не указываем часовой пояс, по умолчанию используется всемирное координированное время (Coordinated Universal Time сокращенно UTC). Получив смещение времени к UTC с помощью вызова UTC_Time_Offset из пакета Ada.Calendar.Time_Zones, мы можем инициализировать TZ и использовать его при вызове Time_Of. Это все, что нам нужно сделать, чтобы информация, предоставляемая Time_Of, относилась к местному часовому поясу.

Мы могли бы добиться аналогичного результата, инициализировав Next с помощью значения типа String. Мы можем сделать это использовав вызов Value из пакета Ada.Calendar.Formatting. Вот модифицированный код:

display_delay_next_specific_time.adb

1 with Ada.Calendar;            use Ada.Calendar;

2 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;

3 with Ada.Calendar.Time_Zones; use Ada.Calendar.Time_Zones;

4 with Ada.Text_IO;             use Ada.Text_IO;

5

6 procedure Display_Delay_Next_Specific_Time is

7    TZ   : Time_Offset := UTC_Time_Offset;

8    Next : Time        :=

9      Ada.Calendar.Formatting.Value

10        ("2018-05-01 15:00:00.00", TZ);

11

12    --  Next = 2018-05-01 15:00:00.00

13    --         (local time-zone)

14 begin

15    Put_Line ("Let's wait until...");

16    Put_Line (Image (Next, True, TZ));

17

18    delay until Next;

19

20    Put_Line ("Enough waiting!");

21 end Display_Delay_Next_Specific_Time;

Build output

display_delay_next_specific_time.adb:7:04: warning: "TZ" is not modified, could be declared constant [-gnatwk]

display_delay_next_specific_time.adb:8:04: warning: "Next" is not modified, could be declared constant [-gnatwk]

Runtime output

Let's wait until...

2018-05-01 15:00:00.00

Enough waiting!

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

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

display_delay_next.adb

1 with Ada.Calendar;            use Ada.Calendar;

2 with Ada.Text_IO;             use Ada.Text_IO;

3

4 procedure Display_Delay_Next is

5    D    : Duration := 5.0;

6    --                 ^ seconds

7    Now  : Time     := Clock;

8    Next : Time     := Now + D;

9    --                       ^ use duration to

10    --                         specify next point

11    --                         in time

12 begin

13    Put_Line ("Let's wait "

14              & Duration'Image (D) & " seconds...");

15    delay until Next;

16    Put_Line ("Enough waiting!");

17 end Display_Delay_Next;

Build output

display_delay_next.adb:5:04: warning: "D" is not modified, could be declared constant [-gnatwk]

display_delay_next.adb:7:04: warning: "Now" is not modified, could be declared constant [-gnatwk]

display_delay_next.adb:8:04: warning: "Next" is not modified, could be declared constant [-gnatwk]

Runtime output

Let's wait  5.000000000 seconds...

Enough waiting!

Здесь мы указываем продолжительность 5 секунд в D, добавляем ее к текущему времени из Now и сохраняем сумму в Next. Затем мы используем его в операторе delay until.

Режим реального времени

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

В предыдущем разделе мы использовали тип Time из Ada.Calendar и оператор delay until, чтобы отложить приложение на 5 секунд. Вместо этого мы могли бы использовать пакет Ada.Real_Time. Давайте изменим этот пример:

display_delay_next_real_time.adb

1 with Ada.Text_IO;   use Ada.Text_IO;

2 with Ada.Real_Time; use Ada.Real_Time;

3

4 procedure Display_Delay_Next_Real_Time is

5    D     : Time_Span := Seconds (5);

6    Next  : Time      := Clock + D;

7 begin

8    Put_Line ("Let's wait "

9              & Duration'Image (To_Duration (D))

10              & " seconds...");

11    delay until Next;

12    Put_Line ("Enough waiting!");

13 end Display_Delay_Next_Real_Time;

Build output

display_delay_next_real_time.adb:5:04: warning: "D" is not modified, could be declared constant [-gnatwk]

display_delay_next_real_time.adb:6:04: warning: "Next" is not modified, could be declared constant [-gnatwk]

Runtime output

Let's wait  5.000000000 seconds...

Enough waiting!

Основное отличие состоит в том, что D теперь является переменной типа Time_Span, определенной в пакете Ada.Real_Time. Мы вызываем функцию Seconds для инициализации D, но мы могли бы получить более тончное значение, вызвав вместо этого Nanoseconds. Кроме того, нам нужно сначала преобразовать D в тип Duration с помощью функции To_Duration, прежде чем мы сможем его напечатать.

Анализ производительности

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

display_benchmarking.adb

1 with Ada.Text_IO;   use Ada.Text_IO;

2 with Ada.Real_Time; use Ada.Real_Time;

3

4 procedure Display_Benchmarking is

5

6    procedure Computational_Intensive_App is

7    begin

8       delay 5.0;

9    end Computational_Intensive_App;

10

11    Start_Time, Stop_Time : Time;

12    Elapsed_Time          : Time_Span;

13

14 begin

15    Start_Time := Clock;

16

17    Computational_Intensive_App;

18

19    Stop_Time    := Clock;

20    Elapsed_Time := Stop_Time - Start_Time;

21

22    Put_Line ("Elapsed time: "

23              & Duration'Image (To_Duration (Elapsed_Time))

24              & " seconds");

25 end Display_Benchmarking;

Runtime output

Elapsed time:  5.007097508 seconds

В этом примере определяется фиктивное приложение Computational_Intensive_App, реализованное с использованием простого оператора задержки delay. Мы инициализируем Start_Time и Stop_Time по текущим на тот момент часам и вычисляем прошедшее время. Запустив эту программу, мы видим, что время составляет примерно 5 секунд, что соответствует реботе оператора задержки delay.

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

display_benchmarking_cpu_time.adb

1 with Ada.Text_IO;        use Ada.Text_IO;

2 with Ada.Real_Time;      use Ada.Real_Time;

3 with Ada.Execution_Time; use Ada.Execution_Time;

4

5 procedure Display_Benchmarking_CPU_Time is

6

7    procedure Computational_Intensive_App is

8    begin

9       delay 5.0;

10    end Computational_Intensive_App;

11

12    Start_Time, Stop_Time : CPU_Time;

13    Elapsed_Time          : Time_Span;

14

15 begin

16    Start_Time := Clock;

17

18    Computational_Intensive_App;

19

20    Stop_Time    := Clock;

21    Elapsed_Time := Stop_Time - Start_Time;

22

23    Put_Line ("CPU time: "

24              & Duration'Image (To_Duration (Elapsed_Time))

25              & " seconds");

26 end Display_Benchmarking_CPU_Time;

Runtime output

CPU time:  0.000125123 seconds

В этом примере Start_Time и Stop_Time имеют тип CPU_Time вместо Time. Однако мы по-прежнему вызываем функцию Clock для инициализации обеих переменных и вычисления прошедшего времени так же, как и раньше. Запустив эту программу, мы видим, что время процессора значительно ниже, чем те 5 секунд, которые мы видели раньше. Это связано с тем, что оператор задержки delay не требует много времени процессора. Результаты будут другими, если мы изменим реализацию Computational_Intensive_App для использования математических функций в длинном цикле. Например:

display_benchmarking_math.adb

1 with Ada.Text_IO;        use Ada.Text_IO;

2 with Ada.Real_Time;      use Ada.Real_Time;

3 with Ada.Execution_Time; use Ada.Execution_Time;

4

5 with Ada.Numerics.Generic_Elementary_Functions;

6

7 procedure Display_Benchmarking_Math is

8

9    procedure Computational_Intensive_App is

10       package Funcs is new Ada.Numerics.Generic_Elementary_Functions

11         (Float_Type => Long_Long_Float);

12       use Funcs;

13

14       X : Long_Long_Float;

15    begin

16       for I in 0 .. 1_000_000 loop

17          X := Tan (Arctan

18                    (Tan (Arctan

19                       (Tan (Arctan

20                          (Tan (Arctan

21                             (Tan (Arctan

22                                (Tan (Arctan

23                                   (0.577))))))))))));

24       end loop;

25    end Computational_Intensive_App;

26

27    procedure Benchm_Elapsed_Time is

28       Start_Time, Stop_Time : Time;

29       Elapsed_Time          : Time_Span;

30

31    begin

32       Start_Time := Clock;

33

34       Computational_Intensive_App;

35

36       Stop_Time    := Clock;

37       Elapsed_Time := Stop_Time - Start_Time;

38

39       Put_Line ("Elapsed time: "

40                 & Duration'Image (To_Duration (Elapsed_Time))

41                 & " seconds");

42    end Benchm_Elapsed_Time;

43

44    procedure Benchm_CPU_Time is

45       Start_Time, Stop_Time : CPU_Time;

46       Elapsed_Time          : Time_Span;

47

48    begin

49       Start_Time := Clock;

50

51       Computational_Intensive_App;

52

53       Stop_Time    := Clock;

54       Elapsed_Time := Stop_Time - Start_Time;

55

56       Put_Line ("CPU time: "

57                 & Duration'Image (To_Duration (Elapsed_Time))

58                 & " seconds");

59    end Benchm_CPU_Time;

60 begin

61    Benchm_Elapsed_Time;

62    Benchm_CPU_Time;

63 end Display_Benchmarking_Math;

Build output

display_benchmarking_math.adb:14:07: warning: variable "X" is assigned but never read [-gnatwm]

Runtime output

Elapsed time:  0.871429605 seconds

CPU time:  0.876544727 seconds

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

Footnotes

Стандартная библиотека: Строки

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

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

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

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

Операции со строками

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

Одна из самых простых операций - это подсчет количества подстрок, доступных в строке (Count), и поиск их соответствующих индексов (Index). Давайте посмотрим на пример:

show_find_substring.adb

1 with Ada.Strings.Fixed; use Ada.Strings.Fixed;

2 with Ada.Text_IO;       use Ada.Text_IO;

3

4 procedure Show_Find_Substring is

5

6    S   : String := "Hello" & 3 * " World";

7    P   : constant String := "World";

8    Idx : Natural;

9    Cnt : Natural;

10 begin

11    Cnt := Ada.Strings.Fixed.Count

12      (Source  => S,

13       Pattern => P);

14

15    Put_Line ("String: " & S);

16    Put_Line ("Count for '" & P & "': "

17              & Natural'Image (Cnt));

18

19    Idx := 0;

20    for I in 1 .. Cnt loop

21       Idx := Index

22         (Source  => S,

23          Pattern => P,

24          From    => Idx + 1);

25

26       Put_Line ("Found instance of '"

27                 & P & "' at position: "

28                 & Natural'Image (Idx));

29    end loop;

30

31 end Show_Find_Substring;

Build output

show_find_substring.adb:6:04: warning: "S" is not modified, could be declared constant [-gnatwk]

Runtime output

String: Hello World World World

Count for 'World':  3

Found instance of 'World' at position:  7

Found instance of 'World' at position:  13

Found instance of 'World' at position:  19

Мы инициализируем строку S умножением. Запись "Hello" & 3 * " World" создает строку Hello World World World. Затем мы вызываем функцию Count, чтобы получить количество экземпляров слова World в S .Затем мы вызываем функцию Index в цикле, чтобы найти индекс каждого экземпляра слова World в S.

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

show_find_words.adb

1 with Ada.Strings;       use Ada.Strings;

2 with Ada.Strings.Fixed; use Ada.Strings.Fixed;

3 with Ada.Strings.Maps;  use Ada.Strings.Maps;

4 with Ada.Text_IO;       use Ada.Text_IO;

5

6 procedure Show_Find_Words is

7

8    S   : String := "Hello" & 3 * " World";

9    F   : Positive;

10    L   : Natural;

11    I   : Natural := 1;

12

13    Whitespace : constant Character_Set :=

14      To_Set (' ');

15 begin

16    Put_Line ("String: " & S);

17    Put_Line ("String length: "

18              & Integer'Image (S'Length));

19

20    while I in S'Range loop

21       Find_Token

22         (Source  => S,

23          Set     => Whitespace,

24          From    => I,

25          Test    => Outside,

26          First   => F,

27          Last    => L);

28

29       exit when L = 0;

30

31       Put_Line ("Found word instance at position "

32                 & Natural'Image (F)

33                 & ": '" & S (F .. L) & "'");

34       --   & "-" & F'Img & "-" & L'Img

35

36       I := L + 1;

37    end loop;

38 end Show_Find_Words;

Build output

show_find_words.adb:8:04: warning: "S" is not modified, could be declared constant [-gnatwk]

Runtime output

String: Hello World World World

String length:  23

Found word instance at position  1: 'Hello'

Found word instance at position  7: 'World'

Found word instance at position  13: 'World'

Found word instance at position  19: 'World'

Мы передаем множество символов, которые будут использоваться в качестве разделителей для процедуры Find_Token. Это множество является значением типа Character_Set из пакета Ada.Strings.Maps. Мы вызываем функцию To_Set (из того же пакета), чтобы инициализировать множество Whitespace, а затем вызываем Find_Token, чтобы перебрать все возможные индексы и найти начальный индекс каждого слова. Мы передаем Outside в параметр Test процедуры Find_Token, чтобы указать, что мы ищем индексы, которые находятся за пределами множества Whitespace, то есть фактические слова. First и Last параметры Find_Token являются выходными параметрами, которые указывают допустимый диапазон подстроки. Мы используем эту информацию для отображения строки (S (F .. L)).

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

Operation

Description

Insert

Вставка подстроки в строку

Overwrite

Замена подстроки в строке

Delete

Удаление подстроки

Trim

Удаление пробелов из строки

Все эти операции доступны как в виде функций, так и в виде процедур. Функции создают новую строку, но процедуры выполняют операции по месту. Процедура вызовет исключение, если ограничения строки будут нарушены. Например, если у нас есть строка S, содержащая 10 символов, вставка в нее строки с двумя символами (например, "!!") дает строку, содержащую 12 символов. Поскольку она имеет фиксированную длину, мы не можем увеличить его размер. Одно из возможных решений в этом случае - указать, что при вставке подстроки следует применять усечение. Это сохраняет длину S. Давайте посмотрим на пример, в котором используются как функции, так и версии процедур Insert, Overwrite и Delete:

show_adapted_strings.adb

1 with Ada.Strings;       use Ada.Strings;

2 with Ada.Strings.Fixed; use Ada.Strings.Fixed;

3 with Ada.Text_IO;       use Ada.Text_IO;

4

5 procedure Show_Adapted_Strings is

6

7    S   : String := "Hello World";

8    P   : constant String := "World";

9    N   : constant String := "Beautiful";

10

11    procedure Display_Adapted_String

12      (Source   : String;

13       Before   : Positive;

14       New_Item : String;

15       Pattern  : String)

16    is

17       S_Ins_In : String := Source;

18       S_Ovr_In : String := Source;

19       S_Del_In : String := Source;

20

21       S_Ins : String :=

22         Insert (Source,

23                 Before,

24                 New_Item & " ");

25       S_Ovr : String :=

26         Overwrite (Source,

27                    Before,

28                    New_Item);

29       S_Del : String :=

30         Trim (Delete (Source,

31                       Before,

32                       Before + Pattern'Length - 1),

33               Ada.Strings.Right);

34    begin

35       Insert (S_Ins_In,

36               Before,

37               New_Item,

38               Right);

39

40       Overwrite (S_Ovr_In,

41                  Before,

42                  New_Item,

43                  Right);

44

45       Delete (S_Del_In,

46               Before,

47               Before + Pattern'Length - 1);

48

49       Put_Line ("Original:  '"

50                 & Source & "'");

51

52       Put_Line ("Insert:    '"

53                 & S_Ins  & "'");

54       Put_Line ("Overwrite: '"

55                 & S_Ovr  & "'");

56       Put_Line ("Delete:    '"

57                 & S_Del  & "'");

58

59       Put_Line ("Insert    (in-place): '"

60                 & S_Ins_In & "'");

61       Put_Line ("Overwrite (in-place): '"

62                 & S_Ovr_In & "'");

63       Put_Line ("Delete    (in-place): '"

64                 & S_Del_In & "'");

65    end Display_Adapted_String;

66

67    Idx : Natural;

68 begin

69    Idx := Index

70      (Source  => S,

71       Pattern => P);

72

73    if Idx > 0 then

74       Display_Adapted_String (S, Idx, N, P);

75    end if;

76 end Show_Adapted_Strings;

Build output

show_adapted_strings.adb:7:04: warning: "S" is not modified, could be declared constant [-gnatwk]

show_adapted_strings.adb:21:07: warning: "S_Ins" is not modified, could be declared constant [-gnatwk]

show_adapted_strings.adb:25:07: warning: "S_Ovr" is not modified, could be declared constant [-gnatwk]

show_adapted_strings.adb:29:07: warning: "S_Del" is not modified, could be declared constant [-gnatwk]

Runtime output

Original:  'Hello World'

Insert:    'Hello Beautiful World'

Overwrite: 'Hello Beautiful'

Delete:    'Hello'

Insert    (in-place): 'Hello Beaut'

Overwrite (in-place): 'Hello Beaut'

Delete    (in-place): 'Hello      '

В этом примере мы ищем индекс подстроки World и выполняем операции с этой подстрокой внутри внешней строки. Процедура Display_Adapted_String использует обе версии операций. Для процедурной версии Insert и Overwrite мы применяем усечение к правой стороне строки (Right). Для процедуры Delete мы указываем диапазон подстроки, которая заменяется пробелами. Для функциональной версии Delete мы также вызываем Trim. Эта функция которая обрезает конечные пробелы.

Ограничение строк фиксированной длины

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

show_char_array.adb

1 with Ada.Text_IO;         use Ada.Text_IO;

2

3 procedure Show_Char_Array is

4    S : String (1 .. 15);

5    --  Strings are arrays of Character

6 begin

7    S := "Hello          ";

8    --  Alternatively:

9    --

10    --  #1:

11    --      S (1 .. 5)      := "Hello";

12    --      S (6 .. S'Last) := (others => ' ');

13    --

14    --  #2:

15    --      S := ('H', 'e', 'l', 'l', 'o',

16    --            others => ' ');

17

18    Put_Line ("String: " & S);

19    Put_Line ("String Length: "

20              & Integer'Image (S'Length));

21 end Show_Char_Array;

Runtime output

String: Hello

String Length:  15

В этом случае мы не можем просто написать S := "Hello", потому что результирующий массив символов для константы Hello имеет длину, отличную от длины строки S .Следовательно, нам необходимо включить конечные пробелы, чтобы соответствовать длине S. Как показано в примере, мы могли бы использовать точный диапазон для инициализации (S (1 .. 5)) или использовать явный массив отдельных символов.

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

Ограниченные строки

Ограниченные строки определены в пакете Ada.Strings.Bounded.Generic_Bounded_Length. Поскольку это общий пакет, вам необходимо создать его экземпляр и установить максимальную длину ограниченной строки. Затем вы можете объявить ограниченные строки типа Bounded_String.

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

show_bounded_string.adb

1 with Ada.Strings;         use Ada.Strings;

2 with Ada.Strings.Bounded;

3 with Ada.Text_IO;         use Ada.Text_IO;

4

5 procedure Show_Bounded_String is

6    package B_Str is new

7      Ada.Strings.Bounded.Generic_Bounded_Length (Max => 15);

8    use B_Str;

9

10    S1, S2 : Bounded_String;

11

12    procedure Display_String_Info (S : Bounded_String) is

13    begin

14       Put_Line ("String: " & To_String (S));

15       Put_Line ("String Length: "

16                 & Integer'Image (Length (S)));

17       --  String:

18       --          S'Length => ok

19       --  Bounded_String:

20       --          S'Length => compilation error:

21       --                      bounded strings are

22       --                      not arrays!

23

24       Put_Line ("Max.   Length: "

25                 & Integer'Image (Max_Length));

26    end Display_String_Info;

27 begin

28    S1 := To_Bounded_String ("Hello");

29    Display_String_Info (S1);

30

31    S2 := To_Bounded_String ("Hello World");

32    Display_String_Info (S2);

33

34    S1 := To_Bounded_String

35      ("Something longer to say here...",

36       Right);

37    Display_String_Info (S1);

38 end Show_Bounded_String;

Runtime output

String: Hello

String Length:  5

Max.   Length:  15

String: Hello World

String Length:  11

Max.   Length:  15

String: Something longe

String Length:  15

Max.   Length:  15

Используя ограниченные строки, мы можем легко назначить S1 и S2 несколько раз во время выполнения. Мы используем функции To_Bounded_String и To_String для преобразования в соответствующем направлении между строками фиксированной длины и ограниченными строками. Вызов To_Bounded_String возбуждает исключение, если длина входной строки больше максимальной длинны ограниченной строки. Чтобы этого избежать, мы можем использовать параметр усечения (Right в нашем примере).

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

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

show_bounded_string_op.adb

1 with Ada.Strings;         use Ada.Strings;

2 with Ada.Strings.Bounded;

3 with Ada.Text_IO;         use Ada.Text_IO;

4

5 procedure Show_Bounded_String_Op is

6    package B_Str is new

7      Ada.Strings.Bounded.Generic_Bounded_Length (Max => 30);

8    use B_Str;

9

10    S1, S2 : Bounded_String;

11 begin

12    S1 := To_Bounded_String ("Hello");

13    --  Alternatively:

14    --

15    --  A := Null_Bounded_String & "Hello";

16

17    Append (S1, " World");

18    --  Alternatively: Append (A, " World", Right);

19

20    Put_Line ("String: " & To_String (S1));

21

22    S2 := To_Bounded_String ("Hello!");

23    S1 := S1 & " " & S2;

24    Put_Line ("String: " & To_String (S1));

25 end Show_Bounded_String_Op;

Runtime output

String: Hello World

String: Hello World Hello!

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

Неограниченные строки

Неограниченные строки определяются в пакете Ada.Strings.Unbounded. Это не настраиваемый пакет, поэтому нам не нужно создавать его экземпляр перед использованием Unbounded_String типа. Как можно вспомнить из предыдущего раздела, для ограниченных строк требуется настройка пакета.

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

Инициализация неограниченных строк очень похожа на ограниченные строки. Рассмотрим пример:

show_unbounded_string.adb

1 with Ada.Strings;           use Ada.Strings;

2 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

3 with Ada.Text_IO;           use Ada.Text_IO;

4

5 procedure Show_Unbounded_String is

6    S1, S2 : Unbounded_String;

7

8    procedure Display_String_Info (S : Unbounded_String) is

9    begin

10       Put_Line ("String: " & To_String (S));

11       Put_Line ("String Length: "

12                 & Integer'Image (Length (S)));

13    end Display_String_Info;

14 begin

15    S1 := To_Unbounded_String ("Hello");

16    --  Alternatively:

17    --

18    --  A := Null_Unbounded_String & "Hello";

19

20    Display_String_Info (S1);

21

22    S2 := To_Unbounded_String ("Hello World");

23    Display_String_Info (S2);

24

25    S1 := To_Unbounded_String ("Something longer to say here...");

26    Display_String_Info (S1);

27 end Show_Unbounded_String;

Runtime output

String: Hello

String Length:  5

String: Hello World

String Length:  11

String: Something longer to say here...

String Length:  31

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

Как и для ограниченных строк, можно использовать процедуру Append и оператор & для неограниченных строк. Например:

show_unbounded_string_op.adb

1 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

2 with Ada.Text_IO;           use Ada.Text_IO;

3

4 procedure Show_Unbounded_String_Op is

5    S1, S2 : Unbounded_String := Null_Unbounded_String;

6 begin

7    S1 := S1 & "Hello";

8    S2 := S2 & "Hello!";

9

10    Append (S1, " World");

11    Put_Line ("String: " & To_String (S1));

12

13    S1 := S1 & " " & S2;

14    Put_Line ("String: " & To_String (S1));

15 end Show_Unbounded_String_Op;

Runtime output

String: Hello World

String: Hello World Hello!

Footnotes

Стандартная библиотека: Файлы и потоки

Ада предлагает различные средства для ввода/вывода файлов (I/O):

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

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

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

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

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

Опция ввода- вывода файлов

Формат

Произвольный доступ

Типы данных

Text I/O

текст

строковый тип

Sequential I/O

двоичный

одиночный тип

Direct I/O

двоичный

да

одиночный тип

Stream I/O

двоичный

да

несколько типов

В следующих разделах мы подробно обсудим эти средства ввода-вывода.

Текстовый ввод-вывод

В большинстве примеров этого курса мы использовали процедуру Put_Line для отображения информации на консоли. Однако эта процедура также принимает параметр File_Type. Например, вы можете выбрать между стандартным выводом (Standard_Output) и выводом стандартной ошибкой (Standard_Error), явно задав этот параметр:

show_std_text_out.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Std_Text_Out is

4 begin

5    Put_Line (Standard_Output, "Hello World #1");

6    Put_Line (Standard_Error,  "Hello World #2");

7 end Show_Std_Text_Out;

Runtime output

Hello World #1

Hello World #2

Вы также можете использовать этот параметр для записи информации в любой текстовый файл. Чтобы создать новый файл для записи, используйте процедуру Create для инициализации объекта File_Type, который впоследствии можно передать в Put_Line (вместо, например, Standard_Output). После того, как вы закончите запись информации, вы можете закрыть файл, вызвав процедуру Close.

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

Давайте посмотрим на пример, который записывает информацию в новый текстовый файл, а затем считывает ее обратно из того же файла:

show_simple_text_file_io.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Simple_Text_File_IO is

4    F         : File_Type;

5    File_Name : constant String := "simple.txt";

6 begin

7    Create (F, Out_File, File_Name);

8    Put_Line (F, "Hello World #1");

9    Put_Line (F, "Hello World #2");

10    Put_Line (F, "Hello World #3");

11    Close (F);

12

13    Open (F, In_File, File_Name);

14    while not End_Of_File (F) loop

15       Put_Line (Get_Line (F));

16    end loop;

17    Close (F);

18 end Show_Simple_Text_File_IO;

Runtime output

Hello World #1

Hello World #2

Hello World #3

В дополнение к процедурам Create и Close стандартная библиотека также включает процедуру Reset, которая, как следует из названия, сбрасывает (стирает) всю информацию из файла. Например:

show_text_file_reset.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Text_File_Reset is

4    F         : File_Type;

5    File_Name : constant String := "simple.txt";

6 begin

7    Create (F, Out_File, File_Name);

8    Put_Line (F, "Hello World #1");

9    Reset (F);

10    Put_Line (F, "Hello World #2");

11    Close (F);

12

13    Open (F, In_File, File_Name);

14    while not End_Of_File (F) loop

15       Put_Line (Get_Line (F));

16    end loop;

17    Close (F);

18 end Show_Text_File_Reset;

Runtime output

Hello World #2

Запустив эту программу, мы замечаем, что, хотя мы записали первую строку ("Hello World #1") в файл, она была удалена вызовом процедуры сброса Reset.

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

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

show_text_file_input_except.adb

1 with Ada.Text_IO; use Ada.Text_IO;

2

3 procedure Show_Text_File_Input_Except is

4    F         : File_Type;

5    File_Name : constant String := "simple.txt";

6 begin

7    --  Open output file and delete it

8    Create (F, Out_File, File_Name);

9    Delete (F);

10

11    --  Try to open deleted file

12    Open (F, In_File, File_Name);

13    Close (F);

14 exception

15    when Name_Error =>

16       Put_Line ("File does not exist");

17    when others =>

18       Put_Line ("Error while processing input file");

19 end Show_Text_File_Input_Except;

Runtime output

File does not exist

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

Последовательный ввод-вывод

В предыдущем разделе представлена подробная информация о вводе/выводе текстовых файлов. Здесь мы обсудим выполнение операций ввода-вывода для файловов двоичного формата. Первый пакет, который мы рассмотрим - это Ada.Sequential_IO. Поскольку этот пакет является настраиваемым, необходимо его настроить на тип данных ввод/вывлд которого мы будем производить. После этого можно использовать те же процедуры, что и в предыдущем разделе: Create, Open, Close, Reset и Delete. Однако вместо вызова процедур Get_Line и Put_Line следует вызвать процедуры Read и Write.

В следующем примере создается настройка пакета Ada.Sequential_IO для типов с плавающей запятой:

show_seq_float_io.adb

1 with Ada.Text_IO;

2 with Ada.Sequential_IO;

3

4 procedure Show_Seq_Float_IO is

5    package Float_IO is

6      new Ada.Sequential_IO (Float);

7    use Float_IO;

8

9    F         : Float_IO.File_Type;

10    File_Name : constant String := "float_file.bin";

11 begin

12    Create (F, Out_File, File_Name);

13    Write (F,  1.5);

14    Write (F,  2.4);

15    Write (F,  6.7);

16    Close (F);

17

18    declare

19       Value : Float;

20    begin

21       Open (F, In_File, File_Name);

22       while not End_Of_File (F) loop

23          Read (F, Value);

24          Ada.Text_IO.Put_Line (Float'Image (Value));

25       end loop;

26       Close (F);

27    end;

28 end Show_Seq_Float_IO;

Runtime output

1.50000E+00

2.40000E+00

6.70000E+00

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

show_seq_rec_io.adb

1 with Ada.Text_IO;

2 with Ada.Sequential_IO;

3

4 procedure Show_Seq_Rec_IO is

5    type Num_Info is record

6       Valid : Boolean := False;

7       Value : Float;

8    end record;

9

10    procedure Put_Line (N : Num_Info) is

11    begin

12       if N.Valid then

13          Ada.Text_IO.Put_Line ("(ok,     "

14                                & Float'Image (N.Value) & ")");

15       else

16          Ada.Text_IO.Put_Line ("(not ok,  -----------)");

17       end if;

18    end Put_Line;

19

20    package Num_Info_IO is new Ada.Sequential_IO (Num_Info);

21    use Num_Info_IO;

22

23    F         : Num_Info_IO.File_Type;

24    File_Name : constant String := "float_file.bin";

25 begin

26    Create (F, Out_File, File_Name);

27    Write (F,  (True,  1.5));

28    Write (F,  (False, 2.4));

29    Write (F,  (True,  6.7));

30    Close (F);

31

32    declare

33       Value : Num_Info;

34    begin

35       Open (F, In_File, File_Name);

36       while not End_Of_File (F) loop

37          Read (F, Value);

38          Put_Line (Value);

39       end loop;

40       Close (F);

41    end;

42 end Show_Seq_Rec_IO;

Runtime output

(ok,      1.50000E+00)

(not ok,  -----------)

(ok,      6.70000E+00)

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

Прямой ввод-вывод

Прямой ввод-вывод доступен в пакете Ada.Direct_IO. Этот механизм похож на только что представленный последовательный ввод-вывод, но позволяет нам получить доступ к любой позиции в файле. Создание пакета и большинство операций очень похожи на последовательный ввод-вывод. Чтобы переписать приложение Show_Seq_Float_IO из предыдущего раздела для использования пакета Ada.Direct_IO, нам просто нужно заменить Ada.Sequential_IO на Ada.Direct_IO в настройке пакета. Это новый исходный код:

show_dir_float_io.adb

1 with Ada.Text_IO;

2 with Ada.Direct_IO;

3

4 procedure Show_Dir_Float_IO is

5    package Float_IO is new Ada.Direct_IO (Float);

6    use Float_IO;

7

8    F         : Float_IO.File_Type;

9    File_Name : constant String := "float_file.bin";

10 begin

11    Create (F, Out_File, File_Name);

12    Write (F,  1.5);

13    Write (F,  2.4);

14    Write (F,  6.7);

15    Close (F);

16

17    declare

18       Value : Float;

19    begin

20       Open (F, In_File, File_Name);

21       while not End_Of_File (F) loop

22          Read (F, Value);

23          Ada.Text_IO.Put_Line (Float'Image (Value));

24       end loop;

25       Close (F);

26    end;

27 end Show_Dir_Float_IO;

Runtime output

1.50000E+00

2.40000E+00

6.70000E+00

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

Чтобы получить доступ к нужной позиции в файле, вызовите процедуру Set_Index и она установит новую позицию / индекс. Вы можете использовать функцию Index чтобы узать текущий индекс. Посмотрим на пример:

show_dir_float_in_out_file.adb

1 with Ada.Text_IO;

2 with Ada.Direct_IO;

3

4 procedure Show_Dir_Float_In_Out_File is

5    package Float_IO is new Ada.Direct_IO (Float);

6    use Float_IO;

7

8    F         : Float_IO.File_Type;

9    File_Name : constant String := "float_file.bin";

10 begin

11    --  Open file for input / output

12    Create (F, Inout_File, File_Name);

13    Write (F,  1.5);

14    Write (F,  2.4);

15    Write (F,  6.7);

16

17    --  Set index to previous position and overwrite value

18    Set_Index (F, Index (F) - 1);

19    Write (F,  7.7);

20

21    declare

22       Value : Float;

23    begin

24       --  Set index to start of file

25       Set_Index (F, 1);

26

27       while not End_Of_File (F) loop

28          Read (F, Value);

29          Ada.Text_IO.Put_Line (Float'Image (Value));

30       end loop;

31       Close (F);

32    end;

33 end Show_Dir_Float_In_Out_File;

Runtime output

1.50000E+00

2.40000E+00

7.70000E+00

Запустив этот пример, мы видим, что файл содержит значение 7.7 и не содержит значения 6.7, которое мы записали сначала. Мы перезаписали значение, установив индекс на предыдущую позицию перед выполнением следующей операции записи.

В этом примере мы использовали режим Inout_File. Используя этот режим, мы просто вернули индекс в начальное положение перед чтением из файла (Set_Index (F, 1)) вместо того, чтобы закрывать файл и повторно открывать его для чтения.

Потоковый ввод-вывод

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

Потоковый ввод-вывод имеет некоторые общие черты с предыдущими подходами. Мы по-прежнему используем процедуры Create, Open и Close. Однако вместо прямого доступа к файлу через объект File_Type вы используете тип Stream_Access. Для чтения и записи информации вы используете атрибуты 'Read или 'Write тех типов данных, которые вы читаете или пишете.

Давайте посмотрим на версию процедуры Show_Dir_Float_IO из предыдущего раздела. Процедура использует потоковый ввод-вывод вместо прямого ввода-вывода:

show_float_stream.adb

1 with Ada.Text_IO;

2 with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO;

3

4 procedure Show_Float_Stream is

5    F         : File_Type;

6    S         : Stream_Access;

7    File_Name : constant String := "float_file.bin";

8 begin

9    Create (F, Out_File, File_Name);

10    S := Stream (F);

11

12    Float'Write (S, 1.5);

13    Float'Write (S, 2.4);

14    Float'Write (S, 6.7);

15

16    Close (F);

17

18    declare

19       Value : Float;

20    begin

21       Open (F, In_File, File_Name);

22       S := Stream (F);

23

24       while not End_Of_File (F) loop

25          Float'Read (S, Value);

26          Ada.Text_IO.Put_Line (Float'Image (Value));

27       end loop;

28       Close (F);

29    end;

30 end Show_Float_Stream;

Runtime output

1.50000E+00

2.40000E+00

6.70000E+00

После вызова Create мы получаем соответствующее значение Stream_Access, вызывая функцию Stream (поток). Затем мы используем этот поток для записи информации в файл используя атрибут 'Write типа Float. После закрытия файла и повторного открытия его для чтения мы снова получаем значение Stream_Access и используем его для чтения информации из файла с помощью атрибута 'Read типа Float.

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

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

show_string_stream.adb

1 with Ada.Text_IO;

2 with Ada.Streams.Stream_IO; use Ada.Streams.Stream_IO;

3

4 procedure Show_String_Stream is

5    F         : File_Type;

6    S         : Stream_Access;

7    File_Name : constant String := "float_file.bin";

8

9    procedure Output (S  : Stream_Access;

10                      FV : Float;

11                      SV : String) is

12    begin

13       String'Output (S, SV);

14       Float'Output (S,  FV);

15    end Output;

16

17    procedure Input_Display (S : Stream_Access) is

18       SV : String := String'Input (S);

19       FV : Float  := Float'Input (S);

20    begin

21       Ada.Text_IO.Put_Line (Float'Image (FV)

22                             & " --- " & SV);

23    end Input_Display;

24

25 begin

26    Create (F, Out_File, File_Name);

27    S := Stream (F);

28

29    Output (S, 1.5, "Hi!!");

30    Output (S, 2.4, "Hello world!");

31    Output (S, 6.7, "Something longer here...");

32

33    Close (F);

34

35    Open (F, In_File, File_Name);

36    S := Stream (F);

37

38    while not End_Of_File (F) loop

39       Input_Display (S);

40    end loop;

41    Close (F);

42

43 end Show_String_Stream;

Build output

show_string_stream.adb:18:07: warning: "SV" is not modified, could be declared constant [-gnatwk]

show_string_stream.adb:19:07: warning: "FV" is not modified, could be declared constant [-gnatwk]

Runtime output

1.50000E+00 --- Hi!!

2.40000E+00 --- Hello world!

6.70000E+00 --- Something longer here...

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

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

Footnotes

Стандартная библиотека: Numerics

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

Элементарные функции

Пакет Ada.Numerics.Elementary_Functions обеспечивает общепринятые операции для типов с плавающей точкой, такие как квадратный корень, логарифм и тригонометрические функции (типа sin, cos). Например:

show_elem_math.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics; use Ada.Numerics;

3

4 with Ada.Numerics.Elementary_Functions;

5 use  Ada.Numerics.Elementary_Functions;

6

7 procedure Show_Elem_Math is

8    X : Float;

9 begin

10    X := 2.0;

11    Put_Line ("Square root of "

12              & Float'Image (X)

13              & " is "

14              & Float'Image (Sqrt (X)));

15

16    X := e;

17    Put_Line ("Natural log of "

18              & Float'Image (X)

19              & " is "

20              & Float'Image (Log (X)));

21

22    X := 10.0 ** 6.0;

23    Put_Line ("Log_10      of "

24              & Float'Image (X)

25              & " is "

26              & Float'Image (Log (X, 10.0)));

27

28    X := 2.0 ** 8.0;

29    Put_Line ("Log_2       of "

30              & Float'Image (X)

31              & " is "

32              & Float'Image (Log (X, 2.0)));

33

34    X := Pi;

35    Put_Line ("Cos         of "

36              & Float'Image (X)

37              & " is "

38              & Float'Image (Cos (X)));

39

40    X := -1.0;

41    Put_Line ("Arccos      of "

42              & Float'Image (X)

43              & " is "

44              & Float'Image (Arccos (X)));

45 end Show_Elem_Math;

Runtime output

Square root of  2.00000E+00 is  1.41421E+00

Natural log of  2.71828E+00 is  1.00000E+00

Log_10      of  1.00000E+06 is  6.00000E+00

Log_2       of  2.56000E+02 is  8.00000E+00

Cos         of  3.14159E+00 is -1.00000E+00

Arccos      of -1.00000E+00 is  3.14159E+00

Здесь мы используем стандартные константы e и Pi из пакета Ada.Numerics.

Пакет Ada.Numerics.Elementary_Functions предоставляет операции для типа Float. Аналогичные пакеты есть для типов Long_Float и Long_Long_Float. Например, пакет Ada.Numerics.Long_Elementary_Functions предлагает тот же набор операций для типа Long_Float. Кроме того, пакет Ada.Numerics.Generic_Elementary_Functions - это настраиваемая версия пакета, которую можно использовать для пользовательских типов с плавающей запятой. Фактически, пакет Elementary_Functions можно определить следующим образом:

package Elementary_Functions is new

Ada.Numerics.Generic_Elementary_Functions (Float);

Генерация случайных чисел

Пакет Ada.Numerics.Float_Random предоставляет простой генератор случайных чисел с диапазоном от 0,0 до 1,0. Чтобы использовать его, объявите генератор G, который вы затем передадите в Random. Например:

show_float_random_num.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics.Float_Random; use Ada.Numerics.Float_Random;

3

4 procedure Show_Float_Random_Num is

5    G : Generator;

6    X : Uniformly_Distributed;

7 begin

8    Reset (G);

9

10    Put_Line ("Some random numbers between "

11              & Float'Image (Uniformly_Distributed'First)

12              & " and "

13              & Float'Image (Uniformly_Distributed'Last)

14              & ":");

15    for I in 1 .. 15 loop

16       X := Random (G);

17       Put_Line (Float'Image (X));

18    end loop;

19 end Show_Float_Random_Num;

Runtime output

Some random numbers between  0.00000E+00 and  1.00000E+00:

9.29413E-01

6.83346E-01

2.30866E-01

9.41220E-01

7.81104E-01

5.04554E-01

2.73398E-01

9.05950E-01

6.31678E-01

7.84824E-01

4.11724E-01

9.77724E-01

5.53477E-01

8.14905E-01

2.13336E-01

Стандартная библиотека также включает генератор случайных чисел для дискретных чисел, который является частью пакета Ada.Numerics.Discrete_Random. Поскольку это настраиваемый пакет, необходимо создать его экземпляр для требуемого дискретного типа. Это позволяет вам задать диапазон генератора. В следующем примере создается приложение, отображающее случайные целые числа от 1 до 10:

show_discrete_random_num.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics.Discrete_Random;

3

4 procedure Show_Discrete_Random_Num is

5

6    subtype Random_Range is Integer range 1 .. 10;

7

8    package R is new

9      Ada.Numerics.Discrete_Random (Random_Range);

10    use R;

11

12    G : Generator;

13    X : Random_Range;

14 begin

15    Reset (G);

16

17    Put_Line ("Some random numbers between "

18              & Integer'Image (Random_Range'First)

19              & " and "

20              & Integer'Image (Random_Range'Last)

21              & ":");

22

23    for I in 1 .. 15 loop

24       X := Random (G);

25       Put_Line (Integer'Image (X));

26    end loop;

27 end Show_Discrete_Random_Num;

Runtime output

Some random numbers between  1 and  10:

1

1

9

4

3

9

4

4

6

6

8

1

2

2

7

Здесь пакет R создается с типом Random_Range с ограничением диапазона от 1 до 10. Это позволяет нам контролировать диапазон получаемых случайных чисел. Мы могли бы легко изменить приложение для получения случайных целых чисел от 0 до 20, изменив спецификацию подтипа Random_Range. Аналогично мы можем использовать типы с плавающей запятой и с фиксированной запятой.

Комплексные числа

Пакет Ada.Numerics.Complex_Types обеспечивает поддержку комплексных чисел, а пакет Ada.Numerics.Complex_Elementary_Functions обеспечивает поддержку общепринятые операций с типами комплексных чисел, аналогично пакету Ada.Numerics.Elementary_Functions. Наконец, вы можете использовать пакет Ada.Text_IO.Complex_IO для выполнения операций ввода-вывода над комплексными числами. В следующем примере мы объявляем переменные типа Complex и инициализируем их с помощью агрегата:

show_elem_math.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2 with Ada.Numerics; use Ada.Numerics;

3

4 with Ada.Numerics.Complex_Types;

5 use  Ada.Numerics.Complex_Types;

6

7 with Ada.Numerics.Complex_Elementary_Functions;

8 use  Ada.Numerics.Complex_Elementary_Functions;

9

10 with Ada.Text_IO.Complex_IO;

11

12 procedure Show_Elem_Math is

13

14    package C_IO is new

15      Ada.Text_IO.Complex_IO (Complex_Types);

16    use C_IO;

17

18    X, Y  : Complex;

19    R, Th : Float;

20 begin

21    X := (2.0, -1.0);

22    Y := (3.0,  4.0);

23

24    Put (X);

25    Put (" * ");

26    Put (Y);

27    Put (" is ");

28    Put (X * Y);

29    New_Line;

30    New_Line;

31

32    R  := 3.0;

33    Th := Pi / 2.0;

34    X  := Compose_From_Polar (R, Th);

35    --  Alternatively:

36    --  X := R * Exp ((0.0, Th));

37    --  X := R * e ** Complex'(0.0, Th);

38

39    Put ("Polar form:    "

40         & Float'Image (R)  & " * e**(i * "

41         & Float'Image (Th) & ")");

42    New_Line;

43

44    Put ("Modulus     of ");

45    Put (X);

46    Put (" is ");

47    Put (Float'Image (abs (X)));

48    New_Line;

49

50    Put ("Argument    of ");

51    Put (X);

52    Put (" is ");

53    Put (Float'Image (Argument (X)));

54    New_Line;

55    New_Line;

56

57    Put ("Sqrt        of ");

58    Put (X);

59    Put (" is ");

60    Put (Sqrt (X));

61    New_Line;

62 end Show_Elem_Math;

Runtime output

( 2.00000E+00,-1.00000E+00) * ( 3.00000E+00, 4.00000E+00) is ( 1.00000E+01, 5.00000E+00)

Polar form:     3.00000E+00 * e**(i *  1.57080E+00)

Modulus     of (-1.31134E-07, 3.00000E+00) is  3.00000E+00

Argument    of (-1.31134E-07, 3.00000E+00) is  1.57080E+00

Sqrt        of (-1.31134E-07, 3.00000E+00) is ( 1.22474E+00, 1.22474E+00)

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

Пакеты Ada.Numerics.Complex_Types и Ada.Numerics.Complex_Elementary_Functions предоставляют операции для типа Float. Подобные пакеты доступны для типов Long_Float и Long_Long_Float. Кроме того, с помощью настраиваемых пакетов Ada.Numerics.Generic_Complex_Types и Ada.Numerics.Generic_Complex_Elementary_Functions можно создавать комплексные типы и их функции из пользовательских или предопределенных типов с плавающей запятой. Например:

with Ada.Numerics.Generic_Complex_Types;

with Ada.Numerics.Generic_Complex_Elementary_Functions;

with Ada.Text_IO.Complex_IO;

procedure Show_Elem_Math is

package Complex_Types is new

Ada.Numerics.Generic_Complex_Types (Float);

use Complex_Types;

package Elementary_Functions is new

Ada.Numerics.Generic_Complex_Elementary_Functions

(Complex_Types);

use Elementary_Functions;

package C_IO is new Ada.Text_IO.Complex_IO

(Complex_Types);

use C_IO;

X, Y  : Complex;

R, Th : Float;

Работа с векторами и матрицами

Пакет Ada.Numerics.Real_Arrays обеспечивает поддержку векторов и матриц. Он включает в себя типичные операции с матрицами, такие как обращение, нахождение определителя и собственного значения, а также более простые операции, такие как сложение и умножение матриц. Вы можете объявлять векторы и матрицы, используя типы Real_Vector и Real_Matrix соответственно.

В следующем примере используются некоторые операции из пакета Ada.Numerics.Real_Arrays:

show_matrix.adb

1 with Ada.Text_IO;  use Ada.Text_IO;

2

3 with Ada.Numerics.Real_Arrays;

4 use  Ada.Numerics.Real_Arrays;

5

6 procedure Show_Matrix is

7

8    procedure Put_Vector (V : Real_Vector) is

9    begin

10       Put ("    (");

11       for I in V'Range loop

12          Put (Float'Image (V (I)) & " ");

13       end loop;

14       Put_Line (")");

15    end Put_Vector;

16

17    procedure Put_Matrix (M : Real_Matrix) is

18    begin

19       for I in M'Range (1) loop

20          Put ("    (");

21          for J in M'Range (2) loop

22             Put (Float'Image (M (I, J)) & " ");

23          end loop;

24          Put_Line (")");

25       end loop;

26    end Put_Matrix;

27

28    V1       : Real_Vector := (1.0, 3.0);

29    V2       : Real_Vector := (75.0, 11.0);

30

31    M1       : Real_Matrix :=

32                 ((1.0, 5.0, 1.0),

33                  (2.0, 2.0, 1.0));

34    M2       : Real_Matrix :=

35                 ((31.0, 11.0, 10.0),

36                  (34.0, 16.0, 11.0),

37                  (32.0, 12.0, 10.0),

38                  (31.0, 13.0, 10.0));

39    M3       : Real_Matrix := ((1.0, 2.0),

40                               (2.0, 3.0));

41 begin

42    Put_Line ("V1");

43    Put_Vector (V1);

44    Put_Line ("V2");

45    Put_Vector (V2);

46    Put_Line ("V1 * V2 =");

47    Put_Line ("    "

48              & Float'Image (V1 * V2));

49    Put_Line ("V1 * V2 =");

50    Put_Matrix (V1 * V2);

51    New_Line;

52

53    Put_Line ("M1");

54    Put_Matrix (M1);

55    Put_Line ("M2");

56    Put_Matrix (M2);

57    Put_Line ("M2 * Transpose(M1) =");

58    Put_Matrix (M2 * Transpose (M1));

59    New_Line;

60

61    Put_Line ("M3");

62    Put_Matrix (M3);

63    Put_Line ("Inverse (M3) =");

64    Put_Matrix (Inverse (M3));

65    Put_Line ("abs Inverse (M3) =");

66    Put_Matrix (abs Inverse (M3));

67    Put_Line ("Determinant (M3) =");

68    Put_Line ("    "

69              & Float'Image (Determinant (M3)));

70    Put_Line ("Solve (M3, V1) =");

71    Put_Vector (Solve (M3, V1));

72    Put_Line ("Eigenvalues (M3) =");

73    Put_Vector (Eigenvalues (M3));

74    New_Line;

75 end Show_Matrix;

Build output

show_matrix.adb:28:04: warning: "V1" is not modified, could be declared constant [-gnatwk]

show_matrix.adb:29:04: warning: "V2" is not modified, could be declared constant [-gnatwk]

show_matrix.adb:31:04: warning: "M1" is not modified, could be declared constant [-gnatwk]

show_matrix.adb:34:04: warning: "M2" is not modified, could be declared constant [-gnatwk]

show_matrix.adb:39:04: warning: "M3" is not modified, could be declared constant [-gnatwk]

Runtime output

V1

( 1.00000E+00  3.00000E+00 )

V2

( 7.50000E+01  1.10000E+01 )

V1 * V2 =

1.08000E+02

V1 * V2 =

( 7.50000E+01  1.10000E+01 )

( 2.25000E+02  3.30000E+01 )

M1

( 1.00000E+00  5.00000E+00  1.00000E+00 )

( 2.00000E+00  2.00000E+00  1.00000E+00 )

M2

( 3.10000E+01  1.10000E+01  1.00000E+01 )

( 3.40000E+01  1.60000E+01  1.10000E+01 )

( 3.20000E+01  1.20000E+01  1.00000E+01 )

( 3.10000E+01  1.30000E+01  1.00000E+01 )

M2 * Transpose(M1) =

( 9.60000E+01  9.40000E+01 )

( 1.25000E+02  1.11000E+02 )

( 1.02000E+02  9.80000E+01 )

( 1.06000E+02  9.80000E+01 )

M3

( 1.00000E+00  2.00000E+00 )

( 2.00000E+00  3.00000E+00 )

Inverse (M3) =

(-3.00000E+00  2.00000E+00 )

( 2.00000E+00 -1.00000E+00 )

abs Inverse (M3) =

( 3.00000E+00  2.00000E+00 )

( 2.00000E+00  1.00000E+00 )

Determinant (M3) =

-1.00000E+00

Solve (M3, V1) =

( 3.00000E+00 -1.00000E+00 )

Eigenvalues (M3) =

( 4.23607E+00 -2.36068E-01 )

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

M1       : Real_Matrix (1 .. 2, 1 .. 3) :=

((1.0, 5.0, 1.0),

(2.0, 2.0, 1.0));

Пакет Ada.Numerics.Real_Arrays предоставляет операции для типа Float. Аналогичные пакеты доступны для типов Long_Float и Long_Long_Float. Кроме того, настраиваемый пакет Ada.Numerics.Generic_Real_Arrays можно использовать для пользовательских типов с плавающей запятой. Например, пакет Real_Arrays может быть определен следующим образом:

package Real_Arrays is new

Ada.Numerics.Generic_Real_Arrays (Float);

Footnotes

Приложения

Приложение A: Формальные типы настройки

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

Формальный тип

Фактический тип

Неполный тип

Формат: type T;

Любой тип

Дискретный тип

Формат: type T is (<>);

Любой целочисленный тип, модульный тип или перечислимый тип

Тип диапазона

Формат: type T is range <>;

Любой целочисленный тип со знаком

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

Формат: type T is mod <>;

Любой модульный тип

Тип с плавающей запятой

Формат: type T is digits <>;

Любой тип с плавающей запятой

Двоичный тип с фиксированной запятой

Формат: type T is delta <>;

Любой двоичный тип с фиксированной запятой

Десятичный тип с фиксированной запятой

Формат: type T is delta <> digits <>;

Любой десятичный тип с фиксированной запятой

Определенный нелимитируемый личный тип

Формат: type T is private;

Любой нелимитируемый, определенный тип

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

Формат: type T (D : DT) is private;

Любой нелимитируемый тип с дискриминантом

Ссылочный тип

Формат: type A is access T;

Любой ссылочный тип для типа T

Определенный производный тип

Формат: type T is new B;

Любой конкретный тип, производный от базового типа B

Лимитируемый частный тип

Формат: type T is limited private;

Любой определенный тип, лимитируемый или нет

Неполное описание тегового типа

Формат: type T is tagged;

Любой конкретный, определенный, теговый тип

Определенный теговый личный тип

Формат: type T is tagged private;

Любой конкретный, определенный, теговый тип

Определенный теговый лимитируемый личный тип

Формат: type T is tagged limited private;

Любой конкретный определенный теговый тип лимитируемый или нет.

Определенный абстрактный теговый личный тип

Формат: type T is abstract tagged private;

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

Определенный абстрактный теговый лимитируемый личный тип

Формат: type T is abstract tagged limited private;

Любой определенный теговый тип, лимитир-мый или нет, абстрактный или конкретный

Определенный производный теговый тип

Формат: type T is new B with private;

Любой конкретный теговый тип, производный от базового типа B

Определенный абстрактный производный теговый тип

Формат: type T is abstract new B with private;

Любой теговый тип, производный от базового типа B абстрактный или конкретный

Тип массива

Формат: type A is array (R) of T;

Любой тип массива с диапазоном R, содержащий элементы типа T

Интерфейсный тип

Формат: type T is interface;

Любой интерфейсный тип

Лимитируемый интерфейсный тип

Формат: type T is limited interface;

Любой лимитируемый интерфейсный тип

Задачный интерфейсный тип

Формат: type T is task interface;

Любой задачный интерфейсный тип

Синхронизированный интерфейсный тип

Формат: type T is synchronized interface;

Любой синхронизированный интерфейсный тип

Защищенный интерфейсный тип

Формат: type T is protected interface;

Любой защищенный интерфейсный тип

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

Формат: type T is new B and I with private;

Любой тип, производный от базового типа B и интерфейса I

Производный тип с несколькими интерфейсами

Формат: type T is new B and I1 and I2 with private;

Любой тип, производный от базового типа B и интерфейсов I1 и I2

Абстрактный производный интерфейсный тип

Формат: type T is abstract new B and I with private;

Любой тип, производный от абстрактного базового типа B и интерфейса I

Лимитируемый производный интерфейсный тип

Формат: type T is limited new B and I with private;

Любой тип, производный от лимитируемого базового типа B и лимитируемого интерфейса

Абстрактный лимитируемый производный интерфейсный тип

Формат: type T is abstract limited new B and I with private;

Любой тип, производный от абстрактного лимитиру емого базового типа B и лимитируемого интерфейса

Синхронизированный интерфейсный тип

Формат: type T is synchronized new SI with private;

Любой тип, производный от синхронизированного интерфейсного типы SI

Абстрактный синхронизированный интерфейсный тип

Формат: type T is abstract synchronized new SI with private;

Любой тип, производный от синхронизированного интерфейса SI

Неопределенные версии типов

Многие из приведенных выше примеров могут быть использованы для формальных неопределенных типов:

Формальный тип

Фактический тип

Неопределенный неполный тип

Формат: type T (<>);

Любой тип

Неопределенный нелимитируемый личный тип

Формат: type T (<>) is private;

Любой нелимитируемый тип неопределенный или определенный

Неопределенный лимитируемый личный тип

Формат: type T (<>) is limited private;

Любой тип, лимитируемый или нет, неопределенный или определенный

Неполный неопределенный личный теговый тип

Формат: type T (<>) is tagged;

Любой конкретный теговый тип, неопределенный или определенный

Неопределенный теговый личный тип

Формат: type T (<>) is tagged private;

Любой конкретный, нелимитируемый теговый тип, неопределенный или определенный

Неопределенный теговый лимитируемый личный типми

Формат: type T (<>) is tagged limited private;

Любой конкретный теговый тип, лимитируемый или нет, неопределенный или определенный

Неопределенный абстрактный теговый личный тип

Формат: type T (<>) is abstract tagged private;

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

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

Формат: type T (<>) is abstract tagged limited private;

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

Неопределенный производный теговый тип

Формат: type T (<>) is new B with private;

Любой теговый тип, производный от базового типа B, неопределенный или определенный

Неопределенный абстрактный производный теговый тип

Формат: type T (<>) is abstract new B with private;

Любой тегоый тип, производный от базового типа B, неопределенный или определенный абстрактный или конкретный

Те же примеры могут также содержать дискриминанты. В этом случае, (<>) заменяется списком дискриминантов, например: (D: DT).

Приложение B: Контейнеры

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

Категория

Контейнер

Std

Bounded

Unbounded

Indefinite

Вектор

Vectors

Y

Y

Y

Список

Doubly Linked Lists

Y

Y

Y

Отображение

Hashed Maps

Y

Y

Y

Отображение

Ordered Maps

Y

Y

Y

Множество

Hashed Sets

Y

Y

Y

Множество

Ordered Sets

Y

Y

Y

Дерево

Multiway Trees

Y

Y

Y

Универсальн

Holders

Y

Очередь

Synchronized Queue Interfaces

Y

Очередь

Synchronized Queues

Y

Y

Очередь

Priority Queues

Y

Y

Note

Чтобы получить имя пакета контейнера, замените пробел на _ в его названии. (Например, пакет для Hashed Maps называется Hashed_Maps.)

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

Версия

Префикс именования

Std

Ограниченный

Bounded_

Неограниченный

Unbounded_

Неопределенный

Indefinite_

Footnotes

Index