М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 42
Текст из файла (страница 42)
Динамический полиморфизм
Перед обсуждением динамического полиморфизма в языке 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++ диспетчеризация имеет место для ссылочного, но не для обычного параметра.
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. Данные класса. В этом разделе обсуждаются создание и использование компонентов данных в классе.
4. Eiffel. Язык Eiffel был разработан для поддержки ООП как единственного метода структурирования программ; поучительно сравнить конструкции языка Eiffel с конструкциями языков Ada 95 и C++, где поддержка ООП была добавлена к уже существующим языкам.
5. Проектные соображения. Каковы компромиссы между использованием класса и наследованием из класса? Для чего может использоваться наследование? Каковы взаимоотношения между перегрузкой и замещением?
-
В заключение приводится сводка методов динамического полиморфизма.
15.1. Структурированные классы
Абстрактные классы
Когда класс порождается из базового класса, предполагается, что базовый класс содержит большую часть требуемых данных и операций, тогда как производный класс всего лишь добавляет дополнительные данные, а также добавляет или изменяет некоторые операции. Во многих проектах лучше рассматривать базовый класс как некий каркас, определяющий общие операции для всего семейства производных классов. Например, семейство классов операций ввода/вывода или графики может определять такие общие операции, как get и display, которые будут определены для каждого производного класса. И Ada 95, и C++ поддерживают такие абстрактные классы.
Мы продемонстрируем абстрактные классы, описывая несколько реализаций одной и той же абстракции; абстрактный класс будет определять структуру данных Set, и производные классы — реализовывать множества двумя различными способами. В языке Ada 95 слово abstract обозначает абстрактный тип и абстрактные подпрограммы, связанные с этим типом:
Ada |
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 |
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;