Read this in English
Welcome.
About me
Oracle
Ada
Guest book.
Counter
Глава II. Особенности Ады 95

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

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

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

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

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

Как следствие, Ада 95 получила возможности объектно-ориентированных языков, не подвергшись перегрузке как SmallTalk и сохранив безопасность, в отличие от языка C++, получившего слабую безопасность по наследству от С. Ада 95, оставаясь строго типизированным языком, предоставляет отличные возможности всех аспектов ООП.

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

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

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

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

II.1 Программирование расширением

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

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

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

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

        type Object is tagged
            record
                X_Coord: Float;
                Y_Coord: Float;
            end record;
    

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

        type Circle is new Object with
            record
                Radius: Float;
            end record;
        
    

и тип Circle будет иметь три компонента X_Coord, Y_Coord и Radius. Он наследует координаты от типа Object, а компонент Radius добавлен явно.

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

        type Point is new Object with null record;
    

В этом последнем случае мы унаследовали тип Point от типа Object, но не добавили не одного нового компонента. Но, поскольку мы имеем дело с теговыми типами, мы должны явно добавить фразу with null record, чтобы указать, что мы не хотим никаких новых компонентов. Преимущество в том, что по описанию всегда ясно видно теговый тип или нет. Заметьте, что tagged - это новое зарезервированное слово; Ада 95 имеет очень мало (шесть) новых зарезервированных слов.

Частный тип может также быть помечен как теговый:

        type Shape is tagged private;
    

и полное описание типа должно быть теговым типом:

        type Shape is tagged
            record ...
    

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

        package Hidden_Shape is
            type Shape is new Object with private; -- client view
            ...
        private
            type Shape is new Object with  -- server view
            record
                -- the private components
            end record;
        end Hidden_Shape;
    

В этом случае нет необходимости, чтобы полное описание тип Shape было прямо производным от типа Object. Может существовать цепочка промежуточных производных типов (можно наследовать от типа Circle), имеет лишь значение, чтобы Shape, в конце концов, был производным от типа Object.

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

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

        function Distance(O: in Object) return Float is
        begin
            return Sqrt(O.X_Coord**2 + O.Y_Coord**2);
        end Distance;
    

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

        function Area(O: in Object) return Float is
        begin
            return 0.0;
        end Area;
    

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

        function Area(C: in Circle) return Float is
        begin
            return Pi*C.Radius**2;
        end Area;
    

которая заменяет наследуемую операцию.

Возможно "конвертировать" значение типа Circle в тип Object и наоборот. Преобразовать от окружности к объекту просто, достаточно написать:

        O: Object := (1.0, 0.5);
        C: Circle := (0.0, 0.0, 12.2);
        ...
        O := Object(C);
    

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

        C := (O with 46.8);
    

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

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

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

Сначала рассмотрим, как это могло бы быть реализовано в Аде 83:

        with Calendar;
        package Alert_System is

            type Priority is (Low, Medium, High);
            type Device is (Teletype, Console, Big_Screen);

            type Alert(P: Priority) is
                record
                    Time_Of_Arrival: Calendar.Time;
                    Message: Text;
                    case P is
                        when Low => null;
                        when Medium | High =>
                            Action_Officer: Person;
                            case P is
                                when Low | Medium => null;
                                when High =>
                                    Ring_Alarm_At: Calendar.Time;
                            end case;
                    end case;
                end record;

            procedure Display(A: in Alert; On: in Device);
            procedure Handle(A: in out Alert);

            procedure Log(A: in Alert);
            procedure Set_Alarm(A: in Alert);

        end Alert_System;
    

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

        procedure Handle(A: in out Alert) is
        begin
            A.Time_Of_Arrival := Calendar.Clock;
            Log(A);
            Display(A, Teletype);
            case A.P is
                when Low => null;  -- nothing special
                when Medium | High =>
                    A.Action_Officer := Assign_Volunteer;
                    Display(A, Console);
                    case A.P is
                        when Low | Medium => null;
                        when High =>
                            Display(A, Big_Screen);
                            Set_Alarm(A);
                    end case;
            end case;
        end Handle;
    

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

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

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

        with Calendar;
        package New_Alert_System is

            type Device is (Teletype, Console, Big_Screen);

            type Alert is tagged
                record
                    Time_Of_Arrival: Calendar.Time;
                    Message: Text;
                end record;

            procedure Display(A: in Alert; On: in Device);
            procedure Handle(A: in out Alert);
            procedure Log(A: in Alert);


            type Low_Alert is new Alert with null record;

            type Medium_Alert is new Alert with
                record
                    Action_Officer: Person;
                end record;

            -- now override inherited operation
            procedure Handle(MA: in out Medium_Alert);

            type High_Alert is new Medium_Alert with
                record
                    Ring_Alarm_At: Calendar.Time;
                end record;

            procedure Handle(HA: in out High_Alert);
            procedure Set_Alarm(HA: in High_Alert);

        end New_Alert_System;
    

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

