При традиционном процедурном стиле программирования можно или ввести для круга совершенно новую структуру, или сконструировать структуру, используя в ней поля ранее определенного типа, то есть сделать «структуру в структуре». Хотя оба подхода приемлемы, но объектно-ориентированное программирование предлагает иной, более предпочтительный подход.
В Турбо Паскале при определении типа потомка после служебного слова object должно быть указано имя родительского типа, например:
Type
Circle = object (Point)
Radius: integer
end;
Это означает, что в объектном типе Circle присутствует не только поле Radius, но неявно и все поля из типа Point, включая и методы:
Var
OneCircle: Circle;
Begin
…
OneCircle. Create (100, 200);
OneCircle. Radius := 30;
…
Тип-потомок может, в свою очередь, выступать как предок по отношению к другому объектному типу, например, определение фигуры «кольцо», состоящей из двух концентрических кругов:
Type
Ring = object (Circle)
Radius2: integer
end;
Здесь так же наследуются поля и методы типа Point, который считается косвенным предком для Ring. Длина такой цепочки не ограничена.
В примере с объектным типом Circle он имеет в своем составе методы объекта-предка Point. Но если методы получения координат центра GetX и GetY здесь можно использовать без изменений, то для рисования круга методы SwitchOn и SwitchOff неприменимы. Наиболее простым решением было бы ввести в новый тип и новые методы с новыми именами. Но объектно-ориентированный подход с использованием свойства полиморфизма позволяет определить новые методы со старыми именами, переопределив тем самым методы типа-родителя:
Type
Circle = object (Point)
Radius: integer;
Procedure Create (a, b,R: integer);
Procedure SwitchOn;
Procedure SwitchOff;
Procedure Move (dx, dy: integer);
Function GetR: integer;
end;
…
Procedure Circle. Create (a, b,R: integer);
Begin
Point. Create (a, b);
Radius := R
end;
…
Procedure Circle. SwitchOn;
Begin
Visible := True;
Graph. Circle (X, Y,Radius)
end;
…
Function Circle. GetR: integer;
Begin
GetR := Radius
end;
…
Так как стандартная процедура для рисования круга из модуля Graph так же имеет имя Circle, то для ее однозначной идентификации необходимо использовать составное имя Graph. Circle.
Это определение содержит следующие элементы:
1. Унаследованные поля X, Y, Visible;
2. Собственное поле Radius;
3. Унаследованные без изменения методы GetX, GetY;
4. Новый собственный метод GetR;
5. Полностью переопределенные методы Circle. SwitchOn, Circle. SwitchOff и Circle. Move;
6. Частично переопределенный метод Circle. Create. Для инициализации полей X, Y, Visible используется унаследованный метод Point. Create, но для инициализации поля Radius метод расширен.
Замечание 1. К полям любого объекта можно обращаться и напрямую, например, без использования процедуры Circle. Create, функции GetR и других. Но является правилом хорошего стиля программирования обращение ко всем полям только с помощью методов данного объекта, то есть поля считаются скрытыми.
Замечание 2. Переопределять можно только методы. Поля, указанные в родительском типе, наследуются безусловно и не могут быть переопределены, то есть имена полей типа-потомка не должны совпадать с именами полей типа-предка. Кроме того, переопределенный метод может иметь совершенно другие параметры в отличие от метода-предка.
Раннее и позднее связывание
Рассмотрим объектные типы Point и Circle. Они связаны отношением наследования и содержат, наряду с другими, методы SwitchOn, SwithOff и Move. Первые два из них реализуют алгоритмы рисования и удаления фигуры с экрана, они существенно различны для разных типов. Алгоритм же метода Move практически одинаков для обоих типов: удаление фигуры со старого места, изменение ее координат и рисование на новом месте. Например:
Procedure Move (dx, dy: integer);
Begin
SwitchOff;
X:=X+dx;
Y:=Y+dy;
SwitchOn;
end;
Рассмотрим вариант унаследования этого метода без переопределения. Пусть имеются экземпляры двух разных объектов:
Var
OnePoint: Point;
OneCircle: Circle;
то вызовы методов
OnePoint. Move (10, -20);
OneCircle. Move (10, -20);
приведут к одному и тому же действию: перемещению точки. Это связано с тем, что экземпляр типа-потомка вызывает унаследованный метод Move, который жестко связан с методами Point. SwitchOn и Point. SwitchOff на ранней стадии, то есть еще во время компиляции программы. В данном случае связь методов является статической.
Для того, чтобы в полной мере использовать свойство полиморфизма и использовать одни и те же унаследованные методы, по-разному применяемые к разным объектам, необходимо, во-первых, разорвать связь метода Move с методами предка Point, во-вторых, обеспечить возможность вызывать разные методы SwitchOn и SwitchOff в зависимости от того, какой объект вызывает Move.
Это можно реализовать только во время вызова метода, то есть в процессе выполнения программы, поэтому такой механизм называется динамическим или поздним связыванием и достигается введением виртуальных методов.
§10.3. Виртуальные методы
Метод становится виртуальным, если после его заголовка стоит служебное слово Virtual. Необходимо помнить, что если метод в родительском типе объявлен как виртуальный, то все одноименные методы у потомков так же должны быть виртуальными. Кроме того, они все должны иметь одинаковый набор формальных параметров, что и самый первый виртуальный метод.
Если объекты являются динамическими, то, по аналогии с динамическими переменными они должны создаваться и уничтожаться. Для этого введены понятия «Конструктор» и «Деструктор», которые обсуждаются далее. Так, типы Point и Circle можно определить следующим образом:
Type
Point = object
…
Constructor Create (a, b: integer);
Destructor Done; VIRTUAL;
Procedure SwitchOn; VIRTUAL;
Procedure SwitchOff; VIRTUAL;
Procedure Move (dx, dy: integer);
end;
Circle = object (Point)
…
Constructor Create (a, b, R: integer);
Procedure SwitchOn; VIRTUAL;
Procedure SwitchOff; VIRTUAL;
end;
Полные описания методов остаются такими же, как и раньше. Теперь обращения к методам
OnePoint. Move (10, -20);
OneCircle. Move (10, -20);
дадут разный результат, то есть определение виртуального метода можно представить как шаблон для всех родственных ему методов.
Свойство полиморфизма предоставляет очень широкие возможности при разработке программ. Типы объектов и методы, определенные в различных модулях, могут поставляться пользователям в виде TPU-файлов без исходного кода. Для работы с объектами модулей только необходимо знать содержимое интерфейсной части. Зная его, можно создавать не только компактные описания новых объектов, но и добавлять новые методы к уже существующим. Возможность добавления новых функциональных характеристик в программу без модификации ее исходного текста называется способностью к расширению. С другой стороны, работа с виртуальными методами происходит немного медленнее и требует дополнительных затрат памяти, но это не должно служить препятствием к широкому использованию виртуальных методов.
Конструкторы и деструкторы
Корректная работа с виртуальными методами требует определенных правил.
Если объектный тип содержит хотя бы один виртуальный метод, то он должен иметь хотя бы один особый метод, называемый конструктором (этот метод может быть и унаследован). Конструктор должен быть применен к экземпляру объекта до первого вызова виртуального метода так же, как перед обращением к динамической переменной необходимо ее создать. Кроме этого, каждый экземпляр объекта должен инициализироваться отдельным вызовом конструктора. Другие экземпляры, даже если они содержат правильные данные, но не проинициализированы, приведут к ошибке выполнения при любых вызовах их виртуальных методов. Например:
Var
OnePoint, TwoPoint: Point;
Begin
OnePoint. Create (50,100);
…
TwoPoint. Move (21, -20); { Неправильный вызов, второй экземпляр не создан }
Каждый тип объекта, содержащий виртуальные методы, имеет таблицу виртуальных методов (ТВМ), хранящуюся в области данных. ТВМ содержит размер типа объекта и для каждого виртуального метода указатель кода, исполняющий данный метод. Конструктор устанавливает связь между вызывающим его экземпляром объекта и его ТВМ.
Так как имеется только одна ТВМ для любого типа объекта, то отдельные экземпляры объекта содержат только адрес ТВМ, а конструктор устанавливает значение этого адреса.
С процедурой освобождения памяти связан особый вид метода, называемый деструктором, и предназначенный для выполнения действий завершающего характера. В отличие от методов-конструкторов, деструкторы могут быть виртуальными и могут наследоваться, в одном объектном типе может быть определено несколько деструкторов.
В случае единственного деструктора в Турбо Паскале рекомендуется использовать название Done. Этот метод должен инкапсулировать все детали очистки своего и вложенных объектов и структур данных. Основное преимущество использования деструктора заключается в удалении из памяти полиморфных объектов, то есть таких, которые были созданы во время вызова метода, а не во время компиляции.
Так как размеры типов объектов различны, то во время компиляции из полиморфного объекта нельзя извлечь какую-либо информацию о его размере. Эта информация становится доступной для деструктора в момент удаления при обращении к таблице виртуальных методов экземпляров объектов этого типа.
Сам по себе метод деструктора может быть пустым и выполнять только функцию связи с процедурой высвобождения памяти:
Destructor Point. Done;
Begin
End;
Деструктор дочернего типа последним действием должен вызывать соответствующий деструктор своего непосредственного предка, чтобы освободить поля всех наследуемых указателей объекта. В Турбо Паскале можно использовать служебное слово INHERITED (V7.0), с помощью которого можно вызывать методы предка без указания его имени, например:
Destructor Circle. Done;
Begin
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Основные порталы (построено редакторами)
