Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
23
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать
  1. Полиморфизм, виртуальные и динамические методы

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

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

Interface type TPerson = class(TObject) //организуем свойства фамилия и номер паспорта private ... public property Name: string read FName write SetName; property PassportNumber: string read FPassportNumber write SetPassportNumber; ... end;

TStudent = class(TPerson) begin //организуем свойства номер зачетки, группа, специальность //свойства фамилия и номер паспорта унаследованы private ... public property TestBookNumber: string read FTestBookNumber write SetTestBookNumber; property Group: string read FGroup write SetGroup; property Subject: string read FSubject write SetSubject; ... end;

TWorker = class(TPerson) //организуем свойство табельный номер //свойства фамилия и номер паспорта унаследованы private ... public property TableNumber: string read FTableNumber write SetTableNumber; ... end;

TEmployee = class(TWorker) //организуем свойство должность //свойства фамилия, номер паспорта и табельный номер //унаследованы private ... public property Appointment: string read FAppointment write SetAppointment; ... end;

TTeacher = class(TWorker) //организуем свойства кафедра и преподаваемые дисциплины //свойства фамилия, номер паспорта и табельный номер //унаследованы private ... public property Chair: string read Fchair write SetChair; property Subjects: TStrings read FSubjects write SetSubjects; ... end;

Массив сведений о людях, то есть массив указателей на объекты класса TPerson, может быть объявлен следующим образом:

Var Persons: array[1..MaxPersonNum] of TPerson;

При создании объектов проверяется какое-либо условие и в зависимости от него создается объект того или иного класса:

... for i := 1 to MaxPersonNum do begin case SomeCondition of StudentValue : Persons[i] := TStudent.Create; EmployeeValue : Persons[i] := TEmployee.Create; TeacherValue : Persons[i] := TTeacher.Create; end; end;

В результате получится структура, подобная показанной на рис. 10.

Непосредственно через указатели Persons[i] можно получить доступ только к тем элементам класса, которые были объявлены в классе TPerson, например свойству Name:

for i := 1 to MaxPersonNum do begin if Persons[i].Name = SomeName then ... end;

К элементам, объявленным в потомках таким образом обратиться нельзя и следующая запись вызовет сообщение об ошибке:

if Persons[i].Chair = SomeChair then ...

даже в том случае, если реальным типом объекта Persons[i] будет TTeacher, для которого определено свойство Chair. Доступ к элементам класса-потомка через указатель типа класс-предок возможен с помощью приведения объектных типов. Приведение объектных типов рассматривается позднее.

Итак, правило совместимости объектных типов дает возможность работать с объектами производных классов через указатели на базовые классы.

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

Но есть ряд методов, которые являются общими по назначению, но отличаются по своей реализации. Например, все они должны уметь отображать себя на экране. Метод отображения будет иметь одинаковый интерфейс, но разную реализацию для разных классов (рис. 11).

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

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

Пример:

interface Type TRectangle = class procedure Draw; virtual; ... end;

TPicture = class(TRectangle) procedure Draw; override; ... end;

TText = class(TRectangle) procedure Draw; override; ... end;

При вызове виртуальных методов будет вызван тот метод, который соответствует реальному типу объекта.

Procedure Test; Var Figure: TRectangle; begin Figure := TPicture.Create; Figure.Draw; //будет вызван TPicture.Draw Figure.Destroy;

Figure := TText.Create; Figure.Draw; //будет вызван TText.Draw Figure.Destroy; end;

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

Итак, в иерархии наследования есть методы, одинаковые как по назначению, так и по реализации для всех потомков – такие методы определяются в предке как статические. Есть методы полностью уникальные для конкретных классов-потомков – они определяются в этих потомках. Но имеется также ряд методов, которые у классов-предков и классов-потомков совпадают по назначению и интерфейсу и различаются по реализации, такие методы объявляют как виртуальные или динамические. И через указатель на класс-предок в случае виртуальных и динамических методов будет вызван метод, соответствующий реальному классу объекта, а не классу указателя. Этот принцип называют полиморфизмом.

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

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

Перекрытые виртуальные и динамические методы предка можно вызывать из методов потомка, также как и статические – используя директиву inherited.

Interface type TOldObject = class(TObject) private SomeAngle: real; public function GetAngle: real; virtual; procedure SetAngle(value: real); virtual; ... end;

TNewObject = class(TOldObject) public function GetAngle: real; override; procedure SetAngle(value: real); override; ... end;

Implementation function TOldObject.GetAngle: real; begin result := SomeAngle; end;

procedure TOldObject.SetAngle(value: real); begin SomeAngle := value; end;

function TNewObject.GetAngle: real; begin //вызываем метод предка result := inherited GetAngle*180/pi; //результат, возвращаемый методом предка преобразуем //из радиан в градусы end;

procedure TNewObject.SetAngle(value: real); begin //вызываем метод предка и передаем ему //значение, преобразованное в радианы inherited SetAngle(value/180*pi); end;

