Объекты. Занятие 2_7. Часть 6.
Задача.
Обращаться к одноимённым методам классов-потомков через объект класса предка.
Создание и вызов метода объекта.
Рассмотрим следующий вопрос.
Имеется класс Obj1 с полем ss и методом draw:
Obj1=class
ss:string;
constructor create(s:string);
procedure draw;
end;
Реализация метода draw простая:
procedure Obj1.draw;
begin
showMessage(‘procedure Obj1.draw ss=’+ss);
end;
Так как изнутри метода видны все поля класса, то в окно сообщений выводится значение поля ss.
Инициализация объекта производится в конструкторе:
constructor Obj1.create(s:string);
begin
ss:=s;
end;
Конструктор вызывается при создании объекта и устанавливает значение поля ss.
Вызов конструктора выглядит следующим образом:
procedure TForm1.Button1Click(Sender: TObject);
var vOb:Obj1;
begin
vOb:=Obj1.Create(‘111111’);//вызывается конструктор Create
vOb.draw;
end;
После создания объекта второй строкой вызывается метод draw этого объекта.
Результат работы программы:
Понятие «виртуальных методов».
Но часто (поверьте на слово, а немного далее будет приведён конкретный пример) возникает ситуация, когда требуется вызывать метод с тем же именем и набором формальных параметров, но с несколько изменённым кодом реализации.
В то же время остаётся необходимость и в использовании старого метода при определённых условиях.
Если использовать простое перекрытие метода, создав класс-наследник, то доступ к старому методу теряется (как мы видели это ранее).
Решить проблему позволяет виртуальный механизм вызова методов. Он состоит в следующем.
В базовом классе-предке метод объявляется с ключевым словом-директивой virtuale
Obj1=class
ss:string;
constructor create(s:string);
procedure draw;virtuale;
procedure draw;virtuale;
end;
Таблица виртуальных методов.
При компиляции программы, встретив диррективу virtuale, компилятор создаёт так называемую «виртуальную» таблицу (таблица VMT — virtual table methods или таблица виртуальных методов)..
В неё записывается адрес входа в метод, помеченного директивой virtuale, и класс, к которому относится метод.
Далее необходимо создать класс-наследник, в котором метод будет переопределён. Сам метод необходимо пометить ключевым словом-директивой override:
Obj2=class(Obj1)
ss,gg:string;
constructor create(s,s1:string);
procedure draw;override;
end;
В новый класс-наследник добавим поле gg и изменим конструктор.
При компиляции компилятор, встретив директиву override, пойдёт вверх по цепочке классов-предков, пока не встретит директиву virtuale. Тогда он добавит в таблицу имя нового класса и адрес входа в новую процедуру.
Теперь, на этапе прогона программы, можно обратиться как к старому методу, так и к новому.
Вызов виртуальных методов.
Конструктор нового класса запишем в виде:
constructor Obj2.create(s,s1:string);
begin
gg:=s1;
ss:=s;
showMessage(‘constructor_2 ss=’+ss+’ gg=’+gg);
end;
Реализация перекрывающего метода в классе Obj2 пусть будет следующая:
procedure Obj2.draw;
begin
showMessage(‘ procedure Obj2 ss=’+ss+’ gg=’+gg);
end;
В конструкторе заполняются поля класса gg и ss и выводится сообщение:
Но как же, обращаясь к Obj1, организовать вызов первого или второго метода?
При компиляции, когда встречается вызов метода, объявленного ранее как виртуальный, компилятор смотрит, от какого класса идёт вызов.
После этого вместо прямого вызова метода, как это делается обычно, на его место вставляется ссылка на виртуальную таблицу — виртуальный вызов.
При прогоне программы, дойдя до виртуального вызова, программа переходит к виртуальной таблице. В ней она ищет соответствие указанному в вызове классу и имени метода. Найдя нужную строку, осуществляется переход к методу по адресу, записанному в строке.
Перекрытие метода. Пример.
Посмотрим, как это выглядит практически.
У нас есть два метода. Один принадлежит базовому классу-предку. Это метод:
procedure Obj1.draw;
Второй метод, который перекрывает базовый и принадлежит классу Obj2— это метод:
procedure Obj2.draw;
Так как же вызвать первый или второй метод, обращаясь только к базовому классу?
Вызов метода базового класса производится обычным образом:
procedure TForm1.Button1Click(Sender: TObject);
var vOb:Obj1;
begin
vOb:=Obj1.Create(‘111111’);
vOb.draw;
end;
Вызов перекрывающего метода производится, в общем-то, аналогично с одним небольшим нюансом:
procedure TForm1.Button2Click(Sender: TObject);
var vOb:Obj1;
begin
vOb:=Obj2.Create(‘222222′,’333333’);
vOb.draw;
end;
В переменную типа Obj1 мы помещаем ссылку на объект-потомок класса Obj2!
Это возможно, так как структура итогового объекта напоминает пирамидку- внизу код базового класса, а далее сверху наслаиваются дополнения от дочерних классов.
Обратившись к объекту-предку мы через его виртуальную таблицу получаем доступ к одноименному методу класса-потомка.
В результате прогона программы получим:
и результат вызова метода draw
Таким образом, при применении приёма виртуализации мы через переменную базового класса-предка можем обращаться к разным методам, имеющим один и тот же заголовок, но принадлежащим разным классам.
Рассмотрим ещё раз сказанное на блок-схеме.
- Компилятор, встретив в описании класса Obj1 метод с директивой virtual, создаст таблицу виртуальных методов.
- Компилятор находит метод Obj1.draw и записывает в VMT адрес входа (точку входа) в это метод.
- Встретив класс-потомок Obj2 и найдя в нём перекрывающую процедуру, помеченную директивой override, компилятор ищет метод Obj2.draw и записывает в VMT адрес входа (точку входа) в это метод.
- Найдя строку vObj.draw первый раз, компилятор определяет, что метод с именем draw вызывается из объекта класса Obj1 (создан через Obj1.create), в котором метод помечен как виртуальный. Поэтому прямой вызов метода заменяется ссылкой на VMT.
- Найдя строку vObj.draw следующий раз, компилятор определяет, что метод с именем draw вызывается из объекта класса Obj2(создан через Obj2.create), в котором метод помечен как перегружаемый. Поэтому прямой вызов метода заменяется ссылкой на VMT.
- При прогоне программа, встретив вызов vObj.draw, обращается к VMT. Если класс объекта Obj1, то выбирается ссылка на метод из первой строки; если Obj2, то из второй строки.