PROGLANG (1129115), страница 5
Текст из файла (страница 5)
// интерфейс наследует интерфейсы
interface I extends I1, I2, ... , IN
П
оговорим о множественном наследовании, почему Страуструпу пришлось его реализовать в языке С++, а в других языках его фактически нет? Не столько проблемы по размещению в памяти, линейное размещение прекрасно нас удовлетворяет, сколько другие проблемы, связанные с эффективностью исполнения. Если у нас
c
lass X X Y X
{ ... } Y
class Y Y Z
{ virtual f();
virtual g();
}
class Z: X, Y
{ virtual f(); }
Начало X совпадает с началом Z, а начало Y не совпадает. Следовательно, нужно динамически модифицировать адрес, добавлять смещение. А с виртуальными функциями еще деликатней.
Z* pz; ...
pz -> f(); // Z::f();
pz -> g(); // Y::f();
Каждой из этих функций передается указатель this, для функции f он правильный, а для g – не правильный. Его необходимо сдвинуть на Y, иначе он затрет X. А если
Y* py;
py -> f();
py -> g();
Указатель py может указывать и на объект Z, что тогда делать – приходиться еще одну ТВМ.
ТВМ X-Z
X
ТВМ Y-Z Z:: f 0
Y Y:: g -y
Z
Эти ТВМ отличаются от ранее рассмотренных тем, что в них, кроме адреса, еще заносится дельта для указателей this.
Причина появления множественного наследования объясняется возникшей проблемой транзита информации от корня к листьям. Стремление сделать базовый класс наиболее универсальным делает его очень раздутым, и все это растет от отца к потомкам. Для эффективного программирования принято, что множественно наследуются интерфейсы, а классы наследуются единичным наследованием.
Динамическое определение типа – RTTI
В Обероне мы использовали стражи типа для определения текущего типа, но большие программы из-за большого количества переключателей программы неудобочитаемы. В С++ стандартными приемами можно также реализовать динамическое определение типа объекта.
enum ClasID { CLASS1, CLASS2, ... }
...
class Base
{ virtual ClassID getClassID()=0;
...
}
Нужно в классах-наследниках переопределить метод getClassID(), который будет выдавать соответствующее значение ClassID. На первый взгляд это надежно, но стоит ошибиться в реализации метода getClassID, и компилятор не сможет проконтролировать.
If (p->getCkassID()==CLASS2)
{ class2* p1 = (class2 *) p;
...
}
Это очень похоже на обероновских стражей типа. Паскалевский typeof(e1) выдает только ссылку на ТВМ. Но if typeof(e1)=typeof(e2) работает эффективно, так как у е1 и е2 либо разные, либо одинаковые ТВМ соответственно. Поэтому можно просто добавить ссылку на свойства объекта, это можно сделать, подключив typeinfo.h.
class type_info
{ const char* name;
bool operator==(const type_info&);
bool operator!=(const type_info&);
before (const type_info&)
...
};
Для безопасного преобразования p1p2 нужно проверить, совпадает ли p2 с p1 или наследует ли p2 p1? Можно использовать следующие механизмы С++ (стандарт).
Общий синтаксис:
кл_слово <T> (e);
Безопасное преобразование:
dynamic_cast <T> (e); // безопасное динамическое преобразование
Если T<e , то dynamic_cast возвратит ссылку на преобразованный к типу T объект e, иначе возвратит NULL.
static_cast <T> (e); // небезопасное статическое преобразование
Эквивалентно обычному статическому преобразованию (T)e.
reinterpret_cast <T> (e);
Преобразование в не совместимый тип: литры – в килограммы. Вся ответственность по последствиям на программисте. Но ведь когда мы распечатываем значение указателя, мы же переводим его в целый, так что это не просто желание внести ненадежность в язык.
В отличие от С++, в Java есть стандартная функция getClass, позволяющая получить полную информацию о классе. В Java компоновка является частью процесса выполнения программы, поэтому в байт-коде JIT-компилятору передается вся информация об объектах.
Так как Java – чисто объектно-ориентированный язык, все методы динамические по природе (по жизни), поэтому понятно для чего есть слово final.
final class C
{ void f() {...};
...
};
C x; x.f(); - тут сразу понятно, что имеется в виду только C::f(), так как метод f() никем не может быть наследован. Поэтому, если у вас классный JIT-компилятор, то он может в данном случае существенно увеличить скорость выполнения байт-кода, иногда даже дело доходит до inline-подстановки.
Как и в языке Java в SmallTalk имеется главный класс – Object, из него выводятся все остальные классы. В SmallTalk реализована не линейная модель распределения памяти под объекты.
Object X ТВМ X Y ТВМ Y
Так как язык чисто объектно-ориентированный, то все методы виртуальные (динамические). ТВМ немного отличается от ранее рассмотренных тем, что в ней не указывается имя класса, в котором реализовано тело метода, а указан профиль его параметров.
Имя Профиль-параметров
f ...
Пусть объекту Z посылается сообщение, которое должен обработать метод f. Если в ТВМ Z имеется строка о методе f и профиль параметров соответствует вызову, то метод Z::f вызывается. Иначе происходит поиск подходящего описания в ТВМ по иерархии до базового супер-класса Object, если поиск безуспешен, то динамически выдается сообщение об ошибке. Причем, если в Java компилятор может статически определить правильность профиля параметров вызова, то SmallTalk это может сделать только динамически, пройдя до суперкласса, таким образом на вызов метода тратится много временных ресурсов.
Такой ультрадинамизм позволяет программе динамически во время выполнения производить подмену обработчика. В Java можно менять реализации интерфейсов, но все равно они заранее статически определены, и при загрузки новой реализации JIT-компилятор сможет проверить профили параметров во всех вызовах.
Следовательно, основные недостатки SmallTalk:
-
неэффективность – медленный вызов метода;
-
ненадежность – неверные вызовы (и неверные сообщения) отлавливаются только на этапе выполнения.
Но благодаря своей гибкости этот язык быстро распространился в научной среде.
При создании нового экземпляра класса нужно использовать оператор new. Но для интерфейсов подобную операцию делать не нужно, так как new выводит новый объект, а интерфейс один на весь класс. Все данные если и есть в интерфейсе являются по умолчанию final static, и, следовательно, могут существовать вне зависимости от существования объектов реализации, чего мы, в принципе, от них и ожидаем.
interface Colors
{ int blue = 1;
int red = 4;
int f();
}
class X implements Colors
{...}
X x; Colors i;
i = x // теперь можно писать i.red
i.f() // вызовется X::f()
Мы знаем, что все параметры в Java передаются по значению, поэтому для реализации, например, функции swap придется воспользоваться классами-оболочками!? Известно, что в С++: С тебе – отдельно, и ++ – отдельно. А в Java имеются как бы интегрированные пакеты, например, пакет java.lang, соответственно, и интегрированные классы. Так для числовых типов имеем:
int Int, long Long, double Double, ... и так далее.
void Swap (Long a, Long b) {...};
У классов-оболочек есть конструкторы преобразования, которые преобразуют a и b в класс Long и мы можем уже менять значение по ссылке. В Java нельзя перекрывать операции «+» и «=», но класс string свою операцию «+». Класс string содержит метод tostring, поэтому следующая запись имеет право на существование
x = y + « » + z;
Она эквивалентна
x = y.tostring + « » + z.tostring;
Ада исповедует концепцию уникальности типа, поэтому перед создателями Ada95 стояла задача сохранить совместимость со старыми программами и добавить концепцию ООП. В можно программировать, придерживаясь нескольких парадигм. При помощи нового служебного слова tagged можно указывать компилятору, что данный объект тэгированный, то есть у него есть некоторая дополнительная запись. Так
type Point is tagged такая запись может быть расширена
record
X, Y : integer;
end record;
type Rect is new Point with
record
W, H : integer;
end record;
Но Point и Rect несовместимы по присваиванию по концепции уникальности типа. Для реализации ООП пришлось ввести понятие расширенный класс – ClassWide. Тогда получаем, что Rect принадлежит расширенному классу Point’Class, а для процедур с обобщенными параметрами ослабляется запрет несовместимости типов. Если
procedure P (X:in Point’Class);
...
begin
Draw(X);
...
end;
то произойдет динамическое связывание метода.
procedure Draw (A:in Point’Class);
В С++ и Java существует 3 уровня доступа, в Ada – только 2, на уровне пакетов. Для расширения пакета, наследования его, необходим доступ к его полям, клиентского взаимодействия не достаточно. Для это введено понятие дочерние пакеты
package P.P1 is
type T1 is new T with private;
...
private
...
end P1;
Дочерний пакет при компиляции вставляется внутрь базового пакета, поэтому ему доступны все поля базового пакета.
23