Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4086 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 15:

OO-программирование и язык Ada

Реализация стеков

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

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

function item (s: STACK) return X;

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

Простой интерфейс

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

package REAL_STACKS is
    type STACK_CONTENTS is array (POSITIVE range <>) of FLOAT;
    type STACK (capacity: POSITIVE) is
        record
            implementation: STACK_CONTENTS (1..capacity);
            count: NATURAL := 0;
        end record;
    procedure put (x: in FLOAT; s: in out STACK);
    procedure remove (s: in out STACK);
    function item (s: STACK) return FLOAT;
    function empty (s: STACK) return BOOLEAN;
    Overflow, Underflow: EXCEPTION;
end REAL_STACKS;

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

Этот пример наводит на несколько общих замечаний:

  • Удивительно видеть все детали представления стека в объявлениях типов STACK и STACK_CONTENTS, появившихся в том, что должно быть чистым интерфейсом. Кратко рассмотрим причину этой проблемы и способ ее устранения.
  • В отличие от класса, пакет не определяет тип. Тип STACK следует определить отдельно. Одним из следствий этого отделения для программиста, создающего пакет вокруг реализации абстрактного типа данных, является необходимость изобретения двух различных имен - одно для пакета, другое - для типа. Другое следствие состоит в том, что подпрограммы имеют еще один аргумент по сравнению со своими ОО-аналогами: здесь все они имеют первым аргументом стек s, в то время как для класса он задается неявно (см. предыдущие лекции).
  • Объявление может определять не только тип сущности, но и ее исходное значение. Здесь объявление count в типе STACK предписывает исходное значение 0. Оно устраняет необходимость явной операции инициализации, задаваемой процедурой создания (конструктором) класса. Однако этот способ не работает, если требуется более сложная инициализация.
  • Для понимания объявления типа следует привести некоторые детали языка Ada: POSITIVE и NATURAL обозначают подтипы INTEGER, включающие, соответственно, положительные и неотрицательные целые, спецификация типа вида array ( TYPE range <> ), где <> известно как Box-символ, описывает шаблон для типов массивов. Для получения действительного типа из такого шаблона нужно выбрать конечный отрезок TYPE. Здесь это делается при определении типа STACK, использующем интервал [1..capacity] типа POSITIVE. STACK является примером параметризованного типа. Любое объявление сущности типа STACK должно задавать фактическое значение емкости стека capacity, как в:
    s: STACK (1000)
  • В языке Ada каждый аргумент подпрограммы характеризуется статусом in, out или in out, определяющим права подпрограммы на использование фактических аргументов (только для чтения, только для записи, для обновления). В отсутствии явного ключевого слова состояние по умолчанию - in.
  • Наконец, интерфейс определяет два имени исключений Overflow и Underflow. Исключение - это ситуация, когда из-за ошибок прерывается нормальный порядок вычислений. Интерфейс пакета должен перечислить любые исключения, которые могут возбуждаться в процессе работы подпрограмм пакета и передаваться для обработки клиентам. Подробно механизм исключений языка Ada описывается ниже.

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

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

s: REAL_STACKS.STACK (1000);
REAL_STACKS.put (3.5, s); ...;
if REAL_STACKS.empty (s) then ...;

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

Синтаксически каждое использование сущности (здесь "сущности" включают имена программ и типов) повторяет имя пакета REAL_STACKS. Это утомительно - необходима неявная форма квалификации. Если включена директива:

use REAL_STACKS;

в начале клиентского пакета, то выражения записываются проще:

s: STACK (1000);
put (3.5, s); ...;
if empty (s) then ...;

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

В литературе по языку Ada иногда встречается совет программистам вообще не использовать директиву use, поскольку она мешает ясности: неквалифицированная ссылка, например вызов empty (s), сразу не говорит о поставщике empty (в нашем примере REAL_STACKS ). Его аналог в ОО-подходе, s.empty, однозначно определяет поставщика через цель s.

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

Реализация

Тело пакета REAL_STACKS может объявляться следующим образом. Полностью показана только одна подпрограмма.

package body REAL_STACKS is
    procedure put (x: in FLOAT; s: in out REAL_STACK) is
        begin
            if s.count = s.capacity then
                raise Overflow
            end if;
            s.count := s.count + 1;
            s.implementation (count) := x;
        end put;
    procedure remove (s: in out STACK) is
            ... Реализация remove ...
        end remove;
    function item (s: STACK) return X is
            ... Реализация item ...
        end item;
    function empty (s: STACK) return BOOLEAN is
            ... Реализация empty ...
        end empty;
end REAL_STACKS;

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

Универсальность

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

generic
    type G is private;
package STACKS is
    ... Все, как и ранее, заменяя все вхождения FLOAT на G ...
end STACKS;

Предложение generic синтаксически более тяжелое, чем наша ОО-нотация для универсальных классов ( class C [G]...), но зато в нем больше возможностей. В частности, параметры, объявляемые в generic, могут представлять не только типы, но и подпрограммы. В приложении B эти возможности обсуждаются при сравнении универсальности и наследования.

В теле пакета generic не повторяется, там достаточно конкретный тип FLOAT заменить родовым G.

Спецификация is private заставляет остальную часть пакета рассматривать G как закрытый тип. Это означает, что сущности этого типа могут использоваться только в операциях, применимых ко всем типам языка Ada: в качестве исходного или целевого объекта при присваивании, как операнд в проверке равенства, как фактический аргумент в подпрограмме, и в некоторых других специальных операциях. Это близко к соглашению для неограниченных формальных параметров универсальных классов нашей нотации. В языке Ada доступны и другие возможности. Можно ограничить операции, объявляя параметр как limited private, что запрещает все использования кроме фактических аргументов подпрограмм.

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

package REAL_STACKS_1 is new STACKS (FLOAT);

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