Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ВВЕДЕНИЕ В ОБЪЕКТНО Ориентированное программиро...docx
Скачиваний:
19
Добавлен:
29.08.2019
Размер:
1.01 Mб
Скачать

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

Рассмотрим следующий пример: имеется иерархия графических объектов, типа кнопка, окно ввода и т.д. Все эти классы являются потомки класса - отображаемый прямоугольник. Они используют наследуемые от него методы типа определить координаты занимаемой области, проверить принадлежит ли точка занимаемой им области и т.д. Они должны определить свои поля методы, специфичные для каждого из них. Например для окна редактирования - функции по обработке текста. Но есть ряд методов, которые являются общими по назначению, но отличаются по своей реализации. Например, все они должны уметь отображать себя на экране. Метод отображения будет иметь одинаковый интерфейс, но разную реализацию для разных классов. Следовательно, метод рисования нужно перекрывать.

Если этот метод будет статическим, то перекрытие возможно. Но этот подход будет иметь один серьезный недостаток: Правила контроля типов в Object Pascal гласят, что объекту может быть присвоен адрес экземпляра любого из дочерних типов. Т.е. можно организовать список указателей на объект-предок, в котором реально будут храниться объекты-потомки или определить формальный параметр процедуры типа объект-предок, а передавать объект-потомок. При этом через указатель класса предка можно вызывать только методы, определенные в предке, а не в потомке. Например в предыдущем примере опросить все графические объекты, не им ли принадлежит точка экрана, по которой щелкнули мышью. Те методы и поля, которые уникальны для каждого класса, вызывать через класс предок нельзя, да и не имеет смысла. Но есть группа методов, которые имеют одинаковое назначение, одинаковый интерфейс, но разную реализацию (прорисовка объекта в прошлом примере). При статическом перекрытии через имя объекта типа предок будет вызвана реализация принадлежащая предку. А хотелось бы, чтобы была вызвана реализация соответствующая фактическому типу. Для этого используют виртуальные и динамические методы.

Чтобы сделать метод виртуальным или динамическим, следует включить директиву virtual или dynamic в его описание. При описании перекрывающего метода в производном классе следует использовать директиву override. Причем, перекрывающий метод должен возвращать значение того же типа и иметь параметры того же типа, что и перекрываемый (в отличие от статического перекрытия). Пример:

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

Рассмотрим следующий пример. Пусть у нас имеется некое обобщенное поле для хранения данных -класс TField и три его потомка - для хранения строк, целых и вещественных чисел:

Какой метод GetData будет вызван в процедуре ShowDate? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом. Полиморфизм - принцип, в соответствии с которым при вызове метода объекта через указатель будет выполнен метод, соответствующий классу реального объекта, а не классу указателя.

Полиморфизм - возможно наиболее важный из принципов ООП. Наследование без него возможно, но не очень полезно. Допустим, вы имеете дело с некоторой совокупностью явлений или процессов. Чтобы смоделировать их средствами ООП, нужно выделить их самые общие черты. Те из них, которые не изменяют своего содержания, должны быть реализованы в виде статических методов. Те же, которые изменяются при переходе от общего к частному, лучше сделать виртуальными или динамическими. Основные, «родовые» методы нужно описать в классе-предке и затем перекрывать в классах-потомках. Структура классов в Delphi - наглядный пример наследования и полиморфизма. Рассмотрим далее следующую ситуацию: если в классе предке объявлен виртуальный метод, а в классе потомке - метод с соответствующими именем и типом, но при его объявлении пропущена директива override, новая декларация скроет метод предка, не перегружая его, т.е. будем иметь дело со статическим наследованием. Например:

Это удобно, если для части классов потомков необходимо статическое перекрытие методов, а для части виртуальное или динамическое. Но возможно программист опустил директиву override по ошибке. Поэтому компилятор выдаст предупреждение о скрытии ранее определенного виртуального метода. Чтобы убрать это предупреждение используется директива reintroduce:

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

TDat = class

destructor Destroy; virtual;

end;

TDatl = class(TDat)

Destructor Destroy; override;

end;

Var

a: TDat;

. . .

a := TDatl.Create;

a. Destroy; {будет вызван деструктор класса потомка, который возможно вызывает перекрытый метод класса предка}

При попытке откомпилировать такую программу компилятор выдаст предупреждение о том, что метод Destroy класса ТDat скрывает метод Destroy класса предка. Это вызвано тем, что в Delphi все классы являются потомками класса ТObject, у которого определен деструктор Destroy, объявленный виртуальным. И во всех производных классах мы должны описывать перекрывающие его деструкторы, т.е. объявлять их override:

TDat = class

. . .

destructor Destroy; override;

end;

TDatl = class(TDat)

Destructor Destroy; override;

end;

Var

a: TDat;

. . .

a := TDatl.Create;

a.Destroy; {будет вызван деструктор класса потомка, который возможно вызывает перекрытый метод класса предка}

