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

13. Тэговые типы (tagged types)

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

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

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

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

13.1.1 Расширение существующего типа данных

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

    
    
    type Object_1 is tagged
        record
            Field_1 : Integer;
        end record;
    

В данном случае тип Object_1 содержит всего одно поле Field_1 типа Integer, и описан как тэговый тип. Не трудно заметить, что внешний вид такого описания подобен описанию обычной записи, и отличается только наличием зарезервированного слова tagged, которое, собственно, и указывает на то, что описываемый тип является тэговым типом.

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

    
    
    type Object_2 is new  Object_1  with
        record
            Field_2 : Integer;
        end record;
    
    type Object_3 is new  Object_2  with
        record
            Field_3 : Integer;
        end record;
    

В данном примере, тип Object_2 является производным типом от типа Object_1, а тип Object_3 - производным типом от типа Object_2. Таким образом, в результате показанных описаний, получилась следующая иерархия типов:

    
    
              Object_1
                 |
              Object_2
                 |
              Object_3
    

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

В результате показанных выше описаний, типы Object_1, Object_2 и Object_3 будут содержать следующие поля:

Примечательным фактом является то, что описание типа Object_2 не указывает явно наличие поля Field_1. Это поле наследуется от типа Object_1. Также, описание типа Object_3 не указывает явно наличие полей Field_1 и Field_2. Эти поля наследуются от типа Object_2.

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

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

    
    
    declare
        Instance_1  : Object_1;
        Instance_2  : Object_2;
        Instance_3  : Object_3;
    begin
        Instance_1.Field_1 := 1;
        Instance_2 := ( 1, 2 );
        Instance_3 := ( Field_1 => 1,
                        Field_2 => 2,
                        Field_3 => 3 );
        . . .
    end;
    

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

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

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

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

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

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

13.1.3 Примитивные и не примитивные операции над тэговыми типами
          Наследование операций

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

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

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

Для уточнения сказанного рассмотрим следующий пример:

    
    
    package Simple_Objects  is
    
        type Object_1 is tagged
            record
                Field_1 : Integer;
            end record;
    
        -- примитивные операции типа Object_1
        procedure Method_1 (Self: in out Object_1);
    
    
        type Object_2 is new  Object_1  with
            record
                Field_2 : Integer;
            end record;
    
        -- примитивные операции типа Object_2
        procedure Method_2 (Self: in out Object_2);
        procedure Method_2 (Self: in out Object_1); -- НЕДОПУСТИМО!!!
                    -- должна быть примитивной операцией для Object_1,
                    -- но недопустима поскольку следует за описанием типа Object_2
                    -- который является производным от типа Object_1
    
    end Simple_Objects;
    

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

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

    
    
        . . .
        procedure Method_1 (Self: in out Object_2);
        . . .
    

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

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

    
    
    package Simple_Objects  is
    
        type Object_1 is tagged
            record
                Field_1 : Integer;
            end record;
    
        -- примитивные операции типа Object_1
        procedure Method_1 (Self: in out Object_1);
        . . .
    
    
        package Constructors  is    -- внутренний пакет содержащий не наследуемые
                                    --   операции типа Object_1
    
            function  Create (Field_1_Value: in Integer) return Object_1;
            . . .
    
        end Constructors;
        . . .
    
    end Simple_Objects;
    

Здесь, функция Create, которая возвращает значение типа Object_1, расположена во внутреннем пакете Constructors. В результате такого описания, функция Create (а также другие подпрограммы для типа Object_1, расположенные во внутреннем пакете Constructors) не будет наследоваться потомками типа Object_1 (типами, производными от типа Object_1).

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

13.1.4 "Пустые" записи (null record) и расширения

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

    
    
    type Root is tagged
        record
            null;
        end record;
    

Для таких случаев, Ада обеспечивает специальный синтаксис описания "пустых" записей:

    
    
    type Root is tagged null record;
    

Описание операций над таким типом традиционно, и может быть выполнено в спецификации пакета:

    
    
    procedure Do_Something(Item : in out Root);
    

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

    
    
    type Child is new Root with null record;
    
    procedure Child_Method (Item : in out Child);
    

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

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

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

    
    
    type Empty_Root is abstract tagged null record;
    
    type Simple_Root is abstract tagged
        record
            Simple_Field: Integer;
        end record;
    

