Полиморфизм, виртуальные методы
Создадим еще один дочерний объектный тип - потомок объекта TPoint, который будет представлять собой окружность. X и Y соответственно превращаются в координаты центра, и добавляется радиус. Само собой разумеется, придется переопределить методы инициализации, прорисовки и скрытия.
Type TCircle = object(TPoint) R : Word; Procedure Init(InitX, InitY, InitR:Word; InitC:Byte); Procedure Show; Procedure Hide; Procedure Done; end; Procedure TCircle.Init(InitX, InitY, InitR : Word; InitColor : Byte); begin inherited Init(InitX, InitY, InitC); R := InitR end; Procedure TCircle.Show; begin Graph.SetColor(Clr); Graph.Circle(X, Y, R); Visib := True end; Procedure TCircle.Hide; begin Graph.SetColor(Graph.GetBkColor); Graph.Circle(X, Y, R); Visib := False end; Procedure TCircle.Done; begin inherited Done; R := 0 end;Таким образом, мы получили два объекта, методы которых Show и Hide делают одно и то же, но разными способами (полиморфизм).
Но вот непредвиденные последствия. Если мы создадим экземпляр этого объекта, проинициализируем его, а затем попытаемся переместить вызовом метода MoveTo, который был унаследован, то переместится точка, а не окружность. Связано это с тем, что при компиляции в машинный код вместо вызова подпрограммы транслятор подставляет адрес точки входа в эту подпрограмму. Это же справедливо и для методов, поэтому при трансляции метода MoveTo объекта TPoint будут подставлены адреса точек входа методов Show и Hide объекта TPoint. Объект же TCircle наследует метод MoveTo, но не переопределяет его. Поэтому при вызове метода MoveTo экземпляром объекта TCircle он, в свою очередь, вызовет методы Show и Hide объекта TPoint, то есть - переместит точку, а не окружность.
Избежать этого можно двумя способами. Во-первых, каждый раз переопределять метод MoveTo, чтобы транслятор всегда компилировал его заново. Однако это не совсем удобно, поскольку эти методы ничем не будут отличаться. Второй способ - объявить методы Show и Hide виртуальными.
При компиляции объекта, содержащего виртуальные методы, создается так называемая таблица виртуальных методов (ТВМ), содержащая адрес точки входа каждого из виртуальных методов, а в месте вызова такого метода ставится ссылка на ТВМ. При обращении к виртуальному методу компьютер сначала "смотрит", экземпляр какого именно объекта обратился к этому методу, затем "ищет" адрес точки входа виртуального метода именно этого объекта и запускает его. Все это происходит уже на этапе выполнения программы и поэтому называется поздним (динамическим) связыванием. Ранним связыванием называется процесс статического связывания методов с реализациями (экземплярами) объектов. Раннее связывание осуществляется на этапе компиляции для всех статических методов. Для объявления виртуального метода используется зарезервированное слово (директива) virtual.
Type
TPoint = object(TLocation)
Clr : Byte; {Цвет}
Visib : Boolean; {Видимость}
Constructor Init (InitX, InitY:Word; InitColor:Byte);
{Переопределяем метод инициализации - добавляем цвет}
Function GetColor : Byte; {Возвращает цвет}
Procedure Show; virtual;
Procedure Hide; virtual;
Procedure IsVisib : Boolean;
Procedure ChangeColor(NewColor : Byte); {Меняет цвет}
Procedure MoveTo(NewX, NewY:Word);
{Перемещает в новую позицию}
Destructor Done; virtual;
end;
TCircle = object(TPoint)
R : Word;
Constructor Init(InitX, InitY, InitR : Word; InitC : Byte);
Procedure Show; virtual;
Procedure Hide; virtual;
Destructor Done; virtual;
end;
Объявление виртуального метода в каком-либо родительском объектном типе накладывает следующие ограничения на все его дочерние типы:
- все методы дочерних типов, одноименные с виртуальными родительскими, обязаны быть также виртуальными (нельзя переопределить виртуальный метод статическим);
- заголовки всех реализаций одного и того же виртуального метода должны быть полностью идентичными, включая количество формальных параметров и их типы;
- каждый объектный тип, имеющий виртуальные методы, обязан иметь конструктор.
Обратите внимание на то, что, кроме добавления слова virtual, изменилось и еще кое-что: при объявлении метода Init появилось незнакомое слово constructor, а метод Done превратился в destructor.
Дело в том, что таблица виртуальных методов изначально не содержит конкретных адресов точек входа. Перед использованием любого из виртуальных методов ее надо заполнить. Делает это специальный метод-конструктор. Метод-конструктор - это разновидность метода-процедуры. Синтаксически он отличается только использованием служебного слова constructor вместо procedure. Однако это приводит к тому, что при компиляции к этому методу добавляется так называемый пролог, код которого как раз и "расставляет" в ТВМ правильные адреса виртуальных методов.
Конструкторов в объекте может быть сколько угодно, один из конструкторов обязательно должен быть вызван перед вызовом первого виртуального метода (иначе программа попросту "зависнет"), и конструктор сам не может быть виртуальным.
Что касается другого "хитрого" метода - деструктора, то он не имеет никакого отношения к виртуализации методов, и зачем он нужен, мы расскажем позже. Отметим только, что деструктор в отличие от конструктора вполне может быть виртуальным