Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






Полиморфизм






Полиморфизм является одним из трёх принципов ООП. Прежде чем дать точное определение полиморфизма и рассмотреть синтаксис его реализации в Object Pascal, изучим ситуации, приводящие к этому понятию.

Опишем простую иерархию классов для графических объектов – базовый класс TFigure и его наследники TSquare и TCircle. Наделим TFigure методом рисования Draw и методом стирания фигуры Hide. Естественно, классы TSquare и TCircle перекрывают эти методы (у класса TFigure методы будут пустыми):

type TFigure = class

// схематическое описание класса

procedure Draw;

procedure Hide;

end;

 

TSquare = class(TFigure)

procedure Draw;

procedure Hide;

end;

 

TCircle = class(TFigure)

procedure Draw;

procedure Hide;

end;

 

procedure TFigure.Draw;

begin

end;

 

procedure TFigure.Hide;

begin

end;

 

procedure TSquare.Draw;

begin

// здесь рисуем квадрат

end;

 

procedure TSquare.Hide;

begin

// здесь стираем квадрат

end;

 

procedure TCircle.Draw;

begin

// рисуем круг

end;

 

procedure TCircle.Hide;

begin

// стираем круг

end;

Рассмотрим следующую ситуацию. Пусть имеется подпрограмма (или метод некоего класса), в которой происходит рисование графического объекта. Этот объект передаётся подпрограмме в качестве параметра. Каким должен быть тип этого параметра? Следуя правилу присваивания объектов, заключаем, что типом параметра должен быть TFigure, что позволит передать в подпрограмму объекты любых его дочерних классов. Заголовок подпрограммы может выглядеть следующим образом:

procedure WorkWithObjects(X: TFigure);

begin

...

X.Draw

end;

Использовать подпрограмму WorkWithObjects можно так:

var A: TCircle;

B: TSquare;

...

A: = TCircle.Create;

B: = TSquare.Create;

WorkWithObjects(A);

WorkWithObjects(B);

Небольшое отступление: обратите внимание на использование конструкторов для создания объектов. Так как конструкторы в TCircle и TSquare не описывались, то в данном примере используется конструктор TObject.Create, доставшийся этим классам «по наследству». Однако этот конструктор создаёт именно объекты класса TCircle и TSquare. Если бы эти классы имели разный набор полей, для объектов резервировался бы разный объем динамической памяти. Напомним, что выделением памяти для объекта занимается некий дополнительный код, присутствующий в любом конструкторе неявно.

Вернёмся к нашему примеру. Хотя он синтаксически корректен и компилируется, работать он будет неверно. Несмотря на тип фактических параметров процедуры WorkWithObjects, при работе эта подпрограмма будет вызывать метод TFigure.Draw.

Смоделируем вторую ситуацию. Пусть имеется массив из объектов класса TFigure или его наследников. Инициализируем такой массив и попытаемся нарисовать все графические объекты в цикле:

var Figures: array[1..3] of TFigure;

...

// корректно по правилам присваивания для объектов

Figures[1]: = TCircle.Create;

Figures[2]: = TSquare.Create;

Figures[3]: = TCircle.Create;

 

// пытаемся нарисовать все объекты в цикле

for i: = 1 to 3 do

Figures[i].Draw;

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

Ещё одна ситуация. Добавим в класс TFigure метод для перемещения фигур. Перемещение фигуры – это стирание фигуры, изменение её координат и рисование фигуры на новом месте:

type TFigure = class

...

procedure Move;

end;

 

procedure TFigure.Move;

begin

Hide;

// здесь изменили координаты фигуры

Draw

end;

Логика работы метода Move сохраняется для дочерних классов TCircle и TSquare. Значит, переопределять этот метод в дочерних классах не требуется. Однако следующий вызов приведёт не к перемещению квадрата, а к вызову TFigure.Hide, изменению координат (квадрата) и вызову TFigure.Draw:

var Square: TSquare;

...

Square.Move; // увидим, что квадрат не переместился!

