Языки программирования - концепции и принципы

         

Даже если вы не согласны


Даже если вы не согласны с нашим про­ектным решением создать объект Заказчик, вы должны согласиться, что объ­ектно-ориентированное проектирование — хороший общий подход для об­суждения проблем разработки и достоинств одного проекта перед другим.

   В следующих разделах языковая поддержка ООП будет обсуждаться на при­мере двух языков: C++ и Ada 95. Сначала мы рассмотрим язык C++, который был разработан как добавление одной интегрированной конструкции для ООП к языку С, в котором нет поддержки даже для модулей. Затем мы увидим, как полное объектно-ориентированное программирование определено в язы­ке Ada 95 путем добавления нескольких небольших конструкций к языку Ada 83, который уже имел много свойств, частично поддерживающих ООП.

 

 

 

 

 

 

14.2. Объектно-ориентированное программирование на языке C++

 

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

• инкапсуляции и абстракции данных,

• наследования,

• динамического полиморфизма.

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

    Такие модули, как пакеты в языке Ada, инкапсулируют вычислительные ресурсы, выставляя только спецификацию интерфейса. Абстракция данных может быть достигнута через определение представления данных в закрытой части, к которой нельзя обращаться из других единиц. Единица инкапсуля­ции и абстракции в языке C++ — это класс (class), который содержит объявления подпрограмм и типов данных. Из класса создаются фактические объек­ты, называемые экземлярами(instances). Пример класса в языке C++:

class Airplane_Data {

public:

     char *get_id(char *s) const                 {return id;}

     void set_id(char *s)                            {strcpy(id, s);}



      int get_speed() const                          {return speed;}

        void set_speed(int i)                        {speed=i;}


Этот пример расширяет пример из


       int get_altitude() const                     {return altitude;}

       void set_altitude(int i)                       {altitude = i;}

 private:

          char id[80];

           int speed;

           int altitude;

};

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

class Airplanes {

 public:

     void New_Airplane(Airplane_Data, int &);

     void Get_Airplane(int, Airplane_Data &) const;

private:

    Airplane_Data database[100];

       int current_airplanes;

       int find_empty_entry();

};

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

    

   Вы можете задать вопрос, почему Airplane_Data лучше сделать отдельным классом, а не просто объявить обычной общей (public) записью. Это спорное проектное решение: данные должны быть скрыты в классе, если вы полагае­те, что внутреннее представление может измениться. Например, вы можете знать, что один заказчик предпочитает измерять высоту в английских футах, тогда как другой предпочитает метры. Определяя отдельный класс для

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

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

Aircraft_Data a;                             // Экземпляр класса

int alt;

alt = a.get_altitud(e);                     // Получить значение, скрытое в экземпляре


Программирование может стать очень утомительным,


alt = (alt* 2)+ 1000;

a.set_altitude(alt);                          // Вернуть значение в экземпляр

вместо простого оператора присваивания в случае, когда а общая (public) за­пись:

a.alt = (a.alt*2) + 1000;

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

   Однако инкапсуляция вовсе не обязана сопровождаться значительными затратами времени выполнения. Как показано в примере, тело интерфейс­ной функции может быть написано внутри объявления класса; в этом случае функция является подставляемой (встраиваемой, inline) функцией, т.е. не ис­пользуется механизм вызова подпрограммы и возврата из нее (см. гл. 7). Вместо этого код тела подпрограммы вставляется непосредственно внутрь последовательности кода в точке вызова. Поскольку при подстановке функции мы расплачиваемся пространством за время, подпрограммы должны быть очень маленькими (не более двух или трех команд). Другой фактор, который следует рассмотреть перед подстановкой подпрограммы, это то, что она вводит дополнительные условия для компиляции. Если вы изменяете подставляемую подпрограмму, все клиенты должна быть пере­компилированы.

 

14.3. Наследование

 

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

package Airplane_Package is

    type Airplane_Data is

       record

Ada

           ID:String(1..80);

          Speed: Integer range 0.. 1000;

          Altitude: Integer range 0..100;

       end record;

     procedure New_Airplane(Data: in Airplane_Data: I; out Integer);

      procedure Get_Airplane(l: in Integer; Data: out Airplane_Data);


Можно объявлять новые подпрограммы, которые


end Airplane_Package;

производный тип можно объявить в другом пакете:

Ada

type New_Airplane_Data is

new Airplane_Package.Airplane_Data;

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

procedure Display_Airplane(Data: in New_Airplane_Data);

Ada

                         -- Дополнительная подпрограмма

procedure Get_Airplane(Data: in New_Airplane_Data; I: out Integer);

                        -- Замененная подпрограмма

                        -- Подпрограмма New_Airplane скопирована из Airplane_Data

Производные типы образуют семейство типов, и значение любого типа из се­мейства может быть преобразовано в значение другого типа из этого семейства:

Ada

А1: Airplane_Data;

А2: New_Airplane_Data := New_Airplane_Data(A1);

A3: Airplane_Data := Airplane_Data(A2);

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

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

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

это реализовано через порождение одного


В языке C++ это реализовано через порождение одного класса из другого:

class SST_Data: public Airplane_Data {

private:

     float mach;

C++

public:

     float get_mach() const {return mach;};

      void set_mach(float m) {mach = m;};

};

Производный класс SST_Data получен из существующего класса Airplane_Data. Это означает, что каждый элемент данных и каждая подпро­грамма, которые определены для базового класса (base class), доступны и в производном классе. Кроме того, каждое значение производного класса SST_Data будет иметь дополнительный компонент данных mach, и есть две новые подпрограммы, которые могут применяться к значениям производно­го типа.

  

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

C++

SST_Data s;

s.set_speed(1400);                         //Унаследованная подпрограмма

s.set_mach(2.4);                           // Новая подпрограмма

Подпрограмма, вызванная для set_mach, — это подпрограмма, которая объ­явлена внутри класса SST_ Data, а подпрограмма, вызванная для set_speed, — это подпрограмма, которая унаследована от базового класса. Обратите внима­ние, что производный класс может быть откомпилирован и скомпонован без изменения и перекомпиляции базового класса; таким образом, расширение на существующий код воздействовать не должно.

14.4. Динамический полиморфизм в языке C++

 

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

class SST_Data: public Airplane_Data {

public:

       int get_spaed() const;                             // Заместить

       void set_speed(int):                                 // Заместить

};

Если задан вызов:

obj.set_speed(100);

то решение, какую именно из подпрограмм вызвать — подпрограмму, унасле­дованную из Airplane_Data, или новую в SST_ Data, — принимается во время компиляции на основе класса объекта оbj.Это называется статическим связы­ванием (static binding), или ранним связыванием (early binding), так как решение принимается до выполнения программы, и при выполнении всегда вызывает­ся одна и та же подпрограмма.


Однако вся суть наследования состоит


   Однако вся суть наследования состоит в том, чтобы создать группу классов с аналогичными свойствами, и резонно ожидать, что должна иметься возмож­ность присвоить переменной значение, принадлежащее любому из этих клас­сов. Что должно произойти, когда вызывается подпрограмма для такой переменной? Решение, какую подпрограмму вызывать, должно быть принято во время выполнения, потому что значение, содержащееся в переменной, до этого неизвестно; фактически, переменная может содержать значения разных классов в разное время выполнения программы. Термины, используемые для обозначения способности выбирать подпрограммы во время выполнения, — динамический полиморфизм, динамическое связывание, позднее связывание и дис­петчеризация во время выполнения (dynamic polymorphism, dynamic binding, late binding и run-time dispatching).

    В языке C++ используются виртуальные функции (virtual functions) для обозначения тех подпрограмм, для которых выполняется динамическое свя­зывание:

class Airplane_Data {

private:



public:

     virtual int get_speed() const;

      virtual void set_speed(int);

….

};

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

class SST_Data : public Airplane_Data {

private:

          float mach;

public:

            float get_mach() const;                          // Новая подпрограмма

            void set_mach(float m);                        // Новая подпрограмма

            virtual int get_speed() const;                 // Заместить виртуальную подпрограмму

            virtual void set_speed(int);                    // Заместить виртуальную подпрограмму



};

Рассмотрим теперь процедуру update со ссылочным параметром на базо­вый класс:

void update(Airplane_Data & d, int spd, int alt)

}

d.set_speed(spd);                                      // На какой тип указывает d??


в том, что производное значение


d.set altitude(alt);                                      //На какой тип указывает d??

}    

                                                

Airplane_Data a;

SST_Data s;

void proc()

{

            update(a, 500, 5000);                        // Вызвать с AirplaneJData

            update(s, 800,6000);                         // Вызвать с SST_Data

}

