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

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

Итак, пакеты. Пакеты в языке Ада - это фактически аналоги библиотек в языках С/С++ (это те файлы, которые в С/С++ подключаются с помощью директивы #include: #include <stdio.h>, #include "my_lib.h" и т.д.). Грубо говоря, пакет представляет собой 2 файла с одинаковыми названиями, но разными расширениями, которые содержат описание элементов (переменных, типов, подпрограмм) и их реализацию. Пакеты позволяют объединить какие-то логически связанные элементы. Например, если Вы создали какой-то свой тип и подпрограммы для работы с этим типом, то вы можете реализовать этот тип и относящиеся к нему подпрограммы с помощью пакета, а потом просто использовать with... use... для подключения этого пакета в любой Вашей программе. Таким образом, написав однажды какой-то код, Вы всегда можете его повторно использовать в разных программах.

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

Сразу оговорюсь: если Вы программируете в Linux (или Вам нужна кроссплатформенность), то при создании файла-спецификации и файла-тела пакета, в названиях этих файлов нужно использовать только строчные буквы. В тексте программы к этим пакетам можно обращаться с использованием как прописных, так и строчных букв, а вот в названии нужно использовать строчные буквы. Это связано вот с чем: Ада, как уже отмечалось, не различает строчные и прописные символы, поэтому при запросе пакета компилятор преобразует все символы к нижнему регистру. В Windows это не имеет значения, а вот в Linux строчные и прописные символы рассматриваются как разные (т.е. в Linux в одной директории могут существовать файлы text.txt и Text.txt и это будут разные файлы. В Windows такой фокус не пройдёт).

Рассмотрим маленькую программку. Я буду использовать полную запись (без использования use), чтобы было понятно, откуда что берётся. Кстати, советую при создании своих пакетов всегда использовать полную нотацию (запись), во избежание т.н. коллизии (когда в разных пакетах присутствуют одинаковые имена функций, переменных или типов). Итак, программа: в пакете my_package объявляется тип Short и одна единственная функция Add(), осуществляющая сложение переменных этого типа. В основной части программы объявляются две переменные типа Short и складываются с помощью функции Add:

Файл my_pack.ads:

package My_Pack is --имя пакета совпадает с именем файла (кроме расширения)
    type Short is new Integer range 0..100; --Объявляется тип Short
 
    function Add(a, b : in Short) return Short; --Объявляется функция Add
end my_pack;

 Файл my_pack.adb:

package body My_Pack is
    --Реализация функции Add
    function Add(a : in Short; b : in Short) return Short is
        c : Short;
    begin
        c := a + b;
        return c;
    end Add;
begin
    NULL; --В теле никаких действий не будет, поэтому NULL.
          --Однако, если нужно выполнить какие-то действия, например заполнить
          --массив какими-то значениями, то эти действия можно прописать в этом блоке.
          --При этом нужно знать, что действия, прописанные здесь, будут выполнены
          --до начала выполнения основной части программы.
end My_Pack;

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

with Ada.Integer_Text_IO;
with My_Pack;
 
procedure main is
    x, y : My_Pack.Short; --Тип Short определён в пакете My_Pack
    res : My_Pack.Short;
begin
    --Так как для типа Short не определена подпрограмма ввода, то будем использовать
    --Get() для целых чисел (с приведением типа)
    Ada.Integer_Text_IO.Get(Integer(x));
    Ada.Integer_Text_IO.Get(Integer(y));
 
    res := My_Pack.Add(x, y); --Вызов функции из созданного нами пакета My_Pack
    Ada.Integer_Text_IO.Put(Integer(res)); 
end main;

Для упрощения понимания материала я не делаю никаких проверок ввода и операции суммирования, но при тестировании этой программы нужно помнить, что вводимые переменные должны быть не меньше 0 и не больше 100. К их сумме предъявляется такое же требование. Все три файла нужно положить в одну директорию. Компилировать нужно файл main.adb. Пакет my_pack будет включен компилятором автоматически.

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

Приватные и ограниченные приватные типы.

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

package My_Pack is
    --Тип объявлен как приватный. Он будет реализован в приватной части пакета
    type Short is private;
    function Add(a, b : in Short) return Short;
private --Приватная часть пакета
    type Short is new Integer range 0..100;
end My_Pack;

Таким образом, мы ограничили операции над типом Short только функцией Add(), а также операциями присваивания (:=) и равенства (=), которые определяются по умолчанию. Для того, чтобы исключить из списка операций присваивание (:=) и равенство (=), тип Short нужно объявить не как private, а как limited private (ограниченный приватный):

type Short is limited private;

Всё, что объявлено внутри блока private...end будет напрямую доступно только внутри пакета, это приватная часть пакета. Для доступа к приватной части внутри других файлов/пакетов, нужно предусмотреть этом пакете неприватные подпрограммы, вроде Set() - установить и Get() - прочитать.

В приведённом выше примере мы использовали функцию Add() для сложения двух чисел. Эту функцию можно переписать немножко иначе. Дело в том, что в Аде возможно переопределение или совмещение операторов (overloading, в языке С++ это называется перегрузкой операторов). То есть, вместо функции Add() можно переопределить оператор "+":

function "+" (a, b : Short) return Short is
begin
    return Short(Integer(a) + Integer(b));
end "+";

При этом эта функция может быть вызвана двумя способами:

res := a + b;
res := "+"(a, b)

Сложность здесь может вызвать только строка return Short(Integer(a) + Integer(b)); Возникает вопрос, для чего нужно преобразовывать a и b к типу Integer, а затем обратно к типу Short?

Для примера уберём преобразование типа Short к типу Integer и получим: return a + b; Тогда получается, что в том месте, где у нас используется "+" опять будет вызвана функция "+" (a, b : Short) (т.к. a и b имеют тип Short, а для Short мы и перегружаем "+"), т.е. внутри функции перегрузки будет ещё раз вызвана функция перегрузки, и так до бесконечности. Приём, когда подпрограмма вызывает сама себя, называется рекурсией. В данном случае мы получим рекурсивную функцию, но вот условия выхода из неё здесь не указано. Компилятор честно ругнётся, что в этом месте программа сбойнёт, но создать итоговый файл позволит. Программа, конечно запустится, но работать не будет, т.к. бесконечная рекурсия сожрёт все ресурсы, отведённые под нашу программу. Вернее, программа отработает и, потратив все ресурсы, упадёт. Добро пожаловать в мир рекурсии, дамы и господа!

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

В данном примере преобразование переменных a и b к типу Integer приводит к тому, что для их сложения будет использован не переопределённый нами оператор "+", а основной оператор сложения, используемый для операций с целыми числами. Т.е. рекурсии не будет.

Приведённый пример - это довольно нечастый случай перегрузки/переопределения. Так как тип (Short) является производным от типа Integer, то здесь проще обойтись без переопределения, а просто использовать приведение типов.

И последний момент: если в пакете My_Pack нужно объявить константу, то делается это так:

package My_Pack is
    type Short is private;
    N : constant Short; --Объявлена константа, но значение ей не присвоено
    ...
private --Приватная часть пакета
    type Short is new Integer range 0..100;
    N : constant Short := 10; --Константе присвоено значение
    ...
end My_Pack;

Для людей, знакомых с ООП, эти возможности Ады, скорее всего, напомнят объявление классов и методов классов. Однако язык Ада появился задолго до рождения ООП, и приватные типы и переопределение/перегрузка операторов существовали в нём всегда. Т.е. в Аде приватные типы и перегрузка операторов не имеют никакого отношения к ООП, это обычное структурное программирование 😉 .

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

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