Набрали: Валентин Буров, Илья Тюрин

Вид материалаЛекция

Содержание


Глава 2. Динамическое связывание методов.
Type node = record
Procedure add (s : shape; var l : shape)
Var l:shape
Type node = record
Подобный материал:
1   ...   11   12   13   14   15   16   17   18   19
^

Глава 2. Динамическое связывание методов.



Мы начнем с языка Оберон, потому что это самый простой язык из нами рассматриваемых. Вспомним пример, в котором мы определяли указатель Shape на некоторую структуру данных Node. Из этой структуры наследовались типы PointObj, LineObj и т.д., для которых существовали соответствующие указатели Point, Line и т.д. То есть наследование базовых типов переносилось на указатели. В качестве примера работы с этими объектами, мы написали процедуру Draw, которая рисовала все объекты из списка типа Shape. Гибкой реализации этой процедуры мы добились только тогда, когда определили интерфейс для Shape, в котором были обработчики Draw, Move и т.д. Для каждого объекта был определен соответствующий обработчик (т.е. для каждого объекта значение указателя на процедуру было разным), и при инициализации соответствующего объекта инициализировался соответствующий обработчик. Т.е. мы в каждом объекте переопределяли данный обработчик. В этом и заключается суть динамического связывания методов. Главный обработчик выглядел так: this.Draw(this). И мы не могли сказать, глядя на эту запись, какой же именно объект будет рисоваться.

Какие недостатки у такого метода? Во-первых, это потенциальная ненадежность, потому что мы можем испортить соответствующую структуру данных Shape. Мы можем присвоить не тот метод Draw для данного объекта. При этом весь контроль ложиться на программиста. К тому же в программе не должно существовать объектов типа Shape, потому что для них нет смысла определять обработчик. В языке должна быть исключена возможность создания объектов типа Shape.

Поэтому появилось некоторое расширение языка Оберон, а именно Оберон-2. Оберон-2 – это уже настоящий объектно-ориентированный язык, по мощности сравнимый с С++. К этому языку были добавлены процедуры, динамически привязанные к типу.

Суть динамического привязывания состоит в следующем. Мы для некоторого базового класса определяется интерфейс, который может выглядеть так:


^ TYPE NODE = RECORD

X, Y : INTEGER;

NEXT : SHAPE;

END;


Существует еще некоторый процедурный интерфейс который, например, может содержать процедуру включения в список фигуры:


^ PROCEDURE ADD (S : SHAPE; VAR L : SHAPE);


Эта процедура не меняется в зависимости от фигуры, т.е. в терминологии языка С++, она не является виртуальной. Есть еще другой функциональный интерфейс, а именно, процедуры, динамически привязанные к типу:

PROCEDURE (P:SHAPE) Draw();


Эта процедура по синтаксису выглядит иначе. В данном случае, она не имеет параметров, а параметр Р играет роль указателя this. Чем отличаются такие процедуры по своей семантике от обычных? В языке Оберон (и Оберон-2) никакого статического перекрытия нет, т.е. отсутствует статический полиморфизм. Имя процедуры в пределах модуля должно быть уникально. Для процедур привязанных к типу все наоборот: они должны быть переопределены. Допустим мы выводим новый тип данных из SHAPE:


TYPE PointObj = RECORD(NODE)

END;

Point = POINTER TO PointObj;


PROCEDURE (P:Point) Draw()

BEGIN



END Draw;


Этот тип данных ничего, с точки зрения данных не дает, но зато он наследует всю функциональность типа NODE. Мы должны переопределить процедуру Draw, для того, чтобы она вызывалась для объектов типа Point. Тоже самое делается для типов данных Line, Circle и др. Как работать с этой процедурой?

^ VAR L:SHAPE;

….

L.Draw();


Этот вызов вызывает процедуру, динамически привязанную к типу SHAPE. Т.е. в зависимости от динамического типа L вызывается соответствующий вариант этой процедуры. Заметьте, что в данном случае, уже никаких процедурных переменных не используется, отсутствует инициализация. Все делается автоматически соответствующей Run-Time системой. При этом компилятор гарантирует надежность, т.е. программисту в теле соответствующего обработчика не требуется писать проверок.

С точки зрения межмодульного интерфейса в языке Оберон есть соответствующие инструменты, которые позволяют сгенерировать интерфейсное описание модуля DEFENITION. Интересно, что DEFENITION для типа NODE будет выглядеть следующим образом:


^ TYPE NODE = RECORD

PROCEDURE Draw;

END;


PROCEDURE ADD();


Обычные процедуры будут выписаны отдельно. А динамически привязанные методы являются как бы частью класса NODE.

Когда мы пишем L.Draw(), то как раз L и выступает как соответствующий аргумент. А в реализации Draw мы используем Р как указатель this. Естественно, что процедура DrawAll() будет выглядеть почти точно также:


WHILE this # NIL DO

this.Draw();

this := this.NEXT;

END;


Теперь не требуется передавать обработчику соответствующего параметра. Т.е. в гибкости мы ничего не потеряли, а приобрели в надежности. И это за счет того, что механизм динамической привязки был реализован как часть языка.


Давайте рассмотрим язык С++. Аналогично, в С++ все функции-члены делятся на два класса: виртуальные и невиртуальные. Виртуальные функции и являются аналогом процедур, динамически привязанных к типу. Рассмотрим пример, который поясняет принцип работы виртуальных функций.


class X{

virtual int f(){cout << "X::f";}

int g(){cout << "X::g";}



}


Пусть есть еще один класс:


class Y: public X{

virtual int f(){cout << "Y::f";} // слово virtual необязательно, функция уже виртуальна

int g(){cout << "Y::g";}



}


X* px = new X;

Y* py = new Y;


px->f(); px->g(); // X::f X::g

py->f(); py->g(); // Y::f Y::g


px = py; //Меняем динамический тип px

px->f(); px->g(); // Y::f X::g


Заметим, что в языке Turbo Pascal, и у его наследника Delphi тоже есть понятие виртуальных методов. Но используется несколько иная терминология. В Turbo Pascal виртуальные методы назывались виртуальными, а невиртуальные – назывались статическими. В С++ статические методы – это совсем другое. В Turbo Pascal статических методов в смысле С++ нет. Но есть не только синтаксические различия.


type MyObj = class

procedure F; virtual;

procedure G;

end;


Поведение виртуальных членов в Delphi полностью аналогично, но есть некоторые различия в подходе. У Страуструпа более жесткий подход, который заключается в следующем. Виртуальные функции определяются как виртуальные на верхнем уровне иерархии. Если функция стала виртуальной, то она останется виртуальной на всех уровнях наследования. Программист не может такую функцию переопределить как не виртуальную. Если функция не является виртуальной где-то на верхнем уровне иерархии, то сделать эту функцию виртуальной уже нельзя.

В языке Delphi ситуация другая. Если мы хотим переопределить виртуальную функцию, то мы должны использовать специальное ключевое слово:


type MyObj2 = class (MyObj)

procedure F; override; // убираем виртуальность

procedure G; virtual; // переопределяем как виртуальную

end;


Хотя такие трюки проделывать можно, но не нужно, и всякий раз, когда происходит такое переопределение, Delphi выдает предупреждение. Рассмотрим пример.


P : PMyObj; //Пусть PMyObj указатель на MyObj

P1 : PMyObj2;

P2 : PMyObj3; // Пусть MyObj3 выведен из MyObj2

// и в нем переопределена G с помощью слова override

P:=P2;

P1:=P2;


P^.G; // вызов G из MyObj

P^.F; // вызов F из MyObj2 (или MyObj3 если в нем F переопределена как виртуальная)


P1^.G; // вызов G из MyObj2 (или MyObj3 если в нем G переопределена как виртуальная)

P1^.F; // вызов F из MyObj3 если в нем F переопределена как виртуальная


Такие трюки дают несколько странные результаты, и поэтому большинство языков программирования запрещают такого рода финты.


Лекция 24