лекция 20 (1161115), страница 2
Текст из файла (страница 2)
Только переменные классового типа и формальные параметры имеют динамический тип. ClanWideType – реализован в виде ссылки.
A:T ' class; //так нельзя писать без присваивания. А сразу присвоить им какое-то значение можно.
Также мы не можем объявить переменную типа неограниченный массив, мы сразу должны приписать к ней какое-нибудь уточнение.
В нашей переписанной процедуре семантика вызова f() существенно меняется - f() вызывается в зависимости от динамического типа А.
P(Y); // f() for T
P(Y1); // f() for T1
Здесь наблюдается существенная разница: когда мы писали f() мы что-нибудь сказали, про то будет ли она динамически привязана к типу или нет? Нет, не сказали. Концептуально: виртуальность функции есть виртуальность её вызова. В яп есть специальные средства, чтобы снять виртуальность вызова.
В языках, в которых мы привязаны к одному объекту, мы виртуальность привязываем для всего типа (не для конкретного вызова). А в Аде можно по-другому: любая процедура связанная с тегированным типом данных может стать динамически привязанной. Все зависит от механизма ее вызова: если она вызывается для обычного объекта – никакой динамической привязки нет. Если для объекта из классового типа – тогда имеет место динамический вызов. А объекту классового типа может соответствовать объект любого конкретного типа, который принадлежит классу соответствующего типа (совпадает с Т, либо является производным). Мы рассматривали до этого привязку к одному объекту, однако в Аде такого нет, для примера рассмотрим пример:
T=>T1; W=>W1// получаются две параллельные и независимые иерархии
procedure F(X:T; Y:W);
procedure F(X:T1; Y:W);
procedure F(X:T; Y:W1);
procedure F(X:T1; Y:W1); // Тут возникает 4 варианта.
Представим некую процедуру:
PROCEDURE PP(A:T ' class; B: W ' class) is
begin
F(A,B);// в зависимости от динамических типов А и В будет вызвана одна из 4-х возможностей
end;
- это мультиметоды (В Си++, Си#, Дельфи, Оберон, Java – монометоды.)
Замечание: Страуструп говорил, что мультиметоды - это бывает полезно. Например, для пересечения геометрических фигур (для каждого типа фигуры мы должны написать свою процедуру):
р:Figure; //указатель на фигуру
q:Figure; //указатель на фигуру
p@q.Intercept(); //метод, который зависит от 2-х аргументов
Но в Си++ мультиметодов нет.
Продолжим рассмотрение Ады95:
Создатели Ады не только продублировали функциональность других ООЯП, но и принесли свои решения. Динамическая связность – свойство вызова.
p.f();// не является ДС, у нее аргументы классового широкого типа
Если у процедуры формальный параметр обычного типа (не класса, но тегированного типа), а фактический параметр классового типа, то процедура вызывается в зависимости от динамического типа своего фактического параметра.
F(A); // динамическое связывание
F(Y); //не динамическое связывание
При вызове РР – никакого ДС нет!
Свойство модульных языков.
-
В языке Оберон нельзя смоделировать приватные типы данных:
MODULE M;
....
TYPE T* = RECORD
X:INTEGER; //Х не экспортируется, становиться как бы приватным для тех процедур и функций, которые находятся извне модуля М
END;
END M;
Если мы в том же модуле выведем тип Т1:
TYPE T1=RECORD(T)
Y:INTEGER;
END;
Все процедуры, которые мы описываем здесь и которые привязаны к типу Т1 (неважно как – статически или динамически), они имеют доступ к переменной Х. Так, скрыться от процедур и функций в одном и том же модуле (Х и У – приватные для внешних, и публик для внутренних процедур и функций) Заметим, что в Си++ аналогов нет, а в Си# есть – protected internal. Нормальные серьёзные иерархии типов для Оберона можно создавать только в пределах модуля, и это ограничение мощности языка.
-
В Аде это ограничение было обойдено при помощи дочерних пакетов:
package M is
type T is tagged private;// С одной стороны Т –тегированный (корень в иерархии типов), а с другой
приватный, чтобы реализовать инкапсуляцию (одно из необходимых условий
ООЯ)
функции над Т; //могут быть ДС
private
type T is tagged record ... //обязаны описать Т как тегированный
end record;
end M;
Что видят все модули использующие М о структуре типа Т? Ничего.
use M;
X:T; //никакой доступ к функциям Т мы получить не можем.
type T1 is new T with record
... //в увидим только добавленные здесь члены, а из Т мы ничего не увидим
end record
Разрабатывать иерархии типов можно только в пределах одного пакета, что противоречит идеологии Ада. В Аде 95 вышли из этого при помощи концепции дочернего пакета:
package M1.M is //пакет М1 является дочерним для М: дочерний пакет является как бы продолжением своего
родителя
....
Все объекты М1 имеют полный доступ к объектам Т. Фактически, пакет М1 объявляет себя дружественным к Т. Если в Си++ у нас сам класс выбирал друзей, то есть, можно набиваться "дочерью" к чужому пакету.
package M1.M is
type T1 is new T with private;//все что выводим из Т делаем приватным, но при этом видим внутреннюю
структуру Т в нашем дочернем пакете
private
type T1 is new T with record
...
end record;
end M1.M;
Дочерние пакеты используются для того, чтобы выводить новые тд из приватных тд родительских пакетов. Фактически, в Аде 95 не осталось приватных членов, зато остались защищённые.
Сделаем несколько попутных замечаний по поводу наследования вообще.
-
Во-первых, это запрещение наследования.
В языке Си++ нельзя запретить наследование. Мы, конечно, можем объявить все члены класса приватными, но запретить наследование нам никто не может.
В других языках, прежде всего в Java и Cи#, есть запрещение наследования - модификаторы:
1)Java: final
2)Cи#: sealed
-
class X
{
public final void f() {...}
}
Даже если мы наследуем X=>Y, то подменять эту функцию f() мы не можем.
-
Аналогично в Cи#:
public sealed class Path {...}; // если модификатор стоит перед именем класса – то из этого класса нельзя ничего
выводить
Sealed нужен из нескольких соображений - идейное (если ничего не нужно наследовать от этой библиотеки), безопасность (например, при проектировании библиотек авторизации), и третье (last but not least) - избавление от виртуальных вызовов (иногда из соображений эффективности снимается виртуальность вызова – актуально для Java, где все методы ДС).
public final class Y; {
f() {...}
}
Y y;
y.f(); // компилятор может снять в этом случае динамичность вызова, поскольку из У ничего быть выведено не может
Запечатанность класса говорит о том, что ДС можно снять, и работа с классом может стать более эффективной, но это крайняя мера.
2) Ещё одно замечание, по поводу видимости.
При наследовании идёт речь о новой области видимости. Пусть мы наследуем У от Х, тогда У добавляет новую часть и получается вложенная область видимости.
Умные фразы:
"Замещение имён идет по именам, а не по сигнатурам".
"Замещение имён не проходит через границы области видимости".
Представим:
class X:
int f(int); //во внешней области видимости
classY:
int f(double); //внутри области видимости
Из умных фраз следует, что когда для объекта У вызываем функцию f, то какая функция вызовется?
Y.f(1) // f(double)
Здесь речь идёт о неявных преобразованиях (В Аде, например, нет неявных преобразований, и это помогает). Мы сначала ищем в нашей области видимости точное соответствие и мы не находим его, так как мы смотрим только внутри нашей области видимости! А находим только приближенное соответствие. Это распространяется и на виртуальные и на невиртуальные функции.
class X:
int f(int); //во внешней области видимости
void f();
classY:
int f(double); //внутри области видимости
Y.f(); // ошибка! Сначала ищем имя, потом применяем к нему правила разрешения. f() не найдено в данной области видимости (найдено – но ни одно перекрытие не подходит).
Тоже действует и при множественном наследовании. Компилятор сначала ищет имя, а потом пытается применить правила разрешения.
То же самое в Си#:
class X:
int f(int) {..}
class Y:
int f(strings);
y.f(1)//ошибка
y.X::f(1) //правильный вызов для Си++
Однако в Си# такого нет, но там есть преобразования. Либо если нам хочется обеспечить доступ к закрытой функции (любая одноименная функция закрывает соответствующее имя из другой области видимости). Перекрытие имен по всех языках идет в пределах одной и той же области видимости, а класс образует область видимости.
class Y:
int f(int i) //перекрываем имя в той же области видимости
{ base f(i); }//делегируем соответствующий вызов наверх
Такое преобразование:
((X)y).f(1) - но здесь всё равно будут лишние проверки
На этом с темой о динамическом связывании закончим.
Глава 3: Абстрактные классы и интерфейсы
Напоминание: Абстрактные классы и абстрактные типы данных - две разные вещи.
Чем хорошо динамическое связывание? За чем нам нужны абстрактные классы?
Рассмотрим пример из геометрии:
Объявим класс "фигура" (если у двух объектов есть что-то общее, то всё общее вынесем в базовый класс):
class Figure {
int x, y; //точка привязки к экрану
virtual void Draw(bool erase); //применим к каждой фигуре если параметр true то стираем, иначе рисуем
void Move (int dx, int dy) { //меняем точку привязки
Draw(true);
x+=dx;
y+=dy;
Draw(false);
};
};
Метод move мы сейчас написать можем:
Draw(true);
x+=dx;
y+=dy;
Draw(false);
А вот метод Draw() зависит от фигуры и он обязан быть виртуальным.
class Point: public Figure {
public:
void Draw(bool erase) {...};//переопределяем метод
}
class Circle: ...... Draw() // и так для каждой фигуры
Figure *p;
p->Draw(false); // теперь мы знаем что это рисовании определенной фигуры
можно написать процедуру DrawAll(); если фигуры у нас в списке. Такая процедура будет идти по списку и зарисовывать фигуры. Все получается очень удобно. DrawAll() – мы можем ее скомпилировать, затем вывести из Point новый класс Rectangle, можно слинковать новый класс со старым кодом DrawAll(), которая в откомпилированном виде будет прекрасно работать с новым классом.
Проблема в том, что в таком виде программу не удастся собрать. Редактор связи выдаст ошибку – не определен метод Draw для класса Figure. Для конкретных фигур понятно как рисовать и стирать (рисовать цветом фона). Но как рисовать для вообще_любой_фигуры непонятно. Редактор связей требует от нас: раз мы везде пишем Figure, то теоретически может быть вызвано Draw для Figure. Но мы знаем, что такого не будет, так как класс Figure – фиктивный. Для таких целей введены абстрактные классы, так Figure – абстрактный(вершина в иерархии) – он нужен для того, чтобы из него выводили другие классы, в которых заменяли метод Draw.
Мы вынуждены писать в Draw для Figure некоторый код: exit(-1) + диагностика
Плохо, то что формально нет никаких препятствия вызова такой процедуры Draw, и появления объектов класса Figure, хотя по идеи их заводить вообще нельзя.
Вот для таких классов и введено понятие абстрактных классов в языке Си++. Этот класс служит только как вершина в дереве классов.
В языке Си++ есть понятие чисто виртуального метода ("pure virtual"). Такие чисто виртуальные методы мы и называем абстрактными методами. Чисто виртуальный метод – такой, у которого может не быть тела. И можно определить виртуальный метод:
virtual void Draw (bool erase) = 0; // это означает, что соответствующий ему класс является абстрактным, и в программе нельзя заводить переменные такого вида (можно заводить указатели и ссылки на них). В классах наследниках мы должны переопределить этот метод.
Figure f; //ошибка
Figure *f;//можно
f = new Point(1,0); // можно
f = new Figure(); // нельзя!
1 На любом языке модно проектировать объектно-ориентировано, если на нем моделируется отношение наследования и наличия функционального типа данных, чтобы можно было реализовывать динамическое связывание.
2 Например, когда базовый метод обрабатывает события. Есть две стратегии обработки: частичная и полная. При частичной мы частично обрабатываем событие, а потом вызываем обработчик события базового класса (нажатие на клавиши или мыши). Полная реакция – обработчик кнопки (выполняется какое-то действие и вызывать процедуру из базового класса нет нужды).