/* //альтернативная реализация procedure TNewObject.SetAngle(value: real); begin //преобразуем значение в радианы value := value/180*pi; //вызываем метод предка, имя метода не указываем, //поскольку оно совпадает с именем самого метода inherited; end; */

procedure Test; var o: TOldObject;//указатель типа предок x: real; begin o := TNewObject.Create; //реально создаем объект производного класса

//В соотвествии с принципом полиморфизма будут вызваны //методы производного класса TnewObject: o.SetAngle(90); x := o.GetAngle; o.Free; end;

При вызове виртуальных или динамических методов из методов, объявленных и реализованных в классе-предке также будет вызываться тот виртуальный метод, который соответствует реальному типу объекта:

Interface type TOldObject = class(TObject) private SomeAngle: real; public function GetAngle: real; virtual; procedure SetAngle(value: real); virtual; procedure SetData(value1: real; value2: integer); ... end;

TNewObject = class(TOldObject) public function GetAngle: real; override; procedure SetAngle(value: real); override; ... end;

Implementation procedure ToldObject.SetData(value1: real; value2: integer); begin SetAngle(value1); //Будет вызван метод SetAngle соответствующий типу объекта ... end;

procedure Test; var o: TOldObject; x: real; begin o := TNewObject.Create; o.SetData(90,0); //из метода SetData класса TOldObject будет вызван //метод SetAngle класса TNewObject x := o.GetAngle; o.Free; end;

В этом примере указатель имеет тип класс-предок TOldObject. Реальный тип объекта – класс-потомок TNewObject. Вызванный метод SetData статический и объявлен в классе-предке. Из него в свою очередь вызывается виртуальный метод SetAngle. Будет вызвана та его реализация, которая соответствует реальному типу объекта. В данном случае – TNewObject.SetAngle.

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

Interface type TPerson = class(TObject) //организуем свойства фамилия и номер паспорта private ... public property Name: string read FName write SetName; property PassportNumber: string read FPassportNumber write SetPassportNumber; //функция, возвращающая все данные в виде одной строки function GetStringData: string; virtual; ... end;

TStudent = class(TPerson) //организуем свойства номер зачетки, группа, специальность //свойства фамилия и номер паспорта унаследованы private ... public property TestBookNumber: string read FTestBookNumber write SetTestBookNumber; property Group: string read FGroup write SetGroup; property Subject: string read FSubject write SetSubject; //функция, возвращающая все данные в виде одной строки function GetStringData: string; override; ... end;

TWorker = class(TPerson) //организуем свойство табельный номер //свойства фамилия и номер паспорта унаследованы private ... public property TableNumber: string read FTableNumber write SetTableNumber; //функция, возвращающая все данные в виде одной строки function GetStringData: string; override; ... end;

TEmployee = class(TWorker) //организуем свойство должность //свойства фамилия, номер паспорта и табельный номер //унаследованы private ... public property Appointment: string read FAppointment write SetAppointment; //функция, возвращающая все данные в виде одной строки function GetStringData: string; override; ... end;

TTeacher = class(TWorker) //организуем свойства кафедра и преподаваемые дисциплины //свойства фамилия, номер паспорта и табельный номер //унаследованы private ... public property Chair: string read Fchair write SetChair; property Subjects: TStrings read FSubjects write SetSubjects; //функция, возвращающая все данные в виде одной строки function GetStringData: string; override; ... end;

Var Persons: array[1..MaxPersonNum] of TPerson;

implementation

//функция, возвращающая все данные в виде одной строки function TPerson.GetStringData: string; begin result := Name + ‘ ‘ + PassportNumber; end;

function TStudent.GetStringData: string; begin result := inherited GetStringData + ‘ ‘ + TestBookNumber + Group + ‘ ‘ + Subject; //inherited GetStringData – вызовется метод предка, //который вернет фамилию + номер паспорта end;

function TWorker.GetStringData: string; begin result := inherited GetStringData + ‘ ‘ + TableNumber; //inherited GetStringData – вызовется метод предка, // который вернет возвращены фамилию + номер паспорта end;

function TEmployee.GetStringData: string; begin result := inherited GetStringData + ‘ ‘ + Appointment; //inherited GetStringData – вызовется метод предка, //который вернет фамилию + номер паспорта + табельный номер end;

function TTeacher.GetStringData: string; begin result := inherited GetStringData + ‘ ‘ + Chair; //inherited GetStringData – вызовется метод предка, // который вернет фамилию + номер паспорта + табельный номер end;

Вызов метода GetStringData будет выглядеть, например, следующим образом:

for i := 1 to MaxPersonNum do begin //вызывается метод соответствующий реальному типу объекта OutData(Persons[i].GetStringData); end;

В этом примере для разных элементов массива TPerson будет вызываться метод GetStringData, определенный в классе, соответствующем реальному типу объекта, то есть тому классу, имя которого было указано перед именем конструктора при создании объекта. И для разных объектов будут возвращены разные по своей структуре данные.