Идея производных классов состоит в том, что производное значение является базовым значением (возможно, с дополнительными полями), поэтому update может вызываться с параметром s производного класса SST_Data. При компиляции update компилятор не может знать, на что указывает d: на значе­ние Airplane_Data или на SST_Data. Поэтому он не может однозначно скомпи­лировать вызов set_speed, поскольку эта подпрограмма по-разному определена в двух классах. Следовательно, компилятор должен сгенерировать код для переключения (диспетчеризации) вызова на правильную подпрограм­му во время выполнения в зависимости от того, на что указывает d. В первом вызове ргос указатель d указывает на Airplane_Data, и вызов будет диспет-черизован на подпрограмму, определенную в классе Airplane_Data, тогда как второй — на подпрограмму, определенную в SST_ Data.

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

class Space_Plane_Data : public SST_Data {

       virtual void set_speed(int);                             // Заместить виртуальную подпрограмм private:


содержащий определение для update, не


         int reentry_speed;

};

Space_Plane_Data sp;

update(sp, 2000,30000);

файл, содержащий определение для update, не нужно перекомпилировать, даже если а) новая подпрограмма заместила set_speed и б) значение формаль­ного параметра d в update содержит дополнительное поле reentry_speed.

Когда используется динамический полиморфизм?

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

class Base_Class {

private:

     int Base_Field;

public:

      virtual void virtual_proc();

      void ordinary_proc();

};

class Derived_Class : public Base_Class {

private:

       int Derived_Field;

public:

      virtual void virtual_proc();

       void ordnary_proc(); };

Затем объявим экземпляры классов в качестве переменных. Присваивание значения производного класса переменной из базового класса разрешено:



Base_Class                     Base_0bject;

Derived_Class                Derived_Object;

if (...) Base_0bject = Derived_Object;

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

    Более того, вызов подпрограммы (виртуальной или не виртуальной) одно­значный, и компилятор может использовать статическое связывание:

Base_0bject .virtual_proc();

Base_Object.ordinary_proc();

Derived_0bject.virtual_proc();

Derived_0bject.ordinary_proc();

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

Base_Class*                      Base_Ptr = new Base_Class;

Derived_Class*                 Derived_Ptr = new Derived_Class;

if (...) Base_Ptr = Derived_Ptr;

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

зации не возникает никаких проблем,


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



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

Base_Ptr- >virtual_proc();

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

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

inti1 = 1;

int i2 = 2;

int *p1 = &i1;                      // p1 ссылается на i1

int *p2 = &i2;                      // p2 ссылается на i2

p1 = p2;                                // p1 также ссылается на i2

i1 = i2;                                  // i1 имеет то же самое значение, что и i2

вы ожидаете, что i1 == i2 и *р1 ==*р2; это, конечно, правильно, пока типы в точности совпадают, но это неверно для присваивания производного класса базовому классу из-за усечения. При использовании наследования вы долж­ны помнить, что указуемый объект может иметь тип, отличный от типа указу­емого объекта в объявлении указателя.

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

Base_Ptr = Derived_Ptr;

Base_Ptr->virtual_proc();        // Диспетчеризуется по указанному типу

Base_Ptr->ordinary_proc();     // Статическое связывание с базовым типом!!

Существует различие в семантике между двумя вызовами: вызов виртуальной подпрограммы диспетчеризуется во время выполнения в соответствии с ти­пом указуемого объекта, в данном случае Derived_Class; вызов обычной под­программы связывается статически во время компиляции в соответствии с типом указателя, ъ данном случае Base_Class.

потому что изменение, которое состоит


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

Динамическая диспетчеризация в языке C++ рассчитана на вызовы вир­туальных подпрограмм, осуществляемые через указатель или ссылку.

 

 

Реализация

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

    Если используются виртуальные подпрограммы, ситуация усложняется, потому что фактическая подпрограмма, которая должна быть вызвана, не из­вестна до времени выполнения. Обратите внимание, что, если виртуальная подпрограмма вызывается с объектом конкретного типа, в противополож­ность ссылке или указателю, то все еще может использоваться статическое связывание. С другой стороны, решение, какую именно подпрограмму следу­ет вызвать, основано на 1) имени подпрограммы и 2) классе объекта. Но первое известно во время компиляции, поэтому нам остается только смодели­ровать case-оператор по классам.

   Обычно реализация выглядит немного иначе; для каждого класса с вирту­альными подпрограммами поддерживается таблица диспетчеризации (см. рис. 14.5). Каждое значение класса должно «иметь при себе» свой индекс для входа в таблицу диспетчеризации для порождающего семейства, в котором оно определено. Элементы таблицы диспетчеризации являются указателями на таблицы переходов; в каждой таблице переходов содержатся адреса входов в виртуальные подпрограммы. Обратите внимание, что два элемента таблицы переходов могут указывать на одну и ту же процедуру; это произойдет, когда класс не замещает виртуальную подпрограмму.

в свою очередь произведен из


На рисунке cls3 произведен из



cls2, который в свою очередь произведен из базового класса cls1. Здесь cls2 заместил р2, но не р1, в то время как cls3 заместил обе подпрограммы.

Когда встречается вызов диспетчеризуемой подпрограммы ptr->p1(), вы­полняется код наподобие приведенного ниже, где мы подразумеваем, что не­явный индекс — это первое поле указуемого объекта:

load               RO.ptr                       Получить адрес объекта

load               R1 ,(RO)                   Получить индекс указуемого объекта

load               R2,&dispatch            Получить адрес таблицы отправлений 

add                R2.R1                        Вычислить адрес таблицы переходов

load               R3,(R2)                     Получить адрес таблицы переходов

load               R4,p1(R3)                 Получить адрес процедуры

call               (R4)                            Вызвать процедуру, адрес которой находится в R4

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

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


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


14.5. Объектно- ориентированное программирование на языке Ada 95

 

В языке Ada 83 наличие пакетной конструкции обеспечивает полную поддержку инкапсуляции, а наличие производных типов частично обеспе­чивает наследование. Полного наследования нет, потому что, когда вы произ­водите новый тип, то можете добавлять только новые операции, но не новые компоненты данных. Кроме того, единственный полиморфизм — это стати­ческий полиморфизм вариантных записей. В языке Ada 95 поддерживается полное наследование за счет того, что программисту дается возможность рас­ширить запись производного типа. Чтобы обозначить, что родительский тип записи пригоден для наследования, его нужно объявить как теговый (tagged)  тип записи:

package Airplane_Package is

     type Airplane_Data is tagged

          record

               ID:String(1..80);

               Speed: Integer range 0..1000;

               Altitude: Integer range 0..100;

          end record;

end Airplane_Package;

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

Чтобы создать абстрактный тип данных, тип должен быть объявлен как приватный и полное объявление типа дано в закрытой части:

package Airplane_Package is

              type Airplane_Data is tagged private;

              procedure Set_ID(A: in out Airplane_Data; S: in String);

              function Get_ID(A: Airplane_Data) return String;

              procedure Set_Speed(A: in out Airplane_Data; I: in Integer);

              function Get_Speed(A: Airplane_Data) return Integer;

               procedure Set_Altitude(A: in out Airplane_Data; I: in Integer);


определенные внутри спецификации пакета, содержащей


               function Get_Altitude(A: Airplane_Data) return Integer;

private

       type Airplane_Data is tagged

          record

              ID:String(1..80);

              Speed: Integer range 0..1000;

              Altitude: Integer range 0.. 100;

           end record;

end Airplane_Package;

Подпрограммы, определенные внутри спецификации пакета, содержащей объявление тегового типа (наряду со стандартными операциями на типе), на­зываются примитивными операциями, или операциями-примитивами (primitive operations) и являются подпрограммами, которые наследуются. Наследование выполняется за счет расширения (extending) тегового типа:

with Airplane_Package; use Airplane_Package;

package SST_Package is

       type SST_Data is new Airplane_Data with

         record

           Mach: Float;

         end record;

       procedure Set_Speed(A: in out SST_Data; I: iri Integer);

       function Get_Speed(A: SST_Data) return Integer;

end SST_Package;

Значения этого производного типа являются копиями значений родительско­го типа Airplane_Data вместе с (with) дополнительным полем записи Mach. Операции, определенные для этого типа, являются копиями элементарных подпрограмм; эти операции могут быть замещены. Конечно, для производно­го типа могут быть объявлены другие самостоятельные подпрограммы.

   В языке Ada нет специального синтаксиса для вызова подпрограмм-примитивов:

A: Airplane_Data;

Set_Speed(A, 100);

С точки зрения синтаксиса объект А — это обычный параметр; И по его типу компилятор может решить, какую именно подпрограмму вызвать. Параметр называется управляющим, Потому что он управляет тем, какую подпрограмму выбрать. Управляющий параметр не обязан быть первым параметром, и их мо­жет быть несколько (при условии, что все они того же типа). Сравните это с языком C++, который использует специальный синтаксис, чтобы вы-звать подпрограмму, объявленную в классе:

C++

Airplane_Data а;


