Полиморфизм, виртуальные методы

Создадим еще один дочерний объектный тип - потомок объекта 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;

Объявление виртуального метода в каком-либо родительском объектном типе накладывает следующие ограничения на все его дочерние типы:

  1. все методы дочерних типов, одноименные с виртуальными родительскими, обязаны быть также виртуальными (нельзя переопределить виртуальный метод статическим);
  2. заголовки всех реализаций одного и того же виртуального метода должны быть полностью идентичными, включая количество формальных параметров и их типы;
  3. каждый объектный тип, имеющий виртуальные методы, обязан иметь конструктор.

Обратите внимание на то, что, кроме добавления слова virtual, изменилось и еще кое-что: при объявлении метода Init появилось незнакомое слово constructor, а метод Done превратился в destructor.

Дело в том, что таблица виртуальных методов изначально не содержит конкретных адресов точек входа. Перед использованием любого из виртуальных методов ее надо заполнить. Делает это специальный метод-конструктор. Метод-конструктор - это разновидность метода-процедуры. Синтаксически он отличается только использованием служебного слова constructor вместо procedure. Однако это приводит к тому, что при компиляции к этому методу добавляется так называемый пролог, код которого как раз и "расставляет" в ТВМ правильные адреса виртуальных методов.

Конструкторов в объекте может быть сколько угодно, один из конструкторов обязательно должен быть вызван перед вызовом первого виртуального метода (иначе программа попросту "зависнет"), и конструктор сам не может быть виртуальным.

Что касается другого "хитрого" метода - деструктора, то он не имеет никакого отношения к виртуализации методов, и зачем он нужен, мы расскажем позже. Отметим только, что деструктор в отличие от конструктора вполне может быть виртуальным

Наши рекомендации