Здесь, в первом случае, тип Empty_Root - это абстрактный тип является "пустой" записью которая не содержит никаких полей. В свою очередь, тип Simple_Root, который также описан как абстрактный, содержит единственное поле Simple_Field типа Integer.

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

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

    
    
    package Sets is
        type Set is abstract tagged null record;
    
        function Empty return Set is abstract;
        function Empty(Element : Set) return Boolean is abstract;
        function Union(Left, Right : Set) return Set is abstract;
        function Intersection(Left, Right : Set) return Set is abstract;
        procedure Insert(Element : Natural; Into : Set) is abstract;
    end Sets;
    

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

    
    
    with Sets;        use Sets;
    
    procedure Wont_Compile is
    
        My_Set : Set;        -- НЕДОПУСТИМО!!! абстрактный тип
    
    begin
        null;
    end Wont_Compile;
    

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

    
    
    with Sets;
    
    package Quick_Sets is
    
        type Bit_Vector is array(0..255) of Boolean;
        pragma Pack (Bit_Vector);
    
        type Quick_Set is new Sets.Set with
            record
                Bits : Bit_Vector := (others => False);
            end record;
    
        -- объявление конкретной реализации
        function Empty return Quick_Set;
        function Empty(Element : Quick_Set) return Boolean;
    
        . . .
    
    

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

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