Подобные расхождения между желаемым поведением объектов и действительным возникают из-за того, что в наших примерах использовались статические методы[4]. Адрес статического метода вычисляется на этапе компиляции. Он определяется классом объекта и не зависит от того, с каким классом будет связан объект на этапе выполнения. Рассмотрим строку вызова X.Draw из процедуры WorkWithObjects. Во время компиляции программы компилятор пытается определить тип переменной X. Он может сделать это по объявлению переменной в заголовке процедуры. Тип X это TFigure, значит запись X.Draw означает вызов метода TFigure.Draw (если вспомнить о неявном параметре self, то более точно – TFigure.Draw(X)).

Нам необходимо, чтобы поведение объекта, вызов его методов, определялись непосредственно в период выполнения программы. Это означает, что адрес метода должен вычисляться непосредственно в период выполнения по тому типу, который объект имеет в данный момент. Подобным образом работают виртуальные методы. Они функционируют по следующей схеме. Каждый объект наряду со значениями своих полей хранит указатель на специальную таблицу виртуальных методов (virtual method table, VMT). Таблица виртуальных методов индивидуальна и единственна для каждого класса. В ней хранятся адреса всех виртуальных методов класса (как собственных, так и унаследованных). Связь между объектом и VMT класса осуществляется во время начальной инициализации объекта, то есть при вызове конструктора. Виртуальные методы идентифицируются по константе-смещению в VMT. Во время выполнения программы из экземпляра объекта извлекается указатель на VMT и, используя константу-смещение, вычисляется адрес необходимого метода.

Для объявления виртуального метода используется директива virtual. Исходный метод, объявленный как виртуальный в классе-предке, перекрывается в классе-потомке методом с тем же именем и параметрами и помечается директивой override. Если хотя бы одно из этих требований не соблюдено, связь между виртуальными методами в иерархии классов теряется. При реализации методов директивы virtual и override не указываются.

Отредактируем классы TFigure, TSquare и TCircle, сделав их методы виртуальным:

type TFigure = class

procedure Draw; virtual;

procedure Hide; virtual;

end;

 

TSquare = class(TFigure)

procedure Draw; override;

procedure Hide; override;

end;

 

TCircle = class(TFigure)

procedure Draw; override;

procedure Hide; override;

end;

Если внести подобные изменения в описания классов, то во всех перечисленных ситуациях работа будет происходить так, как нам требуется.

С учётом перечисленных примеров можно дать следующее определение полиморфизма. Полиморфизм – особый вид перекрытия методов при наследовании, при котором программный код, работавший с методами родительского класса, пригоден для работы с изменёнными методами дочернего класса.

Вернёмся к иерархии классов TFigure, TCircle, TSquare. В базовом классе TFigure реализация методов Draw и Hide абсолютно не важна, так как они всё равно будут перекрываться в классах-наследниках. Мы оформили реализацию этих методов в виде пустых процедур. Более элегантное решение состоит в объявлении таких методов как абстрактных, не нуждающихся в реализации. Абстрактные методы объявляются при помощи директивы abstract, указанной после директивы virtual:

type TFigure = class

procedure Draw; virtual; abstract;

procedure Hide; virtual; abstract;

end;

// теперь реализацию TFigure.Draw и TFigure.Hide писать не надо

Директива abstract применяется только для виртуальных методов в корневых классах иерархий. Попытка создать экземпляр класса, содержащего абстрактные методы, вызовет предупреждение, а обращение к абстрактному методу сгенерирует исключительную ситуацию.

Полиморфизм позволяет создавать в классах виртуальные свойства. Для этого методы чтения и записи свойства объявляются как виртуальные. Определив такие свойства в базовом классе, можно менять их поведение, перекрывая методы чтения и записи в дочерних классах. Виртуальные методы работы со свойствами обычно помещают в секцию protected.

Рассмотрим, как связан полиморфизм с вопросами создания и уничтожения объектов. Предположим, что имеется иерархия классов и планируется использовать набор объектов этих классов, разместив объекты в массиве или списке. Создание объектов в наборе операция достаточно специфическая, каждый объект создаётся вызовом персонального конструктора. А вот уничтожать созданные объекты можно было бы и сообща, в одном цикле. Для этого деструктор в иерархии классов должен быть полиморфным. Создатели Object Pascal посчитали данную ситуацию стандартной и объявили деструктор TObject.Destroy как виртуальный. Чтобы не обрывать цепочку виртуальных методов, рекомендуется деструкторы Destroy в пользовательских классах объявлять без параметров, с директивой override.






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.