М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 45
Текст из файла (страница 45)
Base_Class* Base_Ptr = new Base_Class;
C++ |
if (...) Base_Ptr = Derived_Ptr;
Derived_Ptr = Base_Ptr; // На какой тип указывает Base_Ptr?
Конечно, возможно, что Base_Ptr фактически укажет на объект производного типа; в этом случае нет никакой причины отклонить присваивание. С другой стороны, если указуемый объект фактически имеет базовый тип, мы делаем попытку преобразования вниз, и присваивание должно быть отвергнуто. Чтобы предусмотреть этот случай, в языке C++ определено динамическое преобразование типов (dynamic cast), которое является условным в зависимости от типа указуемого объекта:
C++ |
Если указуемый объект фактически имеет производный тип, преобразование завершается успешно. В противном случае указателю присваивается 0, и программист может это проверить.
Уже в языке Ada 83 допускалось явное преобразование между любыми двумя типами, порожденными друг из друга. Это не вызывало никаких проблем, потому что производные типы имеют в точности те же самые компоненты. Для них допустимо иметь различные представления (см. раздел 5.8), но преобразование типов совершенно четко определено, потому что оба представления имеют одинаковые число и типы компонентов.
Расширение преобразования производного типа до теговых типов не вызывает проблем в случае преобразования вверх от производного типа к базовому. Ненужные поля усекаются:
Ada |
A: Airplane_Data := Airplane_Data(S);
В другом направлении используются агрегаты расширения (extention aggregates), чтобы обеспечить значения для полей, которые были добавлены при расширении:
Ada |
Поля Speed и подобные берутся из соответствующих полей в значении А, а дополнительное поле Mach задано явно.
При попытке преобразования вниз CW-типа к конкретному типу делается проверка во время выполнения, и, если CW-объект не производного типа, произойдет исключительная ситуация:
Ada |
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-спецификатору, feature-группа без спецификатора экспортируется в любой другой класс в системе; однако это отличается от 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, использует подход отличимого получателя, поэтому нет необходимости задавать явный параметр для объекта, подпрограмма которого должна быть вызвана:
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(...); --который теперь можно использовать