distinguished receiver) сообщения set_speed. Отличимый


a.set_speed(100);

Объект а является отличимым получателем ( distinguished receiver) сообщения set_speed. Отличимый получатель является неявным параметром, в данном случае обозначающим, что скорость (speed) будет установлена (set) для объ­екта а.

 

Динамический полиморфизм

Перед обсуждением динамического полиморфизма в языке Ada 95 мы долж­ны коснуться различий в терминологии языка Ada и других объектно-ориен­тированных языков.

    В языке C++ термин класс обозначает тип данных, который используется для создания экземпляров объектов этого типа. Язык Ada 95 продолжает ис­пользовать термины типы и объекты даже для теговых типов и объектов, которые известны в других языках как классы и экземпляры. Слово класс ис-| пользуется для обозначения набора всех типов, которые порождаются от об-|щего предка, в языке C++ мы их назвали семейством классов. Нижеследую­щее обсуждение лучше всего провести в правильной терминологии языка Ada 95; будьте внимательны и не перепутайте новое применение слова класс с его использованием в языке C++.

    С каждым теговым типом Т связан тип, который обозначается как T'Class

и называется типом класса (class-wide type)". T'Class покрывает (covered) все

 типы, производные от Т. Тип класса — это неограниченный тип, и объявить

объект этого типа, не задав ограничений, нельзя, подобно объявлению

 неограниченного массива:

type Vector is array(lnteger range <>) of Float;

V1: Vector;                                                          -- Запрещено, нет ограничений

type Airplane_Data is tagged record . . . end record;

A1: Airplane_Data'Class:                                  -- Запрещено, нет ограничений

Объект типа класса может быть объявлен, если задать начальное значение:

V2: Vector := (1 ..20=>0.0);                                -- Правильно, ограничен

Х2: Airplane_Data;                                              -- Правильно, конкретный тип


в случае массива, коль скоро


ХЗ: SST_Data;                                                      -- Правильно, конкретный тип

А2: Airplane_Data'Class := Х2;                           -- Правильно, ограничен

A3: Airplane_Data'Class := ХЗ;                           --Правильно, ограничен

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