Дополним один из старых примеров виртуальными деструкторами:

type

TFigure = class

procedure Draw; virtual;

destructor Destroy; override;

end;

TRectangle = class(TFigure)

procedure Draw; override;

destructor Destroy; override;

end;

TEllipse = class(TFigure)

procedure Draw; override;

destructor Destroy; override;

end;

var

Figure: TFigure;

begin

Figure := TRectangle.Create;

Figure.Draw; // ,будет вызван TRectangle.Draw

Figure . Destroy; // будет вызван TRectangle . Destroy

Figure := TEllipse.Create;

Figure. Draw; // будет вызван TEllipse. Draw

Figure.Destroy; //будет вызван TEllipse.Destroy

end;

При вызове конструктора мы указываем не имя объекта, а имя класса то есть именно при создании объекта и выясняется, какого он будет типа и конструктор класса не может быть виртуальным. Даже если при его описании было использовано служебное слово virtual, он все равно будет перекрываться как статический. При работе с метаклассами (указателями на класс) использование виртуальных конструкторов возможно. Рассмотрим далее полный пример, который показывает возможность практического использования полиморфизма. Пример представляет,собой иерархию классов геометрических фигур (TFigure и потомки), которые собираются в произвольную схему . (TScheme). Схема способна отображать на экране свои фигуры и обрабатывать событие -щелчок мышью - изменяя, например, цвет фигуры по которой был выполнен щелчок.

type

TFigure = class(TObject)

C:TColor;

Function GetColor:TColor; {метод возвращает текущий цвет – общий у всех фигур – не перекрывается }

procedure Draw; virtual; {метод отрисовки фигуры – свой у каждого типа фигур, перекрывается как виртуальный }

function MouseClick(x,y:integer) :boolean; virtual; {метод обработки щелчка мыши с координатами x, y, возвращает True, если координаты мыши находятся внутри этой фигуры, перекрывается как виртуальный }

end;

TCircle = class(TFigure)

Fx,Fy,FR:integer; {круг задается координатами центра и радиусом}

constructor Create(x,y,R:integer); {конструктор инициализации}

destructor destroy; override; {виртуальный деструктор – перерывает деструктор TObject}

procedure Draw; override;

function MouseClick(x,y:integer):boolean; override;

end;

TEllipse = class(TFigure)

Fx, Fy, FA, FB: integer; {эллипс задается координатами центра и полуосями} constructor Create (x,y, A,B: integer); {конструктор инициализации}

destructor destroy; override; {виртуальный деструктор}

procedure Draw; override;

function MouseClick(x,y:integer):boolean; override;

end;

TSquare = class(TFigure)

Fx,Fy,Fl:integer; {квадрат задается координатами угла и длинной стороны}

constructor Create(x,y,1rinteger); {конструктор инициализации}

destructor destroy; override; {виртуальный деструктор}

procedure Draw; override;

function MouseClick(x,y:integer):boolean; override;

end;

TRectangle = class(TFigure)

Fxl, Fyl, Fxr, Fyr:integer; {прямоугольник задается координатами двух углов} constructor Create(xl,yl,xr,yr:integer); {конструктор инициализации}

destructor destroy; override; {виртуальный деструктор}

procedure Draw; override;

function MouseClick(x,y:integer):boolean; override;

end;

TScheme = class(TObject)

n:integer; {число фигур схемы}

Fig:array[1.. 100] of TFigure; {массив указателей на фигуры схемы – соответствие с правилом совместимости типов может хранить указатели на объект любого класса, производного от TFigure}

procedure AddFig(f:TFigure); {добавить фигуру}

procedure ScMouseClick(x,y: integer); {обработать щелчок мыши }

procedure ScDraw; {нарисовать схему}

constructor Create;

destructor Destroy; override;

end;

constructor TScheme.Create;

begin

inherited Create;

n:=0; {конструктор можно было не определять}

end;

destructor TScheme.Destroy;

var

i:integer;

begin

for i:=l to n do Fig[i] .Destroy;

inherited Destroy;

{В цикле уничтожаются все фигуры схемы – будут вызываться деструкторы, соответствующие реальным типам объектов}

end;

procedure TScheme.AddFig(f: TFigure);

{В схему добавляется фигура, в качестве параметра процедуры реально может предаваться объект любого класса, производного от TFigure}

begin

n:=n+l;

Fig[n] :=f;

end;

procedure TScheme.ScDraw;

var

i:integer;

begin

for i:=l to n do

Fig[i].Draw;

{Метод Draw для иерархии классов фигур объявлен виртуальным, т.е. будет вызываться метод соответствующей реальному классу объекта и изображаться соответственно будет не абстрактная фигура, а конкретно круг, эллипс, прямоугольник или квадрат в зависимости от того какие объекты были добавлены в схему с помощью метола AddFig}

end;

Рассмотрим использование этих классов: