13.5.2. Виртуальные методы

13.5.2.1. Виртуализация методов. Из правил совместимости фактических и формальных параметров типа «объект» следует, что в качестве фактического параметра может выступать объект любого производного типа от типа формального параметра. Таким образом, во время компиляции процедуры неизвестно, объект какого типа будет ей передан в качестве фактического параметра (такой параметр называется полиморфным объектом). В полной мере полиморфизм объектов и методов реализуется при помощи виртуальных методов.

Метод становится виртуальным, когда за его определением в типе объекта ставится служебное слово VIRTUAL:

PROCEDURE ИмяМетода( параметры ); VIRTUAL;

или

FUNCTION ИмяМетода( параметры ) : ТипЗначения; VIRTUAL;

При виртуализации методов должны выполняться следующие условия:

1. Если прародительский тип объекта описывает метод как виртуальный, то все его производные типы, которые реализуют метод с тем же именем, должны описать этот метод тоже как виртуальный. Другими словами, нельзя заменять виртуальный метод статическим. Если же это произойдет, компилятор сообщит об ошибке номер 149 VIRTUAL expected («Ожидается служебное слою VIRTUAL»).

2. Если переопределяется реализация виртуального метода, то заголовок заново определяемого виртуального метода в производном типе не может быть изменен. Иначе говоря, должны остаться неизменными порядок расположения, количество и типы формальных параметров в одноименных виртуальных методах. Если этот метод реализуется функцией, то не должен изменяться и тип результата. При изменении заголовка метода компилятор выдаст сообщение об ошибке номер 131 Header does not match previous definition («Заголовок не соответствует предыдущему определению»).

- 283 -

3. В описании объекта должен обязательно описываться специальный метод, инициализирующий объект (обычно ему дают название Init). В этом методе служебное слово PROCEDURE в объявлении и реализации должно быть заменено на слово CONSTRUCTOR. Это служебное слово обозначает особый вид процедуры — конструктор, который выполняет установочную работу для механизма виртуальных методов. Все типы объектов, которые имеют хотя бы один виртуальный метод, должны иметь конструктор. Конструктор всегда вызывается до первого вызова виртуального метода. Вызов виртуального метода без предварительного вызова конструктора может привести систему к тупиковому состоянию, а компилятор не проверяет порядок вызовов методов. Помните об этом!

13.5.2.2. Конструкторы и таблица виртуальных методов. Каждый экземпляр (переменная) типа «объект», содержащий виртуальные методы, должен инициализироваться отдельным вызовом конструктора. Если переменная А инициализирована вызовом конструктора, а переменная В того же типа не инициализирована, то присваивание В:=А не инициализирует переменную В и при вызове ее виртуальных методов программа может «зависнуть».

Чтобы понять, что делает конструктор, разберемся в механизме реализации виртуальных методов. Каждый объектный тип (именно тип, а не экземпляр) имеет «таблицу виртуальных методов» (VMT), которая содержит размер типа объекта и адреса кодов процедур или функций, реализующих каждый из его виртуальных методов. При вызове виртуального метода каким-либо экземпляром местонахождение кода реализации этого метода определяется по таблице VMT для типа этого экземпляра. Конструктор же устанавливает связь между экземпляром, который вызывает конструктор, и таблицей виртуальных методов данного объектного типа. Если же конструктор не будет вызван до обращения к виртуальному методу, то перед компьютером станет вопрос, где же искать этот метод. Это и приведет к тупиковой ситуации.

Важно помнить, что таблица виртуальных методов — одна для каждого типа, а не у каждой переменной типа «объект». Переменная лишь держит связь с таблицей своего типа, и эта связь устанавливается конструктором. В объекте может быть определено несколько конструкторов. Сами конструкторы могут быть только статическими, хотя внутри конструктора могут вызываться и виртуальные методы.

При передаче в процедуру или функцию полиморфного объекта, имеющего виртуальные методы, адреса этих методов передаются

- 284 -