13.2 Динамическое связывание и полиморфизм

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

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

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

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

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

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

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

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

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

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

    
    
    package Simple_Objects  is
    
        -- тип Root  и его примитивные операции
    
        type Root is tagged null record;
    
        function The_Name (Self: in Root) return String;
        procedure Show (Self: in Root'Class);
    
    
        -- тип Child_1  и его примитивные операции
    
        type Child_1 is new  Root  with null record;
    
        function The_Name (Self: in Child_1) return String;
    
    
        -- тип Child_2  и его примитивные операции
    
        type Child_2 is new  Root  with null record;
    
        function The_Name (Self: in Child_2) return String;
    
    
        -- тип Grand_Child_2_1  и его примитивные операции
    
        type Grand_Child_2_1 is new  Child_2  with null record;
    
        function The_Name (Self: in Grand_Child_2_1) return String;
    
    end Simple_Objects;
    

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

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

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

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

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

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

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

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

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

    
    
    procedure Show (Self : in Root'Class);
    

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

    
    
    declare
    
        Root_Instance             : Root;
        Child_1_Instance          : Child_1;
        Child_2_Instance          : Child_2;
        GRand_Child_2_1_Instance  : GRand_Child_2_1;
    
        Instance : Root'Class := Child_1'(Root with null record);
    
    begin
    
        Show (Root_Instance);
        Show (Child_1_Instance);
        Show (Child_2_Instance);
        Show (GRand_Child_2_1_Instance);
        Show (Instance);
    
    end;
    

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

    
    
    type Root_Ref is access Root'Class;
    

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

    
    
    declare
    
        Any_Instance:  Root_Ref;
    
    begin
        Any_Instance := new Child_1'(Root with null record);
    
            . . .
    
        Any_Instance := new Child_2'(Root with null record);
            . . .
    end;
    

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

13.2.2 Проверка типа объекта во время выполнения программы

В процессе выполнения программы можно осуществить проверку объекта на принадлежность его к какому-либо индивидуальному типу путем использования атрибута 'Tag

Чтобы продемонстрировать это, предположим, что реализация процедуры Show описывается следующим образом:

    
    
    procedure Show (Self: in Root'Class) is
    begin
    
        if    Self'Tag = Root'Tag  then
            Ada.Text_IO.Put_Line ("Root");
        elsif Self'Tag = Child_1'Tag  then
            Ada.Text_IO.Put_Line ("Child_1");
        elsif Self'Tag = Clild_2'Tag  then
            Ada.Text_IO.Put_Line ("Clild_2");
        elsif Self'Tag = Grand_Child_2_1'Tag  then
            Ada.Text_IO.Put_Line ("Grand_Child_2_1");
        else
            Ada.Text_IO.Put_Line ("Unknown type");
        end if;
    
    end Show;
    

Кроме того, в процессе выполнения программы, возможно осуществление проверки принадлежности (или не принадлежности) типа объекта к какому-либо классу. Для выполнения таких проверок используются операции проверки диапазона "in" и "not in":

    
    
        . . .
        if  Some_Instance in Child_1'Class   then
            . . .
        end if;
        . . .
    

В данном примере выполняется проверка принадлежности переменной Some_Instance к иерархии типов, корнем которой будет тип Child_1, причем, предполагается, что переменная Some_Instance является переменной надклассового или тэгового типа.

Следует также обратить внимание на то, что для выполнения этой проверки используется "Some_Instance in Child_1'Class", а не "Some_Instance in Child_1".

13.2.3 Динамическая диспетчеризация

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

В данном случае, подразумевается, что если мы решим описать новый тип, производный от любого типа входящего в показанную иерархию (Root, Child_1, Child_2 и Grand_Child_2_1), то результатом работы такой реализации процедуры Show всегда будет сообщение "Unknown type", извещающее о том, что фактический тип параметра Self - не известен. Например, такая ситуация может возникнуть когда мы опишем новый тип Grand_Child_1_1, производный от типа Child_1, в каком-либо другом пакете.

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

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

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

    
    
    with  Ada.Text_IO;
    
    package body Simple_Objects  is
    
        -- примитивные операции типа Root
    
        function The_Name (Self: in Root) return String is
        begin
            return ("Root");
        end The_Name;
        
        procedure Show (Self: in Root'Class) is
        begin
            Ada.Text_IO.Put_Line ( The_Name(Self) );
        end Show;
    
    
        -- примитивные операции типа Child_1
    
        function The_Name (Self: in Child_1) return String is
        begin
            return ("Child_1");
        end The_Name;
    
    
        -- примитивные операции типа Child_2
    
        function The_Name (Self: in Child_2) return String is
        begin
            return ("Child_2");
        end The_Name;
    
    
        -- примитивные операции типа Grand_Child_2_1
    
        function The_Name (Self: in Grand_Child_2_1) return String is
        begin
            return ("Grand_Child_2_1");
        end The_Name;
    
    end Simple_Objects;
    

Не сложно догадаться, что особое внимание следует обратить на реализацию процедуры Show, которая теперь, перед вызовом Ada.Text_IO.Put_Line, выдающим строку сообщения, вызывает функцию The_Name, возвращающую строку которая, собственно, содержит текст сообщения. Заметим также, что важным моментом в этой реализации является то, что процедура Show имеет формальный параметр надклассового типа, что позволяет ей принимать в качестве фактического параметра любые объекты тип которых принадлежит данной иерархии типов.

При дальнейшем внимательном рассмотрении спецификации и тела пакета Simple_Objects, следует обратить внимание на то, что функция The_Name описана для всех типов иерархии (Root, Child_1, Child_2 и Grand_Child_2_1) и является примитивной операцией для этих типов. Напомним, что в случае тэговых типов, примитивные операции являются потенциально диспетчеризуемыми операциями.

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

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

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

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

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

    
    
        ... The_Name(Any_Instance.all)...
            -- Any_Instance может обозначать объект который принадлежит любому
            -- типу класса Root'Class
    

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

    
    
    declare
        Instance_1 : Root;
        Instance_2 : Root'Class ... ;
    begin
        ... The_Name(Instance_1)...
            -- статическое связывание: компилятор знает индивидуальный тип
            -- Instance_1, поэтому он может определить реализацию
    
        ... The_Name(Instance_2)...
            -- динамическое связывание: Instance_2 может принадлежать любому
            -- типу класса Root'Class
            -- поэтому компилятор не может точно определить реализацию
            -- подпрограммы, она будет выбрана во время выполнения программы
    end ;
    

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

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

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

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

    
    
                             ------------
    Root'Tag            ---> | The_Name |---> The_Name для Root
                             ------------
    
    
                             ------------
    Child_1'Tag         ---> | The_Name |---> The_Name для Child_1
                             ------------
    
    
                             ------------
    Child_2'Tag         ---> | The_Name |---> The_Name для Child_2
                             ------------
    
    
                             ------------
    Grand_Child_2_1'Tag ---> | The_Name |---> The_Name для Grand_Child_2_1
                             ------------
    

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

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

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

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

    
    
    with  Simple_Objects;   use   Simple_Objects;
    
    package Simple_Objects_New  is
    
        type Grand_Child_1_1 is new  Child_1  with null record;
    
        procedure New_Method (Self: in Grand_Child_1_1);
        procedure New_Method_Call (Self: in Grand_Child_1_1'Class);
            . . .
    
    end Simple_Objects_New;
    

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

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

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

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

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

13.2.5 Вызов переопределенной операции предка

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

    
    
        . . .
    procedure Display (Self: in Root);
    procedure Display (Self: in Child_1);
        . . .
    

Поскольку такие операции совмещены, то можно сказать, что реализация операции Display типа Root (предка для типа Child_1) является "затененной", в виду переопределения реализации в производном типе Child_1.

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

    
    
    procedure Display (Self: in Child_1) is
    begin
        Display ( Root(Self) ); -- вызов "затененной" операции предка
        . . .
    end Display;
    

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

13.2.6 Динамическая передиспетчеризация

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

    
    
    procedure Display (Self: in Root) is
    begin
        Put ( The_Name(Self) & ... ); -- ???
        . . .
    end Display;
    

В данном случае, при вызове реализации Display для типа Child_1, которая была показана ранее, первая же инструкция выполняет вызов затененной реализации предка, то есть, реализации для типа Root: Display ( Root(Self) ).

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

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

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

    
    
    procedure Display (Self: in Root) is
    begin
        Put ( The_Name( Root'Class(Self) ) & ... ); -- передиспетчеризация
        . . .
    end Display;
    

В данном случае, в реализации процедуры Display для типа Root выполняется преобразование параметра Self к типу Root'Class предусматривая передиспетчеризацию вызова к корректной реализации The_Name, в зависимости от тэга параметра Self.

13.2.7 Двойная диспетчеризация

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

    
    
    type  Message_Type  is
        tagged record
            . . .
        end record;
    
    type  Output_Device is
        tagged record
            . . .
        end record;
    
    procedure Put (M : in Message_Type; D : in Output_Device);
                -- записать сообщение M в устройство вывода D (не допустимо!!!)
    
        . . .
    

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

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

    
    
    procedure Put (M : in Message_Type;
                   D : in Output_Device'Class);
    

Теперь, параметр D больше не имеет тип Output_Device, а значит, процедура Put больше не является примитивной операцией для типа Output_Device. Однако, внутри процедуры, значение Output_Device'Class приведет к осуществлению диспетчеризации в случае вызова примитивной операции для типа Output_Device. Подобный прием называют двойной диспетчеризацией. Например, предположим, что тип Output_Device имеет следующую примитивную операцию:

    
    
    procedure Write_Output (D : in Output_Device; S : in String);
    

В этом случае, тип Message_Child_Type, производный от типа Message_Type, может переопределить реализацию процедуры Put приблизительно следующим образом:

    
    
    procedure Put (M : in Message_Child_Type;
                   D : in Output_Device'Class) is
    begin
        Put (Message_Type(M), D);    -- вызов версии Put предка
        . . .
        Write_Output (D, ... );      -- отображение данных сообщения
        . . .
    end Put;
    

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

13.3 Стандартные низкоуровневые средства, пакет Ada.Tags

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

    
    
    package Ada.Tags is
    
        type Tag is private;
    
        function Expanded_Name(T : Tag) return String;
        function External_Tag(T : Tag) return String;
        function Internal_Tag(External : String) return Tag;
    
        Tag_Error : exception;
    
    private
    
        . . .   -- стандартом не определено
    
    end Ada.Tags;
    

Функция Expanded_Name возвращает полное расширенное имя типа, индефицируемого значением тэга, в виде строки (в верхнем регистре). Результат будет зависеть от реализации компилятора, если тип описан внутри неименованного блока инструкций.

Функция External_Tag возвращает строку, которая может быть использована для внешнего представления указанного тэга. Вызов External_Tag(S'Tag) эквивалентен обращению к атрибуту S'External_Tag.

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


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