Тип Low_Alert просто копия типа Alert (заметьте with null record;) и без него можно обойтись, хотя он поддерживает эквивалентность с версией на Аде 83; тип Low_Alert наследует процедуру Handle от типа Alert. Тип Medium_Alert расширяет тип Alert и предоставляет свою процедуру Handle, переопределяя, таким образом, наследуемую версию. Тип High_Alert далее расширяет Medium_Alert и так же предоставляет собственную процедуру Handle. Таким образом, вместо единой процедуры Handle, содержащей сложный оператор case, решение на Аде 95 распределяет логику обработки сигналов между специфичными типами без всякой избыточности.

Заметьте, что все типы Low_Alert, Medium_Alert и High_Alert наследуют также процедуры Display и Log, но без всяких изменений. Наконец, тип High_Alert добавляет процедуру Set_Alarm, которая не используется сигналами меньшего приоритета, и, таким образом, кажется нецелесообразным определять ее для них.

Тело пакета следующее:

        package body New_Alert_System is

            procedure Handle(A: in out Alert) is
            begin
                A.Time_Of_Arrival := Calendar.Clock;
                Log(A);
                Display(A, Teletype);
            end Handle;

            procedure Handle(MA: in out Medium_Alert) is
            begin
                Handle(Alert(MA)); -- handle as plain alert
                MA.Action_Officer := Assign_Volunteer;
                Display(MA, Console);
            end Handle;

            procedure Handle(HA: in out High_Alert) is
            begin
                Handle(Medium_Alert(HA)); -- conversion
                Display(HA, Big_Screen);
                Set_Alarm(HA);
            end Handle;

            procedure Display(A: in Alert; On: in Device)
                is separate;
            procedure Log(A: in Alert) is separate;

            procedure Set_Alarm(HA: in High_Alert) is separate;

        end New_Alert_System;
    

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

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

        with New_Alert_System;
        package Emergency_Alert_System is

            type Emergency_Alert is
                new New_Alert_System.Alert with private;

            procedure Handle(EA: in out Emergency_Alert);

            procedure Display(EA: in Emergency_Alert;
                                    On: in New_Alert_System.Device);

            procedure Log(EA: in Emergency_Alert);

            private
                ...
        end Emergency_Alert_System;
    

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


II.2 Программирование с использованием классовых типов

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

Для каждого тегового типа T существует ассоциированный тип T'Class. Этот тип состоит из объединения всех типов из дерева типов, порожденных от типа T. Значения типа T'Class, таким образом, это значения типа T и значения всех его порожденных типов. Более того, значение любого типа, порожденного от T могут быть неявно преобразованы к типу T'Class.

В случае с типом Alert, дерево типов показано на рисунке II-1.

                                        Alert
                                    |
                                    |
                     +--------------+----------------+
                     |              |                |
                  Low_Alert         |         Emergency_Alert
                                    |
                                    |
                                Medium_Alert
                                    |
                                    |
                                 High_Alert
    

        Рисунок II-1: Дерево типов
    