через соответствующую объекту таблицу VMT. Это гарантирует, что сработают именно те методы, которые подразумевались при объявлении типа объекта. Кроме того, если объект Z наследует от объекта Y виртуальный метод, вызывающий другие методы, то последние вызовы будут относиться к методам объекта Z, а не Y. В случае статических методов все было бы наоборот (вызовы не «вернулись» бы в Z).

Мы начали этот раздел с примера полиморфной процедуры (см. рис. 13.6). Чтобы она заработала, надо сделать некоторые методы виртуальными и объявить конструкторы в объектах. Это проделано на рис. 13.7.

| USES CRT; { в примере используется системный модуль CRT }

| TYPE

| ObjPos = OBJECT

| Line : Word; { номер строки }

| Col : Word; { номер столбца }

| { ! } CONSTRUCTOR Init(init_line,init_col: Word);

| { ! } PROCEDURE Print; VIRTUAL { зарезервировано }

| END;

| CONSTRUCTOR ObjPos.Init( init_line, init_col : Word );

| BEGIN

| Line := init_line; { метод задания номера строки }

| Col := init_col; { метод задания номера столбца }

| END;

| PROCEDURE ObjPos.Print; { пустая процедура вывода }

| BEGIN

| Write( #7 ); { это вызовет звуковой сигнал }

| END;

| TYPE

| ObjSym = OBJECT( ObjPos ) { объявление наследования }

| Sym : Char; { поле-значение символа }

| { ! }CONSTRUCTOR Init(init_line,init_col : Word;

| init_sym : Char);

| {!} PROCEDURE Print; VIRTUAL {метод вывода Sym }

| END;

| CONSTRUCTOR ObjSym.Init;

| BEGIN

| ObjPos.Init( init_line, init_col ); { задание позиции }

| Sym := init_sym { задание значения символа }

| END;

Рис. 13.7

- 285 -

| PROCEDURE ObjSym.Print;

| BEGIN CRT.GotoXY( Col, Line );

| { процедура из модуля CRT }

| Write( Sym )

| { вывод символа в позиции }

| END;

| TYPE

| ObjString=OBJECT( ObjPos )

| SubSt : String; { поле-значение подстроки }

| { ! }

| CONSTRUCTOR Init(init_line,init_col: Word;

| init_ss : String);

| { ! }

| PROCEDURE Print; VIRTUAL

| {метод вывода SubSt }

| END; CONSTRUCTOR ObjString.Init;

| {инициализация полей объекта }

| BEGIN

| ObjPos.Init( init_line, init_col );

| {задание позиции }

| SubSt := init_ss { задание значения подстроки }

| END;

| PROCEDURE ObjString.Print;

| BEGIN CRT.GotoXY( Col, Line };

| {процедура из библиотеки CRT }

| Write( SubSt ) {печать подстроки в позиции }

| END;

| {Вывод полиморфного объекта (строки или символа) }

| PROCEDURE PrintObj( VAR Obj : ObjPos );

| BEGIN Obj.Print END;

| { =========== ТЕЛО ОСНОВНОЙ ПРОГРАММ ================ }

| VAR ObjSymVar : ObjSym;

| { экземпляр типа ObjSym }

| ObjStringVar : ObjString; { экземпляр типа ObjString }

| BEGIN { Инициализация и вывод: }

| ClrScr; { очистка экрана }

| ObjSymVar.Init( 10, 10, '*' );

| ObjStringVar.Init( 20, 20, '...ПОДСТРОКА...' );

| PrintObj( ObjStringVar ); { вывод строки }

| PrintObj( ObjSymVar ); { вывод символа }

| END.

Рис. 13.7 (окончание)

- 286 -

Весьма важным является наличие слова VAR перед формальным параметром в процедуре PrintObj. В этом случае мы передаем сам объект. Если бы в процедуре PrintObj формальный параметр был описан как параметр-значение (без слова VAR), то процедура работала бы с копией объекта, приведенной к типу формального параметра. В примере на рис. 13.7 это выразилось бы в том, что несмотря на виртуальность методов, вызывался бы метод ObjPos.Print из типа формального параметра.