procedure P(S: String; С: in Airplane_Data'Class) is

     Local_String: String := S;

     Local_Airplane: Airplane_Data'Class := C;

Begin



end P;

Динамический полиморфизм имеет место, когда фактический параметр имеет тип класса, в то время как формальный параметр — конкретного типа, принадлежащего классу:

with Airplane_Package; use Airplane_Package;

with SST_Package; use SST_Package;

procedure Main is

     procedure Proc(C: in out Airplane_Data'Class; I: in Integer) is

     begin

         Set_Speed(C, I);                                              -- Какого типа С ??

     end Proc;

     A: Airplane_Data;

     S: SST_Data;

begin        -- Main

   Proc(A, 500);                                                    -- Вызвать с Airplane_Data

    Proc(S, 1000);                                                  -- Вызвать с SST_Data end Main:

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

    Рисунок 14.6 поможет вам понять роль формальных и фактических парамет­ров в диспетчеризации. Вызов Set_Speed вверху рисунка делается с фактиче­ским параметром типа класса. Это означает, что только при вызове подпрограм­мы мы знаем, имеет ли фактический параметр тип Airplane_Data или SST_Data.

Однако каждое обтъявление процедуры, показанное


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

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

Set_Speed(A, 500);

Set_Speed(S, 1000);

Точно так же, если формальный параметр имеет тип класса, то никакая диспет­черизация не нужна. Вызовы Ргос — это вызовы отдельной однозначной про-





цедуры; формальный параметр имеет тип класса, который соответствует фак­тическому параметру любого типа, относящегося к классу. Что касается рис. 14.7, то, если бы объявление Set_Speed было задано как:

procedure Set_Speed(A: in out Airplane'Class: I: in Integer);

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

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

type Class_Ptr is access Airplane_Data'Class;

Ptr: Class_Ptr := new Airplane_Data;

if (...) then Ptr := new SST_Data; end if;

Set_Speed(Ptr.all);                                              -- На какой именно тип указывает Ptr??

Динамический полиморфизм в языке Ada 95 имеет место, когда фактиче­ский параметр относится к CW-типу, а формальный параметр относится к конкретному типу.

 

Реализации диспетчеризации во время выполнения в языках Ada 95 и C++ похожи, тогда как условия для диспетчеризации совершенно разные:


подпрограмма должна быть объявлена виртуальной,


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

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

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

14.6. Упражнения

 

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

2. Объявили бы вы Aircraft_Data абстрактным типом данных или сделали поля класса открытыми?

3. Проверьте, что можно наследовать из класса в языке C++ или из тегового пакета в языке Ada 95 без перекомпиляции существующего кода.

4. Опишите неоднородную очередь на языке Ada 95: объявите теговый тип Item, определите очередь в терминах Item, а затем породите из Item производные типы — булев, целочисленный и символьный.

5. Опишите неоднородную очередь на языке C++.

6.  Проверьте, что в языке C++ диспетчеризация имеет место для ссылочного, но не для обычного параметра.


В языке Ada 95 теговый


7. В языке Ada 95 теговый тип может быть расширен приватными добавлениями:

with Airplane_Package; use Airplane_Package;

package SST_Package is

        type SST_Data is new Airplane_Data with private;

        procedure Set_Speed(A: in out SST_Data; I: in Integer);

        function Get_Speed(A: SST_Data) return Integer;

private



end SST_Package;

Каковы преимущества и недостатки такого расширения?

8. Изучите машинные команды, сгенерированные компилятором Ada 95 или C++ для динамического полиморфизма.

Глава 15

 

Еще об

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

программировании

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

1. Структурированные классы.

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

• Родовые подпрограммы (Ada) и шаблоны (C++) можно комбинировать с наследованием для параметризации классов другими классами.

• Множественное наследование: класс может быть производным от двух или нескольких родительских классов и наследовать данные и операции каждого из них.

 

2. Доступ к приватным компонентам: Являются компоненты в закрытой ча­сти пакета или класса всегда приватными, или их можно экспортировать производным классам или клиентам?

 

3. Данные класса. В этом разделе обсуждаются создание и использование компонентов данных в классе.

 


Язык Eiffel был разработан для


4. Eiffel. Язык Eiffel был разработан для поддержки ООП как единственно­го метода структурирования программ; поучительно сравнить конструкции языка Eiffel с конструкциями языков Ada 95 и C++, где поддержка ООП была добавлена к уже существующим языкам.

 

5. Проектные соображения. Каковы компромиссы между использованием класса и наследованием из класса? Для чего может использоваться наследова­ние? Каковы взаимоотношения между перегрузкой и замещением?

 

6.             В заключение приводится сводка методов динамического полимор­физма.

15.1. Структурированные классы

 

Абстрактные классы

Когда класс порождается из базового класса, предполагается, что базовый класс содержит большую часть требуемых данных и операций, тогда как производный класс всего лишь добавляет дополнительные данные, а также добавляет или изменяет некоторые операции. Во многих проектах лучше рас­сматривать базовый класс как некий каркас, определяющий общие операции для всего семейства производных классов. Например, семейство классов опе­раций ввода/вывода или графики может определять такие общие операции, как get и display, которые будут определены для каждого производного клас­са. И Ada 95, и C++ поддерживают такие абстрактные классы.

    Мы продемонстрируем абстрактные классы, описывая несколько реализа­ций одной и той же абстракции; абстрактный класс будет определять структу­ру данных Set, и производные классы — реализовывать множества двумя раз­личными способами. В языке Ada 95 слово abstract обозначает абстрактный тип и абстрактные подпрограммы, связанные с этим типом:

Ada

package Set_Package is

       type Set is abstract tagged null record;

       function Union(S1, S2: Set) return Set is abstract;

       function Intersection(S1, S2: Set) return Set is abstract;

end Set_Package;


Вы не можете объявить объект


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

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

with Set_Package;

package Bit_Set_Package is

      type Set is new Set_Package.Set with private;

      function Union(S1, S2: Set) return Set;

      function lntersection(S1, S2: Set) return Set;

Ada

private

     type Bit_Array is array(1..100) of Boolean;

     type Set is new Set_Package.Set with

        record

            Data: Bit_Array;

        end record;

end Bit_Set_Package;

Конечно, необходимо тело пакета, чтобы реализовать операции.

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

with Bit_Set_Package; use Bit_Set_Package;

procedure Main is

      S1.S2, S3: Set;

Ada

begin

       S1 := Union(S2, S3);

end Main;

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

with Set_Package;

package Linked_Set_Package is

      type Set is new Set_Package.Set with private;

      function Union(S1, S2: Set) return Set;

Ada

      function lntersection(S1, S2: Set) return Set;

private

      type Node;

      type Pointer is access Node;

       type Set is new Set_Package.Set with

           record

                  Head: Pointer;

          end record;

end Linked_Set_Package;

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


Ada with Linked_Set_Package; use Linked_Set_Package;


Ada with Linked_Set_Package; use Linked_Set_Package;

Ada

procedure Main is

       S1.S2, S3: Set;

begin

       S1 := Union(S2, S3);

end Main;

В C++ абстрактный класс создается с помощью объявления чистой виртуаль­ной функции, обозначенной «начальным значением» 0 для функции.

    Абстрактный класс для множеств в языке C++ выглядит следующим обра­зом:

class Set {

C++

public:

      virtual void Union(Set&, Set&) = 0;

      virtual void lntersection(Set&, Set&) = 0;

   };

У абстрактных классов не бывает экземпляров; абстрактный класс может только быть базовым для производных классов:

class Bit_Set: public Set {

public:

     virtual void Union(Set&, Set&);

     virtual void lntersection(Set&, Set&);

C++

private:

      int data[100];

};

class Linked_Set: public Set {

public:

     virtual void Union(Set&, Set&);

     virtual void lntersection(Set&, Set&);

private:

      int data;

      Set *next;

};

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

void proc()

{

C++

Bit_Setb1,b2, bЗ;

Linked_Set 11,12,l3;

b1.Union(b2,b3);

H.Union(l2,I3);

}

Обратите внимание на разницу в синтаксисе двух языков, которая вызвана разными подходами к ООП. В языке Ada 95 определяется обычная функция, которая получает два множества и возвращает третье. В языке C++ одно из множеств — отличимый получатель сообщения. Для

b1.Union(b2,b3);

 

 

подразумевается, что экземпляр b1, отличимый получатель операции Union,  получит результат операции от двух параметров — Ь2 и bЗ — и использует его, • чтобы заменить текущее значение внутренних данных.

    Возможно, вы предпочтете перегрузить предопределенные операции, например «+» и «*», вместо того чтобы использовать имена Union и Intersection. Это можно сделать как в C++, так и в Ada 95.

     Все реализации абстрактного класса покрываются типом класса (CW-типом) Set'Class.

к правильному конкретному типу, т.


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

 

 

 

Родовые возможности

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

чтобы элементы списка можно было сортировать:

generic

     type Item is private;

     with function "<"(X, Y: in Item) return Boolean;

Ada

package List_Package is

     type List is private;

     procedure Put(l: in Item; L: in out List);

     procedure Get(l: out Item; L: in out List);

private

     type List is array( 1.. 100) of Item;

end List_Package;

Этот пакет теперь может быть конкретизирован для любого типа элемента:

Ada

 package Integer_list is new List_Package(lnteger, Integer."<");

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

lnt_List_1, lnt_List_2: lnteger_List.List;

lnteger_List.Put(42, lnt_List_1 );

lnteger_List.Put(59, lnt_List_2);

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

with Set_Package;

Ada

generic

      type Set_Class is new Set_Package.Set; package Set_IO is


Эта спецификация означает, что родовой




end Set_IO;

Эта спецификация означает, что родовой пакет может быть конкретизирован с любым типом, производным от тегового типа Set, такого как Bit_Set и Linked_Set. Все операции из Set, такие как Union, могут использоваться внут­ри родового пакета, потому что из модели контракта мы знаем, что любая конкретизация будет с типом, производным от Set, и, следовательно, она на­следует или замещает эти операции.

Шаблоны

В языке C++ можно определять шаблоны классов:

Ada

template <class ltem>

     class List {

            void put(const Item &);

};

Как только шаблон класса определен, вы можете определять объекты этого класса, задавая параметр шаблона:

C++

List<int>lnt_List1;

       // lnt_List1 является экземпляром класса List с параметром int

Так же как и язык Ada, C++ позволяет программисту для объектов-экземп­ляров класса задать свои программы (процесс называется специализацией, spe­cialization) или воспользоваться по умолчанию подпрограммами, которые су­ществуют для класса. Есть важное различие родовых пакетов Ada и шаблонов C++. В языке Ada конкретизация родового пакета, который определяет тип, даст вам конкретный пакет, содержащий конкретный тип. Чтобы получить объект, потребуется еще один шаг. В C++ конкретизация дает объект сразу, не определяя конкретного класса. Чтобы определить другой объект, нужно просто конкретизировать шаблон снова:

C++

List<int>Int_List2;                                      //Другой объект 

                

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

     Следующее различие между языками состоит в том, что C++ не использу­ет модель контракта, поэтому не исключено, что конкретизация вызовет ошибку компиляции в самом шаблоне (см. раздел 10.3).

 

 

Множественное наследование


Ранее обсуждалось порождение классов от


Ранее обсуждалось порождение классов от одного базового класса, так что се­мейство классов образовывало дерево. При объектно-ориентированном про­ектировании, вероятно, класс будет иметь характеристики двух или несколь­ких существующих классов, и кажется допустимым порождать класс из не­скольких базовых классов. Это называется множественным наследованием (multiple inheritance). На рисунке 15.1 показано, что Airplane (самолет) может



быть многократно порожден из Winged_Vehicle (летательный аппарат с крыльями) и Motorized_Vehicle (летательный аппарат с мотором), в то время как Winged_Vehicle также является (единственным) базовым классом для Glider (планер). Задав два класса:

class Winged_Vehicle {

public:

    void display(int);

C++

protected:

      int Wing_Length;                               // Размах крыла

      int Weight;                                         // Bec

};

class Motorized_Vehicle {

public:

      void display(int);

protected:

      int Power;                                   // Мощность

       int Weight;                                // Bec

};

можно породить класс с помощью множественного наследования:

class Airplane:

C++

       public Winged_Vehicle, public Motorized_Vehicle {

public:

          void display_all();

};

Чтобы использовать множественное наследование, необходимо решить, что делать с данными и операциями, такими как Weight и display, которые насле­дуются из нескольких базовых классов. В языке C++ неоднозначность, вы­званная многократно определенными компонентами, должна быть явно раз­решена с помощью операции уточнения области действия:

void Airplane: :display_all()

{

C++

         Winged_Vehicle::display(Wing_Length);

          Winged_Vehicle::display(Winged_ Vehicle:: Weight);

          Motorized_ Vehicle:: display(Power);

          Motorized_ Vehicle:: display(Motorized_ Vehicle:: Weight);


Это нельзя считать удачным решением,


};

Это нельзя считать удачным решением, так как вся идея наследования в том, чтобы допускался прямой доступ к данным и операциям базы, если не требуется их модификации. Реализовать множественное наследование на­много труднее, чем простое наследование, которое мы описали в разделе 14.4. Более подробно см. разделы с 10.1с по 10.1с упомянутого ранее справочного руководства по языку C++.

    Значение множественного наследования в ООП является предметом для дискуссии. Некоторые языки программирования, такие как Eiffel, поддержи­вают использование множественного наследования, в то время как языки, по­добные Ada 95 и Smalltalk, не имеют таких средств. При этом утверждается, что проблемы, которые можно решить с помощью множественного наследо­вания, изящно решаются с использованием других средств языка. Например, выше мы отмечали, что родовые параметры теговых типов в языке Ada 95 можно использовать для создания новых абстракций, комбинируя уже суще­ствующие абстракции. Очевидно, что наличие возможности множественного наследования оказывает глубокое влияние на проектирование и программи­рование объектно-ориентированной системы. Таким образом, трудно гово­рить об объектно-ориентированном проекте, не зависящем от языка; даже на самых ранних стадиях проектирования вам следует ориентироваться на конк­ретный язык программирования.

5.2. Доступ к приватным компонентам

 

 

 

<<Друзья>> в языке C++

 

Внутри объявления класса в языке C++ можно включать объявление «друже-ственных» (friend) подпрограмм или классов, представляющих собой под-программы или классы, которые имеют полный доступ к приватным данным  операциям класса:

class Airplane_Data {

private:

     int speed;

     friend void proc(const Airplane_Data &, int &);

     friend class CL;

};

Подпрограмма ргос и подпрограммы класса CL могут обращаться к приват­ным компонентам Airplane_Data:

void proc(const Airplane_Data & a, int & i)


Подпрограмма ргос может затем передавать


{

      i = a.speed;                                                                 // Правильно, мы — друзья

}

Подпрограмма ргос может затем передавать внутренние компоненты класса, используя ссылочные параметры, или указатели, как показано выше. Таким образом, «друг» выставил на всеобщее обозрение все секреты абстракции.

      Мотив для предоставления такого доступа к приватным элементам взят из операционных систем, в которых были предусмотрены механизмы явного предоставления привилегий, называемых возможностями (capabilities). Это понятие меньше соответствует языкам программирования, потому что одна из целей ООП состоит в том, чтобы создавать закрытые, пригодные для по­вторного использования компоненты. Идея «друзей» проблематична с проектной точки зрения, поскольку предполагает, что компонент располагает знанием о том, кто им воспользуется, а это определенно несовместимо с идеей многократного использования компонентов, которые вы покупаете или заимствуете из других проектов. Другая серьезная проблема, связанная с конструкцией friend, состоит в слишком частом использовании ее для «за­плат» в программе, вместо переосмысления абстракции. Чрезмерное употреб­ление конструкции friend, очевидно, разрушит абстракции, которые были так тщательно разработаны.

     Допустимо применение «друзей», когда абстракция составлена из двух самостоятельных элементов. В этом случае могут быть объявлены два класса, которые являются «друзьями» друг друга. Например, предположим, что клас­су Keyboard (клавиатура) необходим прямой доступ к классу Display (дисплей), чтобы воспроизвести эхо-символ; и наоборот, класс Display должен быть в состоянии поместить символ, полученный из интерфейса сенсор­ного экрана, во внутренний буфер класса Keyboard:

class Display {

private:

     void echo(char с);

     friend class Keyboard;                                      // Разрешить классу Keyboard вызывать echo

};

class Keyboard {

private:

      void put_key(char c);


Использование механизма friend позволяет избежать


      friend class Display;                                        // Разрешить классу Display вызывать put_key

};

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

     С помощью friend можно также решить проблему синтаксиса, связанную с тем фактом, что подпрограмма в классе C++ имеет отличимый получатель, такой как obj1 при вызове obj1.proc(obj2). Это привносит в подпрограммы асимметрию, в противном случае они были бы симметричны по параметрам. Стандартный пример — перегрузка арифметических операций. Предполо­жим, что мы хотим перегрузить «+» для комплексных чисел и в то же время позволить операции неявно преобразовать параметр с плавающей точкой в комплексное значение:

complex operator + (float);

complex operator + (complex);

Рассмотрим выражение х + у, где одна из переменных (х или у) может быть с плавающей точкой, а другая комплексной. Первое объявление правильно для комплексного х и плавающего у, потому что х+у эквивалентно x.operator+(y), и, стало быть, будет диспетчеризованно отличимому получателю комплекс­ного типа. Однако второе объявление для х+у, где х имеет тип с плавающей точкой, приведет к попытке диспетчеризоваться к операции с плавающей точкой, но операция была объявлена в комплексном классе.

    Решение состоит в том, чтобы объявить эти операции как «друзей» класса, а не как операции класса:

friend complex operator + (complex, complex);

friend complex operator + (complex, float);

friend complex operator + (float, complex);

Хотя эта конструкция популярна в языке C++, на самом деле существует луч­шее решение, при котором не требуется friend.

     Оператор «+=» можно определить как функцию-член (см. справочное руководство, стр. 249), а затем «+» можно определить как обычную функцию за пределами класса:


Когда один класс порождается из


complex operator + (float left, complex right)

{

        complex result = complex(left);

        result + = right;                                               // Результат является отличимым получателем

        return result;

}

Спецификаторы доступа в языке C++

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

class Airplanes {

private:

       Airplane_Data database [100];

};

class Jets : public Airplanes {

        void process Jet(int);

};

        void Jets::process_jet(int i)

 {

        Airplane_Data d = database[i] ;                                // Ошибка, нет доступа!

};

Если объявлен экземпляр класса Jets, он будет содержать память для database, но этот компонент недоступен для любой подпрограммы в произ­водном классе.

       Есть три спецификатора доступа в языке C++:

• Общий (public) компонент доступен для любого пользователя класса.

• Защищенный (protected) компонент доступен внутри данного класса и внутри производного класса.

• Приватный компонент доступен только внутри класса.

В примере, если database просто защищенный, а не приватный член класса, к нему можно обращаться из производного класса Jets:

class Airplanes {

protected:

        Airplane_Data database[100];

};

class Jets : public Airplanes {

       void process_jet(int);

};

       void Jets::process_jet(int i)

{

       Airplane_Data d = database[i];                        // Правильно, в производном классе

};

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


допускает изменение доступности компонентов класса


     Язык C++ допускает изменение доступности компонентов класса при объявлении производного класса. Обычно порождение бывает общим (public). Так было во всех наших примерах, и при этом сохранялась доступность, заданная в базовом класс. Однако вы также можете задать приватное порожде­ние, тогда и общие, и защищенные компоненты становятся приватными:

class Airplanes {

protected:

      Airplane_Data database [100];

};

class Jets : private Airplanes {                                           // Приватное порождение

           void process_jet(int);

};

           void Jets::process_jet(int i)

{

            Airplane_Data d = database[i];                              // Ошибка, нет доступа

};

Пакеты-дети в языке Ada

В языке Ada только тело пакета имеет доступ к приватным объявлениям. Это делает невозможным непосредственное совместное использование пакетами приватных объявлений так, как это можно делать в языке C++ с защищенны­ми объявлениями. В языке Ada 95 для совместного использования приватных объявлений доставлено специальное средство структурирования, так называ­емые пакеты-дети (child packages). Здесь мы ограничим обсуждение пакетов-детей только для этой цели, хотя они чрезвычайно полезны в любой ситуации, когда вы хотите расширить существующий пакет без его изменения или пере­компиляции.

     Зададим приватный тип Airplane_Data, определенный в пакете:

package Airptane_Package is

       type Airplane_Data is tagged private;

private

       type Airplane_Data is tagged

           record

               ID:String(1..80);

              Speed: Integer range 0.. 1000;

              Altitude: Integer 0.. 100;

            end record;

end Airplane_Package;

Этот тип может быть расширен в пакете-ребенке:

package Airplane_Package.SST_Package is

      type SST_Data is tagged private;

      procedure Set_Speed(A: in out SST_Data; I: in Integer);

 private

      type SST.Data is new Airplane_Data with


то Р2 принадлежит области родителя


           record

               Mach: Float;

          end record;

end Airplane_Package.SST_Package;

Если задан пакет P1 и его ребенок Р1 .Р2, то Р2 принадлежит области родителя Р1, как если бы он был объявлен сразу после спецификации родителя. Внут­ри закрытой части и теле ребенка видимы приватные объявления родителя:

package body Airplane_Package.SST_Package is

     procedure Set_Speed(A: in out SST_Data; I: in Integer) is

      begin

         A.Speed := I;                                   -- Правильно, приватное поле в родителе

       end Set_Speed;

end Airplane_Package.SST_Package;

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

15.3. Данные класса

 

Конструкторы и деструкторы

Конструктор (constructor) — это подпрограмма, которая вызывается, когда создается объект класса; когда объект уничтожается, вызывается деструктор (destructor). Фактически, каждый объект (переменная), определенный в ка­ком-либо языке, требует выполнения некоторой обработки при создании и уничтожении переменной хотя бы для выделения и освобождения памяти. В объектно-ориентированных языках программист может задать такую обра­ботку.

     Конструкторы и деструкторы в языке C++ могут быть определены для лю­бого класса; фактически, если вы не определяете их сами, компилятор обес­печит предусмотренные по умолчанию. Синтаксически конструктор — это подпрограмма с именем класса, а деструктор — то же имя с префиксным сим­волом «~»:

class Airplanes {

private:

C++

     Airplane_Data database [100];

     int current_airplanes;

public:

     Airplanes(int i = 0): current_airplanes(i) {};

     ~Airplanes();

};

После создания базы данных Airplanes число самолетов получает значение па­раметра i, который по умолчанию имеет значение ноль:

Airplanes а1 (15);                        // current_airplanes =15


Когда база данных удаляется, будет


Airplanes a2;                                //current_airplanes = О

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

class Airplanes {

public:

     Airplanes(int i = 0): current_airplanes(i) {};

C++

     Airplanes(int i, int j): current_alrplanes(i+j) {};

     ~Airptartes();

};

Airplanes a3(5,6);                       // current_airplanes = 11

В языке C++ также есть конструктор копирования (copy constructor), который дает возможность программисту задать свою обработку для случая, когда объ­ект инициализируется значением существующего объекта или, в более общем случае, когда один объект присваивается другому. Полное определение кон­структоров и деструкторов в языке C++ довольно сложное; более подробно см. гл. 12 справочного руководства по языку C++.

     В языке Ada 95 явные конструкторы и деструкторы обычно не объявля­ются. Для простой инициализации переменных достаточно использовать зна­чения по умолчанию для полей записи:

type Airplanes is tagged

     record

          Current_Airplanes: Integer := 0;

      end record;

Ada

или дискриминанты (см. раздел 10.4):

type Airplanes(lnitial: Integer) is tagged

     record

         Current_Airplanes: Integer := Initial;

      end record;

Программист может определить свои обработчики, порождая тип из абстрак­тного типа, называемого управляемым (Controlled). Этот тип обеспечивает аб­страктные подпрограммы для Инициализации (Initialization), Завершения (Finalization) и Корректировки (Adjust) для присваивания, которые вы можете заместить нужными вам программами. За деталями нужно обратиться к пакету Ada. Finalization, описанному в разделе 7.6 справочного руководства по языку Ada.

Class-wide-объекты

Память распределяется для каждого экземпляра класса:


по 100 символов для с1


C++

class С {

      chars[100];

};

                 С с1,с2;                                                // по 100 символов для с1 и с2

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

package body P is

    Last: Integer := 0;

Ada

end P;

в то время как в языке'C++ нужно воспользоваться другим синтаксисом:

class С {

C++

        static int last;                                //Объявление

        chars[100];

};

     int C::last = 0;                                  // Определение, доступное за пределами файла

Спецификатор static в данном случае означает, что будет заведен один CW-объект*. Вы должны явно определить компонент static за пределами определе­ния класса. Обратите внимание, что статический (static) компонент класса имеет внешнее связывание и может быть доступен из других файлов, в отли­чие от статического объявления в области файла.

Преобразование вверх и вниз

В разделе 14.4 мы описали, как в языке C++ значение порожденного класса может быть неявно преобразовано в значение базового класса. Это называет­ся преобразованием вверх (up-conversion), потому что преобразование делается вверх от потомка к любому из его предков. Это также называется сужением (narrowing), Потому что производный тип «широкий» (так как он имеет допол­нительные поля), в то время как базовый тип «узкий», он имеет только поля, которые являются общими для всех типов в производном семействе. Запом­ните, что преобразование вверх происходит только, когда значение произ­водного типа непосредственно присваивается переменной базового типа, а не когда указатель присваивается от одной переменной другой.


нию производного типа не допускается,


    Преобразование вниз (down-conversion) от значения базового типа к значе­ нию производного типа не допускается, поскольку мы не знаем, какие значе­ния включить в дополнительные поля. Рассмотрим, однако, указатель на ба­зовый тип:

Base_Class*                    Base_Ptr = new Base_Class;

C++

Derived_Class*               Derived_Ptr = new Derived_Class;

if (...) Base_Ptr = Derived_Ptr;

Derived_Ptr = Base_Ptr;                     // На какой тип указывает Base_Ptr?

Конечно, возможно, что Base_Ptr фактически укажет на объект производно­го типа; в этом случае нет никакой причины отклонить присваивание. С дру­гой стороны, если указуемый объект фактически имеет базовый тип, мы дела­ем попытку преобразования вниз, и присваивание должно быть отвергнуто. Чтобы предусмотреть этот случай, в языке C++ определено динамическое пре­образование типов (dynamic cast), которое является условным в зависимости от типа указуемого объекта:

C++

Derived_Ptr = dynamic_cast<Derived_Class*>Base_Ptr;

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

     Уже в языке Ada 83 допускалось явное преобразование между любыми дву­мя типами, порожденными друг из друга. Это не вызывало никаких проблем, потому что производные типы имеют в точности те же самые компоненты. Для них допустимо иметь различные представления (см. раздел 5.8), но пре­образование типов совершенно четко определено, потому что оба представле­ния имеют одинаковые число и типы компонентов.

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

Ada

S:SST_Data;

A: Airplane_Data := Airplane_Data(S);

В другом направлении используются агрегаты расширения (extention aggregates), чтобы обеспечить значения для полей, которые были добавлены при расширении:


и подобные берутся из соответствующих


Ada

S:=(AwithMach=>1.7);

Поля Speed и подобные берутся из соответствующих полей в значении А, а дополнительное поле Mach задано явно.

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

Ada

I Ada procedure P(C: Airplane_Data'Class) is

        S:SST_Data;

begin

        S := SST_Data(C);                                         - Какой тип у С ??

exception

       when Constraint_Error => .. .

end P;

15.4. Язык программирования Eiffel

 

Основные характеристики языка программирования Eiffel:

• Язык Eiffel изначально создавался как объектно-ориентированный, а не как дополнительная пристройка для поддержки ООП в существующем языке.

• В языке Eiffel программу можно построить единственным способом — как систему классов, которые являются клиентами друга друга или на­следуются один из другого.

• Поскольку наследование — это основная конструкция структурирова­ния, центральное место в языке занимает стандартная библиотека клас­сов (связанных наследованием).

• Не будучи частью «языка», развитая среда программирования была со­здана группой разработчиков языка Eiffel. Среда включает ориентиро­ванную на язык поддержку для отображения и изменения классов, для инкрементной компиляции и для тестирования и отладки.

     В отличие от языка Smalltalk (который имеет аналогичные характеристики), язык Eiffel жестко придерживается статического контроля соответствия типов наряду с динамическим полиморфизмом, как в языках Ada 95 и C++. Eiffel идет дальше в попытках поддерживать надежное программирование, интегрируя утверждения в язык, как обсуждалось

в разделе 11.5.

     Единственная программная единица в Eiffel — это класс: никаких файлов, как в языках С и C++, и никаких пакетов, как в языке Ada.

     Терминология языка Eiffel отличается от других языков: подпрограммы (процедуры и функции) называются рутинами (routine), объекты (переменные и константы) называются атрибутами (attribute), а рутины и атрибуты, которые входят в состав класса, называются свойствами (feature) класса.

По существу, нет никакого различия


По существу, нет никакого различия между функциями и константами: подобно литералу пере­числения языка Ada, константа рассматривается просто как функция без пара­метров. Язык Eiffel статически типизирован, подобно языку C++, в том смысле, что при присваиваниях и при передаче параметров типы должны соот­ветствовать друг другу, и это соответствие может быть проверено во время ком­пиляции. Однако язык не имеет таких богатых конструкций для управления со­ответствием типов, как подтипы и числовые типы (numerics) языка Ada.

    Когда объявляется класс, задается список свойств:

class Airplanes

feature                                                                              -- "public"

    New_Airplane(Airplane_Data): Integer is

Do

       ….

       end; -- New_Airplane Get_Airplane(lnteger): Airplane_Data is

       do

       ….

       end; -- Get_Airplane

feature {} --"private"

database: ARRAY[Airplane_Data];

current_airpianes: Integer;

find_empty_entry: Integer is

do

     …

end; -- find_empty_entry

end; -- class Airplanes

     Как и в языке C++, набор свойств может быть сгруппирован, и для каждой такой feature-группы может быть определена своя доступность, feature-груп­па со спецификатором, который изображает пустое множество «{}», не экс­портируется ни в какой другой класс, подобно private-спецификатору, fea­ture-группа без спецификатора экспортируется в любой другой класс в систе­ме; однако это отличается от public-спецификатора в языке C++ и от откры­той части спецификации пакета в языке Ada, потому что экспортируется толь­ко доступ для чтения. Кроме того, вы можете явно написать список классов в feature-спецификаторе; этим классам будет разрешен доступ к свойствам внутри группы, подобно «друзьям» в языке C++.

     В языке Eiffel нет реального различия между предопределенными типами и типами, определенными программистом, database — это объект класса ARRAY, который является предопределенным в библиотеке языка Eiffel.

очень общее понятие; как мы


Ко­нечно, «массив» — очень общее понятие; как мы должны указать тип элемен­тов массива? Нужно применить тот же самый метод, который использовал бы программист для параметризации любого типа данных: обобщения (genetics). Встроенный класс ARRAY имеет один родовой параметр, который использует­ся, чтобы определить тип элементов:

class ARRAY[G]

Когда объявляется объект типа ARRAY, должен быть задан фактический па­раметр, в данном случае Airplane_Data. В отличие от языков Ada и C++, кото­рые имеют специальный синтаксис для объявления встроенных составных типов, в языке Eiffel все создается из родовых классов с помощью единого на­бора синтаксических и семантических правил.

   Обобщения широко используются в языке Eiffel, потому что библиотека содержит определения многих родовых классов, которые вы можете специа­лизировать для своих конкретных требований. Родовые классы также могут быть ограниченными (constrained), чтобы работала модель контракта между родовым классом и его конкретизацией, как это делается в языке Ada (см. раз­дел 10.3). Ограничения задаются не сопоставлением с образцом, а указанием имени класса, для которого фактический родовой параметр должен быть про­изводным. Например, следующий родовой класс может быть конкретизи­рован только типами, производными от REAL:

class Trigonometry[R -> REAL]

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

Наследование

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

Наверху иерархии находится класс, называющийся


Наверху иерархии находится класс, называющийся ANY. Присваи­вание и равенство определены внутри ANY, но могут быть замещены внутри класса. Синтаксис для наследования такой же, как в языке C++: унаследован­ные классы перечисляются после имени класса. Если задан класс Airplane_Data:

class Airplane_Data

feature

     Set_Speed(l: Integer) is...

     Get_Speed: Integer is....

feature {}

       ID: STRING;

       Speed: Integer;

      Altitude: Integer;

end;                                             -- class Airplane_Data

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

class SSTJData inherit

     Airplane_Data

           redefine

                Set_Speed, Get_Speed

          end

feature

        Set_Speed(l: Integer) is...

        Get_Speed: Integer is...

feature {}

       Mach: Real;

end; — class SST_Data

Все свойства в базовом классе наследуются с их экспортируемыми атрибута­ми в неизменном виде. Однако для производного класса программист может переопределить некоторые или все унаследованные свойства. Переопределя­емые свойства должны быть явно перечислены в redefine-конструкции, кото­рая следует за inherit-конструкцией. Кроме переопределения, свойство можно просто переименовать. Обратите внимание, что унаследованное свойство мо­жет быть реэкспортировано из класса, даже если оно было приватным в базо­вом классе (в отличие от языков C++ и Ada 95, которые не разрешают втор­гаться в ранее скрытую реализацию).

    Среда языка Eiffel может отображать плоскую (flat) версию класса, которая показывает все действующие на данный момент свойства, даже если они бы­ли унаследованы и повторно объявлены где-то еще в иерархии. Таким обра­зом, интерфейс класса отчетливо отображается, и программисту не нужно «раскапывать» иерархию, чтобы точно увидеть, что было переобъявлено, а что не было.

    Eiffel, аналогично языку C++, но, в отличие от языка Ada 95, использует подход отличимого получателя, поэтому нет необходимости задавать явный параметр для объекта, подпрограмма которого должна быть вызвана:


В языке EifFel нет никаких


A: Airplane_Data;

A.Set_Speed(250);

 

 

Распределение памяти

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

database: expanded ARRAY[Airplane_Data];

Кроме того, класс может быть объявлен как расширенный, и все его объекты будут доступны непосредственно. Само собой разумеется, что встроенные ти­пы Integer, Character и т.д. являются расширенными.

      Обратите внимание, что оператор присваивания или проверки равен­ства

X :=Y;

дает четыре варианта, в зависимости от того, являются объекты X и Y расширенными оба, либо только один из них, либо ни тот ни другой. В языках Ada и C++ программист отвечает за то, чтобы различать, когда подразумева­ется присваивание указателя, а когда — присваивание обозначенных объек­тов. В языке EifFel присваивание прозрачно для программиста, а значение каждого варианта в языке тщательно определено.

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

A: Airplane_Data;

S: SST_Data;

A:=S;

Если распределение было статическим, в объекте А не будет «места» для до­полнительного поля Mach из S. Когда используется косвенное распределе­ние, присваивание — это, по сути, просто копирование указателя. Сравните это с языками Ada 95 и C++, в которых требуются дополнительные понятия: CW-типы и указатели для присваивания, которые поддерживают конкретный тип.

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

Замещая унаследованное определение присваивания, вы


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

Динамический полиморфизм получаем как непосредственное следствие. Возьмем

A.Set_Speed(250);

Компилятор не имеет никакой возможности узнать, является конкретный тип значения, находящегося в данный момент в А, базовым типом Air-plane_Data для А или некоторым типом, порожденным из Airplane_Data. Так как подпрог­рамма Set_Speed была переопределена, по крайней мере, в одном порожден­ном классе, должна выполняться диспетчеризация во время выполнения. Об­ратите внимание, что не требуется никакого специального синтаксиса или се­мантики: все вызовы потенциально динамические, хотя компилятор проведет оптимизацию и использует статическое связывание, где это возможно.

 

 

Абстрактные классы

Абстрактные классы в языке Eiffel такие же, как в языках C++ и Ada 95. Класс или свойство в классе может быть объявлено как отсроченное (deferred). Отсроченный класс должен быть сделан конкретным при помощи эффективизации (effecting) всех отсроченных свойств, т. е. предоставления ре­ализации. Обратите внимание, что, в отличие от языков C++ и Ada 95, вы мо­жете объявить объект, чей тип отсрочен; вы получаете null-указатель, который не может использоваться до тех пор, пока ему не будет присвоено значение имеющего силу производного типа:

deferred class Set...                             -- Абстрактный класс

class Bit_Set inherit Set...                   -- Конкретный класс

S: Set;                                                  -- Абстрактный объект!

В: Bit_Set;                                            - Конкретный объект

!!B;                                                       --Создать экземпляр В

S := В;                                               -- Правильно,S получает конкретный объект,

S.Union(...);                                       --который теперь можно использовать

 

 

 


как разрешить неоднозначности, если имя


 

 

 

 

 

Множественное наследование

Язык Eiffel поддерживает множественное наследование:

class Winged_Vehicle

feature

           Weight: Integer;

           display is . .. end;

end;

class Motorized_Vehicle

feature

          Weight: Integer;

          display is ... end;

end;

class Airplane inherit

              Winged_Vehicle, Motorized_Vehicle

              …

end;

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

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

rename- и redef ine-конструкции могут использоваться для изменения имен по мере необходимости. В примере класс Airplane наследует только одно по­ле Weight. Очевидно, по замыслу предлагалось для класса иметь два поля Weight, одно для корпуса летательного аппарата и одно для двигателя. Это­го можно достичь за счет переименования двух унаследованных объектов:

class Airplane inherit

       Winged_Vehicle

              rename Weight as Airframe_Weight;

        Motorized_Vehicle

              rename Weight as Engine_Weight;

        …

end;

Предположим теперь, что мы хотим заместить подпрограмму display. Мы не можем использовать redefine, потому что при этом возникла бы неоднознач­ность указания подпрограммы, которую мы переопределяем. Решение состоит в том, чтобы использовать undefine для отмены определений обеих унасле­дованных подпрограмм и написать новую:

class Airplane inherit

         Winged_Vehicle

                undefine display end;

          Motorized_Vehicle

                 undefine display end;


В справочном руководстве по языку


feature

display is... end;

end;

В справочном руководстве по языку Eiftel подробно обсуждается использова­ние rename, redefine и undefine для разрешения неоднозначности при множе­ственном наследовании.

15.5. Проектные соображения

 

 

Наследование и композиция

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

with Airplane_Package;

package SS"f.Package is

       type SST_Data is private;

private

       type SST_Data is

            record

                A: Airplane. Data;

                 Mach: Float;

            end record;

end SST_Package;

и в языке C++ класс может включать экземпляр другого класса как элемент:

class SST_Data {

private:

   Airplane_Data a;

    float mach;

};

Композиция — более простая операция, чем наследование, потому что для ее поддержки не требуется никаких новых конструкций языка; любая поддерж­ка инкапсуляции модуля автоматически дает вам возможности для композиции абстракций. Родовые единицы, которые в любом случае необхо­димы в языке с проверкой соответствия типов, также могут использоваться для формирования абстракций. Наследование, однако, требует сложной под­держки языка (теговых записей в языке Ada и виртуальных функций в языке C++) и дополнительных затрат при выполнении на динамическую диспетче­ризацию.

    Если вам нужна динамическая диспетчеризация, то вы должны, конеч­но, выбрать наследование, а не композицию. Однако, если динамической диспетчеризации нет, выбор зависит только от решения вопроса, какой ме­тод дает «лучший» проект. Вспомните, что язык C++ требует, чтобы при со­здании базового класса вы решили, должна ли выполняться динамическая диспетчеризация, объявляя одну или несколько подпрограмм как виртуаль­ные; эти и только эти подпрограммы будут участвовать в диспетчеризации.

В языке Ada 95 динамическая


В языке Ada 95 динамическая диспетчеризация потенциально произойдет в любой подпрограмме, объявленной с управляющим параметром тегового типа:

type T is tagged ...;

procedure Proc(Parm: T);

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

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

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

     Нет никакой опасности при аккуратном и продуманном использовании любого метода; проблемы могут возникнуть, когда наследование использует­ся беспорядочно, поскольку при этом может возникнуть слишком много за­висимостей между компонентами программной системы. Мы оставляем под­робное обсуждение относительных достоинств этих двух понятий специализированным работам по ООП. О преимуществах наследования см. книгу Мейера по конструированию объектно-ориентированного программного обеспечения (Meyer, Object-oriented Software Construction, Prentice-Hall International, 1988), особенно гл. 14 и 19. Сравните ее с точкой зрения предпочтения композиции, выраженной в статье J.P. Rosen, «What orien­tation should Ada objects take?» Communications of the ACM, 35(11), 1992, стр. 71—76.


Удобно разделить случаи применения наследования


Использование наследования

Удобно разделить случаи применения наследования на несколько катего­рий:

Подобие поведения. SST ведет себя как Airplane. Это простое применение наследования для совместного использования кода: операции, подходя­щие для Airplane, подходят для SST. Операции при необходимости могут быть замещены.

 

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

 

Родовая совместимость. Общие свойства наследуются несколькими клас­сами. Эта методика применяется в больших библиотеках, таких как в языках Smalltalk или Eiffel, где общие свойства выносятся в классы-пре­дки, иногда называемые аспект-классами (aspect classes). Например, класс Comparable (сравнимый) мог бы использоваться для объявления таких операций отношения, как «<», и любой такой класс, как Integer или Float, обладающий такими операциями, наследуется из Compara­ble.

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

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

Перегрузка и полиморфизм

Хотя перегрузка (overloading) — это форма полиморфизма («многофор-менности»), эти две концепции применяются в совершенно разных целях.

Перегрузка используется как удобное средство


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

C++

void proc put(int);

void proc put(float);

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

C++

virtual void set_speed(int):

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

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

C++

class SST_Data : public Airplane_Data {

public:

       void set_speed(float);                                                 //float, а не int

};

Правила языка C++ определяют, что эта подпрограмма и не перегружает, и не замещает подпрограмму в базовом классе; вместо этого она скрывает определение в базовом классе точно так же, как внутренняя область дей­ствия!

    Язык Ada 95 допускает сосуществование перегрузки и замещения :

with Airplane_Package; use Airplane_Package;

package SST_Package is

Ada

        type SSTJData is new Airplane_Data with ...

    procedure Set_Speed(A: in out SST_Data; I: in Integer);

                                       -- Замещает примитивную подпрограмму из Airplane_Package procedure Set_Speed(A: in out SST_Data; I: in Float);

                                       -- Перегрузка, не подпрограмма-примитив

end SST_Package;

Поскольку нет примитивной подпрограммы Set_Speed с параметром Float для родительского типа, второе объявление — это просто самостоятельная подпрограмма, которая перегружает то же самое имя. Хотя это допустимо, этого следует избегать, потому что пользователь типа, скорее всего, запута­ется.

жете сказать, какая именно подпрограмма


Посмотрев только на SST_Package (и без комментариев!), вы не смо­ жете сказать, какая именно подпрограмма замещается, а какая перегру­жается:

Ada

procedure Proc(A: Airplane_Data'Class) is

begin

              Set_Speed(A, 500);                                -- Правильно, диспетчеризуется

              Set_Speed(A, 500.0);                             -- Ошибка, не может диспетчеризоваться!

end Proc;

15.6. Методы динамического полиморфизма

 

Мы заключаем эту главу подведением итогов по динамическому полимор­физму в языках для объектно-ориентированного программирования.

 

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

 

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

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

Ada  95. Динамическая диспетчеризация неявно используется для примитивных подпрограмм тегового типа, когда фактический параметр является CW-типом, а формальный параметр имеет конкретный тип. Затраты на диспетчеризацию во время выполнения фиксированы.

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


Реализуйте пакеты на языке Ada


15.7. Упражнения

 

1. Реализуйте пакеты на языке Ada 95 и классы на языке C++ для работы с множествами.

2. Может ли абстрактный тип в языке Ada 95 или абстрактный класс в язы­ке C++ иметь компоненты-данные? Если так, для чего они могли бы ис­пользоваться?

type Item is abstract tagged

Ada

    record

        I: Integer;

     end record;

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

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

5. Подробно изучите множественное наследование в языке Eiffel и сравни­те его с множественным наследованием в языке C++.

6. Стандартный пример множественного наследования в языке Eiffel -спи­сок фиксированного размера, реализованный с помощью наследова­ния, как от списка, так и от массива. Как бы вы написали такие ADT (аб­страктные типы данных) на языке Ada 95, в котором нет множественно­го наследования?

7. Чем опасно определение защищенных (protected) данных в языке C++? Относится ли это также к пакетам-детям в языке Ada 95?

7.             Изучите структуру стандартной библиотеки в языке Ada 95, в котором широко используются пакеты-дети. Сравните ее со структурой стандар­тных классов ввода-вывода в языке C++.

9. Изучите пакет Finalization в языке Ada 95, который может использовать­ся для написания конструкторов и деструкторов. Сравните его с конст­рукциями языка C++.

10. Какова связь между операторами присваивания и конструкторами/де структорами?

11. Дайте примеры использования CW-объектов.

5Непроцедурные

      языки

      программирования

 

 

 

Глава 16

Функциональное программирование

 

 

16.1. Почему именно функциональное программирование?

 

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

Машины Тьюринга очень похожи на


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

    Формулировка модели вычислений Черча (названная лямбда-исчислением) совершенно другая — она основана на математическом понятии функции. Эта формулировка полностью эквивалентна формулировке Тьюринга в смысле представления вычислений, которые могут быть точно описаны, но в качестве формализма, применяемого для вычислений на практике, функцио­нальный подход всегда был менее популярен. В языке Lisp, разработанном в 1956 г., для вычислений используется функциональный подход, подобный модели лямбда-исчисления, хотя многие его особенности поощряют стиль процедурного программирования.

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

    Многие проблемы, с которыми мы сталкиваемся при написании надежной программы, возникают непосредственно из-за использования обновляемой памяти:

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

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

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

При функциональном подходе обе эти


При функциональном подходе обе эти про­блемы исчезают.

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

16.2. Функции

 

Функции определяются в языке ML заданием равенства между именем функ­ции с формальным параметром и выражением:

fun even n = (n mod 2 = 0)

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

even 4 = true

even 5 = false

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

even: int -> bool

Это означает, что она отображает значение целочисленного типа в значение булева типа.

В языке ML выражения могут содержать условия:

fun min (x,y) = if x < у then x else у

Приведем пример вычисления при применении функции:

min (4,5) =

(if x < у then x else у) (4,5) =

if 4 < 5 then 4 else 5 =

if true then 4 else 5 =

4

Обратите внимание, что это не if-оператор, а условное выражение, аналогич­ное имеющемуся в языке С:

х< у?х:у

Какой тип у min? В функциональном программировании считается, что фун­кция имеет в точности один аргумент, если же вам требуется большее число аргументов, вы должны создать кортеж (двойной, тройной и т.д.), используя функцию декартова произведения. Таким образом, (4,5) имеет тип int x int, a функция min имеет тип:

min: (int x int) -> int

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

fun min_c x у = if x < у then x else у

Это карризованная функция (curriedfunction, от имени математика Н.В.

вом обращении создается другая функция,


Сипу). Когда эта функция применяется к последовательности аргументов, при пер­ вом обращении создается другая функция, которая затем применяется ко вто­рому аргументу.

Функция min_c берет один целочисленный аргумент и создает новую фун­кцию, также с одним аргументом:

min_c 4 = if 4 < у then 4 else у

Эта функция может затем применяться к другому одиночному аргументу:

min_c 4 5 =

(if 4 < у then 4 else у) 5 =

if 4 < 5 then 4 else 5 =

if true then 4 else 5 =

4

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

fun min_4 = min_c 4

min_4 5 =

(if 4 < у then 4 else y) 5 =

if 4 < 5 then 4 else 5 =

if true then 4 else 5 =

4

16.3. Составные типы

 

Списки

 

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

[2,3,5,7,11 ]                                  [true, false, false]

имеют типы int list и bool list, соответственно. Список можно создать также с помощью конструкторов (constructors); конструкторы списка — это [] для пус­того списка, и element::list для непустого списка, создаваемого добавлением элемента (element) к существующему списку (list). Конструкторы могут ис­пользоваться при определении функций путем сопоставления с образцом:

fun                           member [] e = false

|                               member [e :: tail] e = true

j                               member [e1 :: tail] e = member tail e

Тип функции member (член) определяется как:

member: int list x jnt -> boolean

и это можно прочитать следующим образом:

Когда функция member применяется к списку L, а (затем) к элементу е, вычисление основывается на вариантах выбора в зависимости от аргумен­тов: 1) если L пуст, е не является членом L; 2) если е — первый элемент L, то е является членом L; 3) в противном случае, е1, первый элемент списка L, отличен от е, и мы (рекурсивно) проверяем, является ли е членом остав­шейся части списка L.


В языке ML вам не


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

    Обратите внимание, что эта функция рекурсивная. Рекурсия чрезвычайно важна в функциональных языках программирования; при отсутствии «опера­торов» это единственный способ создания циклов при вычислении выраже­ния.

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

fun        insertion_sort [] = []

|             insertion_sort head:: tail =

              insert_element head insertion_sort tail

and

fun        insert_element x [] = [x]

|             insert_element x head:: tail =

              if x < head then x::head:: tail

              else head:: (insert_element x tail)

Эти функции имеют типы:

insertion_sort: int list -> int list

insert_element: int -> int list -> int list

Как только вы привыкнете к этой записи, читать такие программы будет про­сто:

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

Элемент х вставляется в пустой список, создавая список из одного элемен­та. Чтобы вставить х в непустой список, нужно сравнить х с первым эле­ментом (head) списка: 1) если х меньше head, сделать х новым первым эле­ментом списка; 2) в противном случае создать новый список, составлен­ный из head, за которым следует остаток списка, в который вставлен х.