Лекции (1129116), страница 32
Текст из файла (страница 32)
Последнее, о чем мы поговорим в теме объектно-ориентированного программирования – это о языке Ада.
Ада-95.
Язык Ада строился изначально строился на иных принципах чем объектно-ориентированные языки. Давайте рассмотрим как все выглядит в языке Ада-95. Рассмотрение этого языка интересно с точки зрения рассмотрения эволюции языка от одной парадигмы к другой. В Аде-95 необходимо было обеспечить преемственность с Адой-83, и в то же время добавить объектно-ориентированные свойства:
1.Инакпсуляция. Казалось бы инкапсуляция в Аде есть, но должен быть добавлен механизм защищенных членов модуля. У производного типа должны быть особые права доступа к базовому типу.
2. Наследование. В Аде должен появиться полноценный механизм наследования, который должен наследовать не только операции, но и данные, который должен позволять добавлять и переопределять данные.
3. Полиморфизм.
Поговорим о наследовании. В Аде введены т.н. тегированные (tagged) записи. Если запись обозначена как тегированная, то это тот тип данных, который может быть корнем иерархии. Очевидно, все наследники будут также тегированными. Из тегированных записей можно выводить наследников с помощью ключевого слова with. При выведении нового типа, все операции наследуются, при этом если есть процедура PROCEDURE P(x: in T1), где Т1 – тегированный тип, и если есть переменная Y типа Т2, который является наследником Т1, то можно писать так: P(Y). Т.е. здесь присутствуют те же правила совместимости типов. Причем процедуры можно переопределять при наследовании.
Чтобы появилась виртуальность был добавлен специальный класс class widetypes. Если есть некоторая иерархия: из типа Т выведены типа Т1 и Т2. Пусть одна из операций типа Т определена следующим образом:
PROCEDURE P(x: T'class);
В классах Т1 и Т2 эта процедура может переопределяться:
PROCEDURE P(x: T1'class);
PROCEDURE P(x: T2'class);
Если аргумент является классовым (как в данном случае), то такая процедура является настоящим виртуальным методом. Разумеется, такие процедуры должны иметь соответствующий интерфейс.
Лекция 26
Ada-95
Ада – это язык, на основе которого можно изучать концепцию уникальности типа (КУТ).
КУТ
-
Одному типу данных может соответствовать одному объекту
-
Типы эквивалентны только, когда равны их имена
-
Фиксированный набор операций.
Объектно-ориентированная парадигма расширяет КУТ. Как же сделать в языке, который построен полностью на КУТ, объектно-ориентированную парадигму. Поэтому вопрос Ada-95 интересен прежде всего с этой точки зрения.
Рассмотрим наследование. Чтобы сохранить совместимость с Ada-83, вводится особый класс типов данных – теггированные записи:
tagged
если некоторая запись объявлена, как
type T is tagged record ... end record;
то эта запись не подчиняется КУТ. То есть, грубо говоря, введено семейство типов данных, которые специально реализуют расширение КУТ. Тэггирование – это что-то вроде ссылки на таблицу виртуальных методов.
Объектно-ориентированное расширение Ada будет больше похоже на C++, чем на Oberon или SmallTalk, так как Ada-95 все-таки больше рассчитана на эффективность, как и C++.
Если запись обозначена, как tagged, то она может являться корнем иерархии. Для генерации наследственных типов употребляется конструкция:
type T1 is new T with record ... end record;
таким образом объекты T1 принадлежат уже обоим типам, нарушается КУТ.
Если у нас есть:
X:T; X:=X1; - это писать можно
X1: T1; X1:=X; - это нельзя.
Приведем классический пример:
type Rectangle is tagged record
Length, Width: integer;
end record;
type Cuboid is new Rectangle with
record
Height: integer;
end record;
Что же происходит с операциями? До этого по КУТ каждый тип данных характеризовался своей структурой и набором операций. Мы могли в Ada-83 сделать производный тип, но он был абсолютно несовместим со своим предком. Унаследованные методы могли работать только со своим типом. Полная несовместимость. Здесь же ситуация другая. Пусть у нас есть операция над Rectangle:
function Area(R: Rectangle) return integer;
Area унаследуется типом Cuboid так как положено в ООП. Конечно, данная функция вычислит лдя Cuboid лишь площадь основания, но мы можем переопределить Area под нужды нового типа. Заметим, что это все вполне подпадает под возможность перекрытия операций в Ada-83. Однако, что важно, даже если мы не будем переопределять Area, эта функция сможет работать и с Rectangle и с Cuboid.
Теперь, что касается инкапсуляции. В рамках старой парадигмы можно писать хитрые вещи, например:
type T2 is tagged private:
мы не хотим публиковать T2, структура расписывается уже в пакете.
type T3 is new T2 with private
мы расширяем каким-то образом, но расширяем приватно.
type T4 is new T with null record;
мы можем просто унаследовать без доопределения структуры. Это можно использовать, когда мы хотим добавить только операции.
С точки зрения наследования мы видим, что старые возможности инкапсуляции остались, а также добавились возможности, присущие другим ОО языкам.
Что происходит с динамическим полиморфизмом? Чтобы можно было ввести эту концепцию, в терминологии C++ - ввести виртуальные и невиртуальные методы (причем, как и в C++ виртуальность – механизм вызова). Надо отметить, что и в других ОО языках, где все методы виртуальные, точно также.
В Java, например, все методы должны вызываться в зависимости от динамического типа параметра, однако существует такой спецификатор final, который может стоять перед всем классом, либо перед элементом класса:
class X{
final void f( );
};
Для данных final говорит о том, что соответствующие данные не могут принимать значения, кроме указанных в конструкторе. Либо если final стоит перед методом, это означает, что метод не может переопределяться
X x;
Y y;
x=y;
x.f( ); // здесь будет вызвана X::f, несмотря на то, что динамический тип x – Y, так как перед определением f( ) стоит final.
Если final стоит перед классом, это означает, что его нельзя наследовать.
Очевидно, что в Ada-95 должно быть динамическое связывание в добавлению к статическому. Это привело к появлению новой концепции:
class wide types
x<-> tagged
X class
X class означает тип X и все унаследованные от него:
Чтобы обеспечить совместимость снизу вверх вводить новое объявление было бы не очень правильно, да и не нужно, а вот передавать параметры таких типов надо уметь:
X<-> procedure Print (a: X);
в этом случае будет вызываться статически функция X.Print.
(a: X’class) => X ->X1 -> procedure Print (a: X1’class)
при таком объявлении параметров будет учитываться динамический тип параметра. То есть у нас получается виртуальный метод.
Возникает вопрос, а можно ли в Ada-95 снять механизм виртуального вызова? В Java и C++ такая возможность есть, явно квалифицируя класс. В Ada-95 такое тоже можно сделать:
t: X; Print(t);
T’a Print(X’t)
Здесь нет никаких преобразований – просто указание компилятору трактовать t как типа X. За счет этого снимается виртуальность.
Однако, с появлением концепции наследования многие вещи просто перестают работать. Пусть у нас есть:
package P is
type T is tagged private;
private
type T is ... end record;
end P;
будет, конечно, и тело пакета, где это все будет реализовано. Конечно же, мы хотим использовать этот тип данных в наследовании:
package P1 is
use P;
type T1 is new T with private
...
private
type T1 is ...
end P1;
Скажем, мы хотим переопределить операцию над T1, например, мы хотим сделать новую процедуру Area из одного из предыдущих примеров, чтобы она еще и высоту множила.. То есть написать что-то такое:
Height*Area();
Но мы не можем так сделать. Ведь T – приватный тип, мы не можем увидеть его из другого пакета. Получается, что у нас просто нет в Ada-95 protected защищенности. Все это привело к введению концепции child units. Понятно, что пакет, назначение которого – расширение типа данных из другого пакета, должен быть принципиально более мощным, а именно, он должен иметь доступ к внутренней структуре соответствующего типа. Причем не только видеть, но и обращаться.
P1 надо было бы писать так:
package P.P1 is
type T1 is new T with private
...
private
type T1 is ...
end P.P1;
package body P.P1 is
...
здесь есть доступ как к T1 так и к T
Но данный дочерний пакет имеет доступ ко всему родительскому, тогда как в языках C++ и других родительский может регулировать доступ к своим структурам.
Понятно, что механизм инкапсуляции в Ada-95 слабее.