Значение любого из типов сигналов может быть неявно преобразовано к Alert'Class. Обратите внимание, что Medium_Alert'Class это не то же самое, что Alert'Class; последний состоит только из Medium_Alert и High_Alert. Каждое значение классового типа имеет тег, который отличает конкретный его тип от других типов дерева типов во время исполнения. Из-за существования этого тега данные типы и получили название теговые.

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

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

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

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

        procedure Process_Alerts(AC: in out Alert'Class) is
            ...
        begin
            ...
            Handle(AC); -- dispatch according to tag
            ...
        end Process_Alerts;
    

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

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

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

        type Alert_Ptr is access Alert'Class;
    

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

        procedure Process_Alerts is
            Next_Alert: Alert_Ptr;
        begin
            ...
            Next_Alert := -- get next alert
            ...
            Handle(Next_Alert.all); -- dispatch to appropriate Handle
            ...
        end Process_Alerts;
    

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

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


II.3 Абстрактные типы и подпрограммы

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

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

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

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

Основной пакет мог бы стать просто таким:

        package Base_Alert_System is

            type Alert is abstract tagged null record;
            procedure Handle(A: in out Alert) is abstract;

        end Base_Alert_System;
    

где мы объявляем, что тип Alert - это пустая теговая запись с абстрактной подпрограммой Handle, здесь нет тела пакета. (Обратите внимание на сокращенную форму для объявления пустой записи, которая избавляет от необходимости писать record null; end record;).

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

        with Calendar;
        with Base_Alert_System;
        package Normal_Alert_System is

            type Device is (Teletype, Console, Big_Screen);

            type Low_Alert is new Base_Alert_System.Alert with
                record
                    Time_Of_Arrival: Calendar.Time;
                    Message: Text;
                end record;

            -- now provide actual subprogram for abstract one
            procedure Handle(LA: in out Low_Alert);

            procedure Display(LA: in Low_Alert; On: in Device);
            procedure Log(LA: in Low_Alert);

            type Medium_Alert is new Low_Alert with
                record
                    Action_Officer: Person;
                end record;

            procedure Handle(MA: in out Medium_Alert);

            type High_Alert is new Medium_Alert with
                record
                    Ring_Alarm_At: Calendar.Time;
                end record;

            procedure Handle(HA: in out High_Alert);
            procedure Set_Alarm(HA: in High_Alert);

        end Normal_Alert_System;
    

В этой новой формулировке мы должны предоставить процедуру Handle для Low_Alert, чтобы удовлетворить обещанию абстрактного типа. Процедуры Display и Log теперь принимают параметр типа Low_Alert, и тип Medium_Alert более естественно наследуется от типа Low_Alert.

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

Соответствующие изменения необходимы и в теле пакета; процедура Handle, перед этим применявшаяся к типу Alert, теперь применяется к типу Low_Alert, и в процедуре Handle для типа Medium_Alert нам нужно изменить преобразование типа в вызове "родительской" процедуры Handle, которая теперь конечно Handle для типа Low_Alert. Эти две процедуры теперь:

        procedure Handle(LA: in out Low_Alert) is
        begin
            LA.Time_Of_Arrival := Calendar.Clock;
            Log(LA);
            Display(LA, Teletype);
        end Handle;

        procedure Handle(MA: in out Medium_Alert) is
        begin
            Handle(Low_Alert(MA)); -- handle as low alert
            MA.Action_Officer := Assign_Volunteer;
            Display(MA, Console);
        end Handle;
    

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


II.4 Итог о расширении типов

8-(

Итак, можно выделить следующие ключевые моменты:

  • Язык Ада 95 вводит понятие теговых типов. Только записи (и личные типы) могут быть теговыми. Значение тегового типа хранит внутри себя тег. Тег указывает конкретный тип. При помощи наследования теговый тип может быть расширен с добавлением новых компонентов.
  • Примитивные операции наследуются при порождении от тегового типа. Они включают в себя неявно определенные операции, плюс, когда тип определен в спецификации пакета, все подпрограммы, определенные в спецификации пакета, у которых есть параметр или результат этого типа. Примитивные операции могут быть переопределены в момент наследования, а также могут быть добавлены новые.
  • Теговый тип может быть объявлен как абстрактный и может иметь абстрактные подпрограммы. Абстрактная подпрограмма не имеет тела, но оно может быть определено в момент наследования. Абстрактные типы служат как основа для построения типов с некоторым общим интерфейсом.
  • T'Class обозначает классовый тип (class-wide type) построенный на типе T. Разрешено преобразование по умолчанию в значение типа T'Class. Объекты и параметры типа T'Class расцениваются как неограниченные. Соответствующий ссылочный тип может обозначать любое значение типа T'Class.
  • Вызов примитивной операции с аргументом классового типа происходит при помощи позднего связывания: выбор процедуры происходит в момент выполнения при помощи тега.
  • Еще один термин, распространенный в литературе об ООП - полиморфизм. Классовый тип называется полиморфным, так как значения этого типа имеют разную "форму" (от греческого poly - много, morphe- форма). Полиморфизм - еще одно свойство объектно-ориентированных языков.

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

II.5 Динамический выбор

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

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

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

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

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

   type Trig_Function is access function(F: Float) return Float;
   T: Trig_Function;
   X, Theta: Float;
 

и T может указывать теперь на такие функции как Sin, Cos и Tan. Затем мы можем присвоить T значение типа ссылка-на-подпрограмму, как в следующем примере:

   T := Sin'Access;
 

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

   X := T(Theta);
 

что, на самом деле, является сокращением от

   X := T.all(Theta);
 

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

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

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

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

   type Integrand is access function(X: Float) return Float;

   function Integrate(F: Integrand; From, To: Float;
                      Accuracy: Float := 1.0E-7) return Float;

 

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

   Area := Integrate(Log'Access, 1.0, 2.0);
 

чтобы вычислить площадь под кривой log(x) на участке от 1.0 до 2.0. Внутри функции Integrate будут происходить вызовы подпрограммы, переданной как параметр; это простая форма обратного вызова.

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


   type Action is access procedure;
   Action_Sequence: array(1 .. N) of Action;

   ... -- build the array

       -- and then obey it
   for I in Action_Sequence'Range loop
      Action_Sequence(I).all;
   end loop;

 

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

   package Push_Buttons is

      type Button is private;

      type Button_Response is access procedure(B: in out Button);

      function Create(...) return Button;

      procedure Push(B: in out Button);

      procedure Set_Response(B: in out Button;
                             R: in Button_Response);

      procedure Default_Response(B: in out Button);

      ...

   private
      type Button is
         record
            Response: Button_Response :=
                          Default_Response'Access;
            ...    -- other aspects of the button
         end record;
   end Push_Buttons;

 

Кнопка представлена как личная запись, содержащая компоненты, описывающие свойства кнопки (например, позицию на экране). Один из компонентов - ссылка на процедуру, которая будет выполнена в момент нажатия кнопки. Заметьте, что значение кнопки передается в процедуру, как параметр для того, чтобы процедура могла получить доступ к другим компонентам записи, описывающей кнопку. Процедура Create заполняет эти компоненты, а другие функции (не показано) предоставляют доступ к ним. Процедура Push выполняет действие по нажатию на кнопку, также предоставляется процедура по умолчанию, которая сообщает пользователю, что кнопка не установлена. Тело могло бы быть следующим:



   package body Push_Buttons is

      procedure Push(B: in out Button) is
      begin
         B.Response(B);  -- indirect call
      end Push;

      procedure Set_Response(B: in out Button;
                             R: in Button_Response) is
      begin
         B.Response := R;  -- set procedure value in record
      end Set_Response;

      procedure Default_Response(B: in out Button) is
      begin
         Put("Button not set");
         Monitor.Beep;
      end Default_Response;

      ...

   end Push_Buttons;

 

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

   Big_Red_Button: Button;

   procedure Emergency(B: in out Button) is
   begin
      -- call fire brigade etc
   end Emergency;

   ...

   Set_Response(Big_Red_Button, Emergency'Access);
   ...

   Push(Big_Red_Button);
 

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


II.6 Другие ссылочные типы

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

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

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

В Аде 95 мы можем объявить общий ссылочный тип:

   type Int_Ptr is access all Integer;
 

и теперь можем присвоить переменной типа Int_Ptr "адрес" любой переменной типа Integer при условии, что адресуемая переменная объявляется с пометкой aliased. Так, мы можем написать:

   IP: Int_Ptr;
   I: aliased Integer;
   ...
   IP := I'Access;
 

Мы можем читать и изменять значение переменной I через ссылочную переменную IP. Отметим также использование атрибут 'Access, Слово aliased - еще одно новое зарезервированное слово.

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

Мы можем ограничить доступ только на чтение, заменив all на constant в определении ссылочного типа. Это разрешит доступ на чтение к любым переменным, а также к константам:

   type Const_Int_Ptr is access constant Integer;
   CIP: Const_Int_Ptr;
   I: aliased Integer;
   C: aliased constant Integer := 1815;
 

затем

   CIP := I'Access;  -- доступ к переменной или
   CIP := C'Access;  -- доступ к константе
 

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

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

   type Ref_Count is access constant Integer range 0 .. 1;
   type Ref_Count_Array is array (Integer range <>) of Ref_Count;

   type Cell is
      record
         Life_Count: aliased Integer range 0 .. 1;
         Total_Neighbor_Count: Integer range 0 .. 8;
         Neighbor_Count: Ref_Count_Array(1 .. 8);
         ...
      end record;
 

8-( Следующим оператором мы можем соединить ячейки в соответсвии с нашей моделью:

   This_Cell.Neighbor_Count(1) :=
   Cell_To_The_North.Life_Count'Access;
 

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

   C.Total_Neighbor_Count := 0;
   for I in C.Neighbor_Count'Range loop
      C.Total_Neighbor_Count :=
          C.Total_Neighbor_Count + C.Neighbor_Count(I).all;
   end loop;
 

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

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

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


II.7 Иерархические библиотеки

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

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

Бывают случаи когда нам нужно написать два логически различных пакета, которые разделяют частный тип. В языке Ада 83 мы не можем этого сделать. Мы либо должны сделать этот тип не частным, так, что оба пакета могли бы его видеть, как, к сожалению, и все клиенты этих пакетов. Это ломает абстракцию. Либо, если мы хотим сохранить абстракцию, слить два пакета вместе. Получившийся большой монолитный пакет увеличит затраты на перекомпиляцию. (Мы не рассматриваем такие "хитрые" приемы как использование Unchecked_Conversion).

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

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

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

   package Complex_Numbers is

      type Complex is private;
      function "+" (Left, Right: Complex) return Complex;
      ... -- similarly "-", "*" and "/"

      function Cartesian_To_Complex(Real, Imag: Float)
   return Complex;
      function Real_Part(X: Complex) return Float;
      function Imag_Part(X: Complex) return Float;

   private
   ...
   end Complex_Numbers;
 

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

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

Но в языке Ада 95 мы можем добавить дочерний пакет:


   package Complex_Numbers.Polar is

      function Polar_To_Complex(R, Theta: Float) return
   Complex;
      function "abs" (Right: Complex) return Float;
      function Arg(X: Complex) return Float;

   end Complex_Numbers.Polar;

 

и внутри тела пакета мы имеем доступ самому частному типы Complex.

Отметим синтаксис, пакет с именеи P.Q - дочерний пакет своего родителя P. Мы можем думать о дочернем пакете как о находящимся в декларативной области родительского пакета, сразу за окончанием его спецификации. Большенство правил видимости следуют из этой модели. Другими словами декларативная область (в основном спецификация и тело) определенная родительским модулем также включает пространство занимаемое текстом дочерних модулей; но важно сознавать, что дочерние модуля находяться внутри области, а не просто расширяют его. Следовательно, дочерний модуль не нуждается во фразе with для родительского пакета, а родительские понятия непосредственно видимы без фразы use.

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

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

Тело дочернего пакета наших комплексных чисел могло бы быть


   package body Complex_Numbers.Polar is

      -- bodies of Polar_To_Complex etc

   end Complex_Numbers.Polar;

 

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


   with Complex_Numbers.Polar;
   package Client is
   ...

 

мы можем затем использовать обычным образом различные процедуры, написав, например, Complex_Numbers.Real_Part или Complex_Numbers.Polar.Arg и т.д.

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


   with Complex_Numbers.Polar; use Complex_Numbers;

 

разрешают нам обращаться к тем же подпрограммам при помощи имен Real_Part и Polar.Arg соответственно.

Естественно мы можем добавить


   use Complex_Numbers.Polar;

 

что даст нам возможность ссылаться на подпрограмму Polar.Arg просто как Arg.

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

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

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


   package XTK is
      type Widget is tagged private;
      type Widget_Access is access Widget'Class;
      ...
   private
      type Widget is tagged
         record
            Parent: Widget_Access;
            ...
         end record;
   end XTK;

   -- теперь разширим тип Widget

   package XTK.Color is
      type Colored_Widget is new Widget with private;
      ...
   private
      type Colored_Widget is new Widget with
         record
            Color: ...
         end record;
   end XTK.Color;

 

Интерестный момент этой конструкции в том, что клиенты на родительском уровне видят только внешние свойства общие для всех объектов Widget.