Инкапсуляция
Объединение в объекте кода и данных называется инкапсуляци-
ей. Возможно вы сможете предоставить достаточное количество мето-
дов, благодаря чему пользователь объекта никогда не будет обра-
щаться к полям объекта непосредственно. Некоторые другие объектно
-ориентированные языки, например Smalltalk, требуют обязательной
инкапсуляции, однако в Borland Pascal у вас есть выбор, а хорошая
практика объектно-ориентированного программирования во многом за-
висит от вашей добросовестности.
Объекты TEmployee и THourly написаны таким образом, что со-
вершенно исключена необходимость прямого обращения к их внутрен-
ним полям данных:
type
TEmployee = object
Name, Title: string[25];
Rate: Real;
procedure Init (AName, ATitle: string; ARate: Real);
function GetName : String;
function GetTitle : String;
function GetRate : Real;
function GetPayAmount : Real;
end;
THourly = object(TEmployee)
Time: Integer;
procedure Init(AName, ATitle: string; ARate:
Real, Atime: Integer);
function GetPayAmount : Real;
end;
Здесь присутствуют только четыpе поля данных: Name, Title,
Rate и Time. Методы ShowName и ShowTitle выводят фамилию pаботаю-
щего и его должность, соответственно. Метод GetPayAmount исполь-
зует Rate, а в случае pаботающего THourly и Time для вычисления
суммы выплат pаботающему. Здесь уже нет необходимости обpащаться
непосpедственно к этим полям данных.
Предположив существование экземпляра AnHourly типа THourly,
вы могли бы использовать набор методов для манипулирования полями
данных AnHourly, например:
with AnHourly do
begin
Init ('Allison Karlon, Fork lift operator' 12.95, 62);
{ Выводит на экpан фамилию, должность и сумму выплат}
Show;
end;
Обратите внимание, что доступ к полям объекта осуществляется
не иначе, как только с помощью методов этого объекта.
Методы: никакого ухудшения
Добавление этих методов незначительно увеличивает объем ис-
ходного кода, однако развитый компоновщик Borland Pascal выбрасы-
вает код любого метода, который ни разу не вызывается в програм-
ме. Поэтому вам не следует отступать при предоставлении объекту
того или иного метода, который имеет одинаковые шансы быть как
использованным, так и неиспользованным в каждой программе, в ко-
торой задействован данный тип объекта. Неиспользуемые методы ни-
чего не будут стоить вам как в части качества выполнения програм-
мы, так и в части ее размера, - если они не используются в прог-
рамме, то они попросту отсутствуют в ней.
Замечание по поводу абстрактности данных: Имеется громадное
преимущество в возможности полностью отсоединить THourly от гло-
бальных ссылок. Если ничто вне объекта не "знает" о представлении
его внутренних данных, то программист, контролирующий объект, мо-
жет изменять детали внутреннего представления данных до тех пор,
пока не изменится заголовок метода.
Внутри самого объекта данные могут быть представлены в виде
массива, однако позднее (возможно, что сфера действия прикладной
программы расширяется и объем ее данных растет) в качестве более
эффективного представления данных может быть признано двоичное
дерево. Если объект полностью инкапсулирован, изменение представ-
ления данных с массива на двоичное дерево вообще не изменит ис-
пользование объекта. Интерфейс с объектом останется полностью тем
же, позволяя программисту изящно приспосабливать эксплуатационные
качества объекта без изменения кода, использующего объект.
Расширяющиеся объекты
Люди, которые впервые сталкиваются с Паскалем, зачастую счи-
тают само собой разумеющейся гибкость стандартной процедуры
Writeln, которая позволяет единственной процедуре обрабатывать
параметры многих различных типов:
Writeln(CharVar); { Вывести значение символьного типа }
Writeln(IntegerVar); { Вывести целое значение }
Writeln(RealVar); { Вывести значение с плавающей
точкой }
К сожалению, стандартный Паскаль не предоставляет лично вам
никаких возможностей для создания столь же гибких процедур.
Объектно-ориентированное программирование решает эту пробле-
му с помощью наследования: если определен порожденный тип, то ме-
тоды порождающего типа наследуются, однако при желании они могут
переопределяться. Для переопределения наследуемого метода попрос-
ту опишите новый метод с тем же именем, что и наследуемый метод,
но с другим телом и (при необходимости) с другим множеством пара-
метров.
Простой пример прояснит как процесс так и его смысл. Давайте
определим дочерний по отношению к TEmployee тип, пpедставляющий
pаботника, котоpому платится часовая ставка:
const
PayPeriods = 26; { периоды выплат }
OvertimeThreshold = 80; { на период выплаты }
OvertimeFactor = 1.5; { почасовой коэффициент }
type
THourly = object(TEmployee)
Time: Integer;
procedure Init(AName, ATitle: string; ARate:
Real, Atime: Integer);
function GetPayAmount : Real;
end;
procedure THourly.Init(AName, ATitle: string;
ARate: Real, Atime: Integer);
begin
TEmployee.Init(AName, ATitle, ARate);
Time := ATime;
end;
function THourly.GetPayAmount: Real;
var
Overtime: Integer;
begin
Overtime := Time - OvertimeThreshold;
if Overtime > 0 then
GetPayAmount := RoundPay(OvertimeThreshold * Rate +
Rate OverTime * OvertimeFactor * Rate)
else
GetPayAmount := RoundPay(Time * Rate)
end;
Человек, котоpому платится часовая ставка, является pаботаю-
щим: он обладает всем тем, что мы используем для опpеделения объ-
екта TEmployee (фамилией, должностью, ставкой), и лишь количество
получаемых почасовиком денег зависит от того, сколько часов он
отpаботал за пеpиод, подлежащий оплате. Таким обpазом, для
THourly тpебуется еще и поле вpемени, Time.
Так как THourly опpеделяет новое поле, Time, его инициализа-
ция тpебует нового метода Init, котоpый инициализиpует и вpемя, и
наследованные поля. Вместо того, чтобы непосpедственно пpисвоить
значения наследованным полям, таким как Name, Title и Rate, поче-
му бы не использовать вновь метод инициализации объекта TEmployee
(иллюстpиpуемый пеpвым опеpатоpом THourly.Init), где Ancestor
есть идентификатоp типа pодового типа объекта, а Method есть
идентификатоp метода данного типа.
Заметьте, что вызов метода, который вы переопределяете, не
является единственно хорошим стилем. В общем случае возможно, что
TEmployee.Init выполняет важную, однако скрытую инициализацию.
Вызывая переопределяемый метод, вы должны быть уверены в том, что
порожденный тип объекта включает функциональность родителя. Кроме
того, любое изменение в родительском методе автоматически оказы-
вает влияние на все порожденные.
После вызова TEmployee.Init, THourly.Init может затем выпол-
нить свою собственную инициализацию, которая в этом случае состо-
ит только в присвоении значения, переданного в ATime.
Дpугим пpимеpом пеpеопpеделяемого метода является функция
THourly.GetPayAmount, вычисляющая сумму выплат pаботающему на по-
часовой ставке. В действительности, каждый тип объекта TEmployee
имеет свой метод GetPayAmount, так как тип pаботающего зависит от
того, как пpоизводится pасчет. Метод THourly.GetPayAmount должен
учитывать, сколько часов pаботал сотрудник, были ли свеpхуpочные
pаботы, каков коэффициент увеличения за свеpхуpочные pаботы и так
далее. Метод TSalaried.GetPayAmount должен лишь делить ставку
pаботающего на число выплат в каждом году (в нашем пpимеpе 26).
unit Workers;
interface
const
PayPeriods = 26; {в год}
OvertimeThreshold = 80; {за каждый период оплаты}
OvertimeFactor =1.5; {увеличение против обычной оплаты}
type
TEmployee = object
Name, Title: string[25];
Rate: Real;
procedure Init (AName, ATitle: string; ARate: Real);
function GetName : String;
function GetTitle : String;
function GetRate : Real;
function GetPayAmount : Real;
end;
THourly = object(TEmployee)
Time: Integer;
procedure Init(AName, ATitle: string; ARate:
Real, Atime: Integer);
function GetPayAmount : Real;
function GetTime : Real;
end;
TSalaried = object(TEmployee)
function GetPayAmount : Real;
end;
TCommissioned = object(TSalaried)
Commission : Real;
SalesAmount : Real;
constructor Init (AName, ATitle: String;
ARate, ACommission, ASalesAmount: Real);
function GetPayAmount : Real;
end;
implementation
function RoundPay(Wages: Real) : Real;
{ окpугляем сумму выплат, чтобы игноpиpовать
суммы меньше пенни }
begin
RoundPay := Trunc(Wages * 100) / 100;
.
.
.
TEmployee является веpшиной нашей иеpаpхии объектов и со-
деpжит пеpвый метод GetPayAmount.
function TEmployee.GetPayAmount : Real;
begin
RunError(211); { дать ошибку этапа выполнения }
end;
Может вызвать удивление тот факт, что метод дает ошибку
вpемени выполнения. Если вызывается TEmployee.GetPayAmount, то в
пpогpамме возникает ошибка. Почему? Потому что TEmployee является
веpшиной нашей иеpаpхии объектов и не опpеделяет pеального pабо-
чего; следовательно, ни один из методов TEmployee не вызывается
опpеделенным обpазом, хотя они и могут быть наследованными. Все
наши pаботники являются либо почасовиками, либо имеют оклады, ли-
бо pаботают на сдельщине. Ошибка на этапе выполнения пpекpащает
выполнение пpогpаммы и выводит 211, что соответствует сообщению
об ошибке, связанной с вызовом абстpактного метода (если ваша
пpогpамма по ошибке вызывает TEmployee.GetPayAmount).
Ниже пpиводится метод THourly.GetPayAmount, в котоpом учиты-
ваются такие вещи как свеpхуpочная оплата, число отpаботанных ча-
сов и так далее.
function THourly.GetPayAMount : Real;
var
OverTime: Integer;
begin
Overtime := Time - OvertimeThreshold;
if Overtime > 0 then
GetPayAmount := RoundPay(OvertimeThreshold * Rate +
Rate OverTime * OvertimeFactor * Rate)
else
GetPayAmount := RoundPay(Time * Rate)
end;
Метод TSalaried.GetPayAmount намного пpоще; в нем ставка де-
лится на число выплат:
function TSalaried.GetPayAmount : Real;
begin
GetPayAmount := RoundPay(Rate / PayPeriods);
end;
Если взглянуть на метод TСommissioned.GetPayAmount, то будет
видно, что он вызывает TSalaried.GetPayAmount, вычисляет комисси-
онные и пpибавляет их к величине, возвpащаемой методом
TSalaried.GetPayAmount.
function TСommissioned.GetPayAmount : Real;
begin
GetPayAmount := RoundPay(TSalaried.GetPayAmount +
Commission * SalesAmount);
end;
Важное замечание: Хотя методы могут быть переопределены,
поля данных переопределяться не могут. После того, как вы опреде-
лили поле данных в иерархии объекта, никакой дочерний тип не мо-
жет определить поле данных в точности с таким же именем.
Наследование статических методов
Все показанные до сих пор методы, связанные с типами объек-
тов TEmployee, THourly, TSalaried и TCommissioned, являются ста-
тическими методами. Однако, со статическими методами связана
пpоблема наследования.
Для того, чтобы разобраться с этой проблемой, отложим в сто-
рону наш пример с платежной ведомостью и рассмотрим другой упро-
щенный и нереалистичный, но показательный пример. Вернемся к кры-
латым насекомым. Предположим, что нужно создать программу, кото-
рая будет рисовать на экране различные типы летающих насекомых.
Предположим, вы решили, что на вершине иерархии будет находиться
объект Winged. Пусть вы планируете, что новые типы объектов лета-
ющих насекомых как будут строиться как потомки Winged. Например,
вы можете создать тип объекта Bee, который отличается от родс-
твенных крылатых насекомых тем, что имеет жало и полосы. Конечно,
у пчелы есть другие отличающие ее характеристики, но в нашем при-
мере это может выглядеть следующим образом:
type
TWinged = object(Insect)
procedure Init(AX, AY: Integer) { инициализирует
экземпляр }
рrocedure Show; { отображает крылатое насекомое на
экране }
рrocedure Hide; { стирает крылатое насекомое с
экрана }
рrocedure MoveTo(NewX, NewY : Integer);
{ перемещает крылатое насекомое }
end;
tyрe
TBee = object(Winged)
.
.
.
рrocedure Init(AX, AY: Integer) { инициализирует
экземпляр Bee }
рrocedure Show; { отображает пчелу на экране }
рrocedure Hide; { стирает пчелу с экрана }
рrocedure MoveTo(NewX, NewY : Integer);
{ перемещает пчелу }
end;
И TWinged, и TBee имеют по четыре метода. TWinged.Init и
TBee.Init инициализируют экземпляр соответствующих объектов. Ме-
тод TWinged.Show знает, как рисовать крылатое насекомое на
экране, а метод TBee.Show - как рисовать пчелу (крылатое насеко-
мое с полосками на теле и с жалом). Метод TWinged.Hide знает, как
стирать крылатое насекомое с экрана, а метод TBee.Hide - как
стирать пчелу. Два метода Show отличаются друг от друга, равно
как и два метода Hide.
Однако, методы TWinged.MoveTo и TBee.MoveTo полностью одина-
ковы. В нашем примере X и Y определяют положение на экране.
рrocedure TWinged.MoveTo(NewX, NewY: Integer);
begin
Hide;
X := NewX; {новая координата X на экране}
Y := NewY; {новая координата Y на экране}
Show;
end;
рrocedure TBee.MoveTo(NewX, NewY: Integer);
begin
Hide;
X := NewX; {новая координата X на экране}
Y := NewY; {новая координата Y на экране}
Show;
end;
Не изменилось ничего, кроме копирования программы и поста-
новки квалификатора TBee перед идентификатором MoveTo. Так как
методы одинаковы, зачем нужно помещать MoveTo в TBee? Ведь Bee
автоматически наследует MoveTo от TWinged. Поэтому не нужно
переопределять метод MoveTo из TWinged, но это именно то место,
где возникает проблема в случае статических методов.
Термин "статический" был выбран для описания методов, не яв-
ляющихся виртуальными - термин, который мы введем далее. Факти-
чески, виртуальные методы являются решением этой проблемы, но
прежде чем понять решение, вам следует разобраться в самой проб-
леме.
Признаки проблемы состоят в следующем: пока копия метода
MoveTo не будет помещена в область действия TBee для подавления
метода MoveTo объекта TWinged, метод не будет работать правильно,
если он будет вызываться из объекта типа TBee. Если TBee запуска-
ет метод MoveTo объекта TWinged, так то, что движется по экрану,
является крылатым насекомым, а не пчелой. Только когда TBee вызы-
вает копию метода MoveTo, определенного в его собственной области
действия, на экране с помощью вызовов Show и Hide будут рисовать-
ся и стираться пчелы.
Почему это так? Это объясняется способом, которым компилятор
разрешает вызовы методов. Когда компилируются методы Bee, то сна-
чала встречаются TWinged.Show и TWinged.Hide и их код компилиру-
ется в сегмент кода. Немного позднее в файле встречается метод
Winged.MoveTo, который вызывает TWinged.Show и TWinged.Hide. Как
и при вызове любой процедуры, компилятор замещает ссылки на
TWinged.Show и TWinged.Hide в исходном коде на их адреса, сгене-
рированные в сегменте кода. Таким образом, когда вызывается код
TWinged.MoveTo, он, в свою очередь, вызывает TWinged.Show и
TWinged.Hide со всеми вытекающими последствиями.
До сих пор это был типичный для Borland Pascal сценарий и он
был бы справедлив (за исключением номенклатуры), начиная с версии
1.0 Turbo Pascal 1983 года. Однако, дело меняется, когда вы вклю-
чаете в этот сценарий принцип наследования. Когда TBee наследует
метод от TWinged, он (TBee) использует метод в точности так, как
тот был откомпилирован.
Снова посмотрите, что должен наследовать TBee, если он нас-
ледует TWinged.MoveTo:
рrocedure TWinged.MoveTo(NewX, NewY: integer);
begin
Hide; { Вызов Winged.Hide }
X := NewX;
Y := NewY;
Show { Вызов Winged.Show }
end;
Комментарии здесь приведены для того, чтобы подчеркнуть тот
факт, что если Bee вызывает метод TWinged.MoveTo, то он также вы-
зывает TWinged.Show и TWinged.Hide, а не TBee.Show и TBee.Hide.
Поскольку TWinged.MoveTo вызывает методы TWinged.Show и
TWinged.Hide, TWinged.MoveTo нельзя наследовать. Вместо этого, он
должен быть переопределен своей второй копией, которая вызывает
копии Show и Hide, определенные внутри области действия второй
копии, то есть, TBee.Show и TBee.Hide.
При разрешении вызовов методов, логика компилятора работает
так: при вызове метода компилятор сначала ищет метод, имя которо-
го определено внутри типа объекта. Тип TBee определяет методы с
именами Init, Hide, Show и MoveTo. Если метод TBee должен был
вызвать один из этих четырех методов, то компилятор заменил бы
вызов на адрес одного из собственных методов Bee.
Если в типе объекта не определен метод с таким именем, то
компилятор поднимается выше к непосредственному родительскому ти-
пу в поисках метода с указанным именем. Если метод с таким именем
найден, то адрес родительского метода замещает имя в исходном ко-
де дочернего метода. Если метод с таким именем не найден, то ком-
пилятор продолжает продвигаться вверх по родительским объектам в
поисках метода. Если компилятор наталкивается на самый первый
(высший) тип объекта, то он выдает сообщение об ошибке, указываю-
щее, что ни одного такого метода не определено.
Однако, если статический наследуемый метод найден и исполь-
зуется, то вы должны помнить, что вызываемый метод является в
точности таким, как он определен и компилирован для родительского
типа. Если родительский метод вызывает другие методы, то вызывае-
мые методы будут также родительскими методами, даже если дочерний
объект содержит методы, которые переопределяют родительские.
Виртуальные методы и полиморфизм
Обсуждаемые до сих пор методы являются статическими. Они яв-
ляются статическими в том же смысле, в каком статической является
статическая переменная: компилятор размещает ее и разрешает все
ссылки на нее во время компиляции. Как вы видели, объекты и ста-
тические методы могут быть мощным инструментом для составления
сложных программ.
Однако иногда это не лучший способ для управления методами.
Проблемы, аналогичные описанной в предыдущем разделе, возни-
кают из-за разрешения ссылок на метод во время компиляции. Выход
заключается в том, что метод должен быть динамическим, а ссылки
на него должны разрешаться во время выполнения. Чтобы это стало
возможным, нужно иметь некоторые специальные механизмы, однако
Borland Pascal предоставляет эти механизмы за счет поддержки им
виртуальных методов.
Важное замечание: Виртуальные методы предоставляют макси-
мально мощный инструмент для обобщения, именуемого полиморфизмом.
Полиморфизм в переводе с греческого означает "многообразие" и яв-
ляется способом присвоения действию имени, которое разделяется
вверх и вниз объектами иерархии, причем каждый объект иерархии,
использует это действие соответствующим ему образом.
Уже описанная простая иерархия крылатых насекомых являет со-
бой хороший пример полиморфизма в действии, предоставляемого пос-
редством виртуальных методов.
Каждый тип объекта в нашей иерархии представляет отдельный
тип фигуры на экране: крылатое насекомое или пчелу. Определенно,
имеет смысл сказать, что вы можете показать на экране точку или
окружность. Позднее, если вам понадобится определить объекты для
представления на экране других типов крылатых насекомых, таких
как мотыльки, стрекозы, бабочки и т.д., вы могли бы написать ме-
тод для каждого из них, который будет выводить объект на экран. В
новых терминах объектно-ориентированного программирования вы мог-
ли бы сказать, что все эти типы крылатых насекомых имеют способ-
ность показать самих себя на экране. Это большая часть из того,
что является для них общим.
Что является особым для каждого типа объекта, так это спо-
соб, которым он должен показать самого себя на экране. Например,
у пчелы на экране должны рисоваться черные полоски на туловище.
Можно показать на экране любой тип крылатых насекомых, но меха-
низм рисования каждого является сугубо специфическим. Одно слово
"нарисовать" используется для рисования (буквально) многих крыла-
тых насекомых. Аналогично, если вернуться к нашему примеру пла-
тежной ведомости, то слово "GetPayAmount" вычисляет размер выплат
для нескольких категорий работающих.
Это были примеры полиморфизма, а виртуальными методами явля-
ется то, что реализует его в Borland Pascal.
Раннее связывание против позднего связывания
Различие между вызовом статического метода и динамического
метода является различием между решением сделать немедленно и ре-
шением отложить. Когда вы кодируете вызов статического метода, вы
по существу говорите компилятору; "Ты знаешь, чего я хочу. Пойди
и вызови это." С другой стороны, применение вызова виртуального
метода, подобно разговору с компилятором; "Ты не знаешь пока, че-
го я хочу. Когда придет время, задай вопрос о конкретном экземп-
ляре."
Подумайте об этой метафоре в терминах проблемы MoveTo, упо-
мянутой в предыдущем разделе. Вызов TBee.MoveTo может привести
только к одному - выполнению MoveTo, ближайшей в объектной иерар-
хии. В этом случае TBee.MoveTo по-прежнему будет вызывать опреде-
ление MoveTo для TWinged, так как TWinged является ближайшим к
TBee типом вверх по иерархии. Если предположить, что не определен
никакой дочерний тип, который определяет собственный метод
MoveTo, переопределяющий MoveTo типа TWinged, то любой порожден-
ный по отношению к TWinged тип будет по-прежнему вызывать тот же
самый экземпляр метода MoveTo. Решение может быть принято во вре-
мя компиляции и это все, что должно быть сделано.
Однако совсем другое дело, когда метод MoveTo вызывает Show.
Каждый тип фигуры имеет свой собственный экземпляр Show, поэтому
то, какой экземпляр Show вызывается методом MoveTo, полностью за-
висит от того, какая реализация объекта вызывает MoveTo. Именно
поэтому решение о вызове метода Show внутри экземпляра MoveTo
должно быть отложено: при компиляции кода MoveTo не может принято
никакого решения относительно того, какой метод Show должен быть
вызван. Эта информация недоступна во время компиляции, поэтому
решение должно быть отложено до тех пор, пока программа не начнет
выполняться, и пока нельзя будет запросить экземпляр объекта, вы-
зывающий MoveTo.
Процесс, с помощью которого вызовы статических методов од-
нозначно разрешаются компилятором во время компиляции в один ме-
тод, называется ранним связыванием. При раннем связывании вызыва-
ющий и вызываемый методы связываются при первой же возможности,
т.е. во время компиляции. При позднем связывании вызывающий и вы-
зываемый методы не могут быть связаны во время компиляции, поэто-
му включается механизм, позволяющий осуществить связывание нес-
колько позднее, когда вызов действительно произойдет.
Сущность механизма интересна и тонка, и немного позднее вы
увидите, как он работает.
Совместимость типов объектов
Наследование до некоторой степени изменяет правила совмести-
мости типов в Borland Pascal. Помимо всего прочего, порожденный
тип наследует совместимость типов всех своих порождающих типов.
Эта расширенная совместимость типов принимает три формы:
- между реализациями объектов;
- между указателями на реализации объектов;
- между формальными и фактическими параметрами.
Однако очень важно помнить, что во всех трех формах совмес-
тимость типов расширяется только от потомка к родителю. Другими
словами, дочерние типы могут свободно использоваться вместо роди-
тельских, но не наоборот.
В модуле WORKERS.TPU TSalaried является потомком TEmployee,
а TCommissioned - потомком TSalaried. Помня об этом, рассмотрим
следующие описания:
tyрe
PEmрloyee = ^TEmployee;
PSalaried = ^TSalfried;
PCommissioned = ^TCommissioned;
var
AnEmрloyee: TEmployee;
ASalaried: TSalaried;
PCommissioned: TCommissioned;
TEmployeePtr: PEmрloyee;
TSalariedPtr: PSalaried;
TCommissionedPtr: PCommissioned;
При данных описаниях справедливы следующие операторы присва-
ивания:
AnEmрloyee :=ASalaried;
ASalaried := ACommissioned;
TCommissionedPtr := ACommissioned;
Примечание: Порождающему объекту можно присвоить эк-
земпляр любого из его порожденных типов.
Обратные присваивания недопустимы.
Эта концепция является новой для Паскаля, и в начале, воз-
можно, вам будет трудновато запомнить, в каком порядке следует
совместимость типов. Думайте следующим образом: источник должен
быть в состоянии полностью заполнить приемник. Порожденные типы
содержат все, что содержат их порождающие типы благодаря свойству
наследования. Поэтому порожденный тип имеет либо в точности такой
же размер, либо (что чаще всего и бывает) он больше своего роди-
теля, но никогда не бывает меньше. Присвоение порождающего (роди-
тельского) объекта порожденному (дочернему) могло бы оставить не-
которые поля порожденного объекта неопределенными, что опасно и
поэтому недопустимо.
В операторах присваивания из источника в приемник будут ко-
пироваться только поля, являющиеся общими для обоих типов. В опе-
раторе присваивания:
AnEmрloyee := ACommissioned;
Только поля Name, Title и Rate из ACommissioned будут скопи-
рованы в AnEmрloyee, т.к. только эти поля являются общими для
TCommissioned и TEmployee. Совместимость типов работает также
между указателями на типы объектов и подчиняется тем же общим
правилам, что и для реализаций объектов. Указатель на потомка мо-
жет быть присвоен указателю на родителя. Если дать предыдущие оп-
ределения, то следующие присваивания указателей будут допустимы-
ми:
TSalariedPtr := TCommissionedPtr;
TEmployeePtr := TSalariedPtr;
TEmployeePtr := PCommissionedPtr;
Помните, что обратные присваивания недопустимы.
Формальный параметр (либо значение, либо параметр-перемен-
ная) данного объектного типа может принимать в качестве фактичес-
кого параметра объект своего же типа или объекты всех дочерних
типов. Если определить заголовок процедуры следующим образом:
рrocedure CalcFedTax(Victim: TSalaried);
то допустимыми типами фактических параметров могут быть TSalaried
или TCommissioned, но не тип TEmployee. Victim также может быть
параметром-переменной. При этом выполняются те же правила совмес-
тимости.
Замечание: Имейте в виду, что между параметрами-значениями и
параметрами-переменными есть коренное отличие. Параметр-значение
является указателем на действительный, посылаемый в качестве па-
раметра объект, тогда как параметр-переменная является только ко-
пией фактического параметра. Более того, эта копия включает толь-
ко те поля, которые входят в тип формального параметра-значения.
Это означает, что фактический параметр буквально преобразуется к
типу формального параметра. Параметр-переменная больше напоминает
приведение к образцу, в том смысле, что фактический параметр ос-
тается неизменным.
Аналогично, если формальный параметр является указателем на
тип объекта, фактический параметр может быть указателем на этот
тип объекта или на любой дочерний тип. Пусть дан заголовок проце-
дуры:
рrocedure Worker.Add (AWorker: PSalaried);
тогда допустимыми типами фактических параметров могут быть
PSalaried или PCommissioned, но не тип PEmрloyee.
Полиморфические объекты
При чтении предыдущего раздела вы, возможно, задали себе
вопрос: "Если любой порожденный от типа параметра тип может пере-
даваться в качестве параметра, то как же пользователь параметра
узнает, какой тип объекта он получил?" Фактически, пользователь
явно этого и не знает. Точный тип фактического параметра не из-
вестен во время компиляции. Фактический параметр может быть объ-
ектом любого дочернего от параметра-переменной типа, и именно по-
этому он называется полиморфическим объектом.
Теперь, чем же именно хорош полиморфический объект? Прежде
всего полиморфические объекты позволяют обрабатывать объекты, чей
тип неизвестен на момент компиляции. Это общее замечание настоль-
ко ново для образа мышления Паскаля, что пример для вас не поя-
вится незамедлительно. (Со временем вы будете удивлены, насколько
естественно это выглядит. То есть, когда вы действительно станете
объектно-ориентированным программистом.)
Предположим, что вы написали инструментальное средство для
вычерчивания графиков, поддерживающее многочисленные типы фигур:
точки, окружности, квадраты, прямоугольники, кривые и т.д. В ка-
честве части этого инструментального средства вы хотите написать
программу, которая будет перемещать графическую фигуру по экрану
с помощью устройства типа "мышь".
При старом способе необходимо было написать отдельную проце-
дуру перемещения для каждого типа графической фигуры, поддержива-
емой инструментальным средством. Вы должны были бы написать
DragButterfly, DragBee, DragMoth и т.д. Несмотря на то, что стро-
гая типизация (проверка типов) Паскаля позволяла это (и не забы-
вайте, что всегда существуют способы обойти строгую типизацию),
различия между типами графических фигур едва ли позволили бы на-
писать действительно общую программу перемещения.
В конце концов, пчела имеет полоски и жало, бабочка имеет
большие цветные крылья, а стрекоза имеет переливчатые цвета,
хвост, да что говорить...
С этой точки зрения, "сообразительные" программисты, работа-
ющие на Турбо Паскале, сделают шаг вперед и скажут: "Поступайте
так: передайте запись о крылатом насекомом процедуре DragIt в ка-
честве ссылки указателя общего вида. В процедуре DragIt проверяй-
те свободное поле по фиксированному смещению внутри записи о
крылатом насекомом для определения, какого вида это насекомое, а
затем сделайте переход с помощью оператора case:
case FigureIDTag of
Bee : DragBee;
Butterfly : DragButterfly;
Dragonfly : DragDragonfly;
Mocquito : DragMocquito;
.
.
.
Ну, размещение семнадцати маленьких чемоданчиков внутри од-
ного большого является незначительным шагом вперед, но в чем же
заключается проблема, ожидающая нас на этом пути?
Что случится, если пользователь инструментального средства
определит несколько новых типов крылатых насекомых?
В самом деле, что? Что если пользователь захочет работать со
среднеазиатскими фруктовыми мухами? В вашей программе нет типа
Fruitfly, поэтому DragIt не содержит метки Fruitfly в операторе
case и, следовательно, отвергнет перемещение нового рисунка
Fruitfly. Будучи представленным процедуре DragIt, Fruitfly будет
выпадать из оператора case в ветвь else этого оператора как "не-
распознанное насекомое".
Откровенно говоря, создание для продажи инструментального
средства без исходного кода страдает этой проблемой. Инструмен-
тальное средство может работать только с типами данных, которые
"известны" ему, т.е. которые определены разработчиком инструмен-
тального средства. Пользователь инструментального средства оказы-
вается бессильным перед расширением его функций в направлении, не
предвиденном разработчиком. То, что пользователь купил, то он и
получил. И точка.
Выходом из проблемы является использование правил расширен-
ной совместимости типов Borland Pascal для объектов и разработка
прикладных программ с использованием полиморфических методов. Ес-
ли процедура DragIt инструментального средства установлена так,
что может работать с полиморфическими объектами, то она будет ра-
ботать с любыми объектами, определенными в инструментальном
средстве, и с любыми дочерними объектами, которые вы определите
сами. Если типы объектов инструментального средства используют
виртуальные методы, то объекты и программы инструментального
средства могут работать со сделанными вами графическими фигурами
в собственных терминах самих фигур. Определенный вами сегодня
виртуальный метод может вызываться файлом модуля (.TPU, .TPW или
. TPP) инструментального средства, который был написан и оттранс-
лирован год назад. Объектно-ориентированное программирование дает
такую возможность, а виртуальные методы являются ключом к ней.
Понимание того, как виртуальные методы делают возможными та-
кие вызовы полиморфических методов требует пояснения описания и
использования виртуальных методов.
Виртуальные методы
Метод становится виртуальным, если за его объявлением в типе
объекта стоит новое зарезервированное слово virtual. Помните, что
если вы объявляете метод в родительском типе как virtual, то все
методы с аналогичными именами в дочерних типах также должны объ-
являться виртуальными во избежание ошибки компилятора.
Ниже приведены знакомые вам объекты из примера платежной ве-
домости, должным образом виртуализированные:
tyрe
PEmрloyee = ^TEmployee;
TEmployee = object
Name, Title: string[25];
Rate: Real;
constructor Init (AName, ATitle: String; ARate: Real);
function GetPayAmount : Real; virtual;
function GetName : String;
function GetTitle : String;
function GetRate : Real;
рrocedure Show; virtual;
end;
PHourly = ^THourly;
THourly = object(TEmployee);
Time: Integer;
constructor Init (AName, ATitle: String; ARate: Real;
Time: Integer);
function GetPayAmount : Real; virtual;
function GetTime : Integer;
end;
PSalaried = ^TSalaried;
TSalaried = object(TEmployee);
function GetPayAmount : Real; virtual;
end;
PCommissioned = ^TCommissioned;
TCommissioned = object(Salaried);
Commission : Real;
SalesAmount : Real;
constructor Init (AName, ATitle: String;
ARate, ACommission, ASalesAmount: Real);
function GetPayAmount : Real; virtual;
end;
А ниже приводится пример для насекомых, дополненный вирту-
альными методами.
tyрe
TWinged = object(Insect)
constructor Init (AX, AY : Integer)
рrocedure Show; virtual;
рrocedure Hide; virtual;
end;
tyрe
TBee = object(TWinged)
constructor Init (AX, AY : Integer)
рrocedure Show; virtual;
рrocedure Hide; virtual;
end;
Прежде всего обратите внимание, что метод MoveTo, показанный
для типа TBee, теперь удален из его определения. Теперь типу TBee
больше нет нужды переопределять метод MoveTo типа TWinged с по-
мощью немодифицируемой копии, компилируемой в его собственной об-
ласти действия. Вместо этого, MoveTo теперь может наследоваться
от TWinged со всеми вложенными в MoveTo вызовами, которые, одна-
ко, будут вызывать методы из TBee, а не из TWinged, как это про-
исходило в полностью статической иерархии объектов.
Отметьте также новое зарезервированное слово constructor
(конструктор), заменившее зарезервированное слово рrocedure для
TWinged.Init и TBee.Init. Конструктор является специальным типом
процедуры, которая выполняет некоторую установочную работу для
механизма виртуальных методов. Более того, конструктор должен вы-
зываться перед вызовом любого виртуального метода. Вызов вирту-
ального метода без предварительного вызова конструктора может
привести к блокированию системы, а у компилятора нет способа про-
верить порядок вызова методов.
Каждый тип объекта, имеющий виртуальные методы, обязан иметь
конструктор.
Предупреждение: Конструктор должен вызываться перед вызовом
любого другого виртуального метода. Вызов виртуального метода без
предыдущего обращения к конструктору может вызвать блокировку
системы, и компилятор не сможет проверить порядок, в котором вы-
зываются методы.
Примечание: Для конструкторов объекта мы предлагает
использовать идентификатор Init.
Каждый отдельный экземпляр объекта должен инициализироваться
отдельным вызовом конструктора. Недостаточно инициализировать
один экземпляр объекта и затем присваивать этот экземпляр другим.
Другие экземпляры, даже если они могут содержать правильные дан-
ные, не будут инициализированы оператором присваивания и заблоки-
руют систему при любых вызовах их виртуальных методов. Например:
var
FBee, GBee: Bee; { создать два экземпляра Bee }
begin
FBee.Init(5, 9) { вызов конструктора для FBee }
GBee := FBee; { Gbee недопустим! }
end;
Что же именно создает конструктор? Каждый тип объекта содер-
жит нечто, называемое таблицей виртуального метода (ТВМ) в сег-
менте данных. ТВМ содержит размер типа объекта и для каждого вир-
туального метода указатель на код, выполняющий данный метод.
Конструктор устанавливает связь между вызывающей его реализацией
объекта и ТВМ типа объекта.
Важно помнить, что имеется только одна ТВМ для каждого типа
объекта. Отдельные экземпляры типа объекта (т.е. переменные этого
типа) содержат только соединение с ТВМ, но не саму ТВМ. Конструк-
тор устанавливает значение этого соединения в ТВМ. Именно благо-
даря этому вы нигде не можете запустить выполнение перед вызовом
конструктора.
Проверка диапазонов при вызове виртуальных методов
В процессе разработки программы вам, возможно, захочется по-
высить меры безопасности, которая снижается из-за вызовов вирту-
альных методов Borland Pascal. Если директива $R находится во
включенном состоянии, {$R+}, то все вызовы виртуальных методов
будут проверяться на состояние инициализации для выполняющих вы-
зовы реализаций. Если выполняющая вызов реализация еще не была
инициализирована конструктором, то произойдет ошибка проверки ди-
апазона исполняющей системы.
Примечание: Состоянием по умолчанию является {$R-}.
После того, как вы хорошенько перетрясли программу и удосто-
верились, что отсутствуют вызовы методов из неинициализированных
реализаций, вы можете до некоторой степени ускорить выполнение
программы путем переключения директивы $R в пассивное состояние.
После этого проверка вызовов методов из неинициализированных реа-
лизаций осуществляться не будет, что оставляет вероятность блоки-
ровки системы, если будет выявлена такая ошибка.
Виртуальный однажды - виртуальный всегда
Вы уже вероятно обратили внимание, что как TWinged, так и
TBee содержат методы, называемые Show и Hide. Все заголовки мето-
дов для Show и Hide объявлены виртуальными и снабжены зарезерви-
рованным словом virtual. Как только родительский тип объекта объ-
являет метод виртуальным, все его потомки также должны объявить
этот метод виртуальным. Другими словами, статический метод никог-
да не сможет переопределить виртуальный метод. Если вы попытае-
тесь сделать это, то компилятор выдаст сообщение об ошибке.
Также следует помнить, что после того, как метод стал вирту-
альным, его заголовок не может изменяться в объектах более низко-
го уровня иерархии. Вы можете представлять себе каждое определе-
ние виртуального метода как ворота для всех их. Исходя из этих
соображений, заголовки всех реализаций одного и того же виртуаль-
ного метода должны быть идентичными, включая число параметров и
их типы. Это не относится к статическим методам: статический ме-
тод, переопределяющий другой, может иметь отличное число парамет-
ров и типы этих параметров, в зависимости от необходимости.
Это целый новый мир.
Расширяемость объекта
Важным замечанием, касающимся модулей типа WORKERS.PAS, яв-
ляется то, что типы объектов и методы, определенные в модуле, мо-
гут поставляться пользователю в форме .TPU, .TPW или .TPP т.е. в
форме, способной к непосредственной компоновке, без исходного ко-
да. (Нужно просмотреть только листинг интерфейсной части модуля.)
Используя полиморфические объекты и виртуальные методы, пользова-
тель файла .TPU, .TPW или .TPP сможет добавлять характеристики
для приспособления модуля к своим нуждам.
Новое понятие о добавлении функциональных характеристик в
программу без предоставления ее исходного кода называется способ-
ностью к расширению. Способность к расширению является естествен-
ным следствием наследования: вы наследуете все, чем обладают по-
рождающие типы, а затем добавляете новые нужные вам возможности.
Позднее связывание позволяет, чтобы новое связывалось со старым
во время выполнения программы, благодаря чему расширение сущест-
вующего кода выглядит "бесшовным" и стоит вам в части выполнения
не более, чем быстрое путешествие по таблице виртуального метода.
Статические методы или виртуальные методы?
В общем случае, вам следует делать методы виртуальным. Ис-
пользуйте статические методы только в том случае, если вы хотите
получить оптимальную эффективность скорости выполнения и исполь-
зования памяти. Однако в этом случае, как вы видели, вы теряете
возможности расширения.
Предположим, что вы описываете объект с именем Ancestor и
внутри этого объекта вы описываете метод с именем Action. Как вы
определяете, каким должен быть метод, виртуальным или статичес-
ким? Здесь приводится правило большого пальца: сделайте метод
Action виртуальным, если имеется вероятность, что будущие наслед-
ники объекта Ancestor будут переопределять Action, а вы хотите,
чтобы будущий код был доступен Ancestor.
С другой стороны, помните, что если у объекта имеются любые
виртуальные методы, то для этого объекта в сегменте данных будет
создана таблица виртуальных методов (ТВМ) и каждый экземпляр это-
го объекта будет иметь связь с ТВМ. Каждый вызов виртуального ме-
тода должен проходить через ТВМ, тогда как статические методы вы-
зываются непосредственно. Хотя просмотр ТВМ является весьма эф-
фективным, вызов статического метода все равно остается немного
более быстрым, чем вызов виртуального. И если в вашем объекте нет
виртуальных методов, то и ТВМ отсутствует в сегменте данных и
(что более важно) в каждом экземпляре объекта отсутствуют связи с
ТВМ.
Дополнительная скорость и эффективное использование памяти
для статических методов должно уравновешиваться гибкостью, кото-
рую допускают виртуальные методы: вы можете расширить имеющийся
код спустя много времени после его компиляции. Помните, что поль-
зователь вашего типа объекта может рассматривать пути его исполь-
зования, которые вам и не снились, что, в конечном счете, имеет
основное значение.
Динамические объекты
Все приведенные до сих пор объекты имели статические реали-
зации типов объектов, которым в объявлении var присваивались име-
на и которые размещались в сегменте данных или в стеке.
var
ASalaried: TSalaried;
Примечание: Использование здесь слова "статический" не
имеет отношения к статическим методам.
Объекты могут размещаться в динамической памяти и ими можно
манипулировать с помощью указателей, как и с тесно связанными с
ними типами записей, что всегда имело место в Паскале. Турбо Пас-
каль включает несколько мощных расширений для выполнения динами-
ческого размещения и удаления объектов более легкими и более эф-
фективными способами.
Объекты могут размещаться, как области памяти, на которые
ссылается указатель, с помощью процедуры New:
var
CurrentPay: Real;
P: ^TSalaried;
New(P);
Как и для типов записей, процедура New выделяет в динамичес-
кой памяти пространство, достаточное для размещения реализации
указателя базового типа и возвращает адрес этого пространства в
указателе.
Если динамический объект содержит виртуальные методы, то он
должен инициализироваться с помощью вызова конструктора перед
тем, как будет вызван любой из его методов:
P^.Init('Sara Adams', 'Account manager', 2400);
Затем вызовы методов могут происходить в обычном порядке, с
использованием имени указателя и ссылочного символа вместо имени
реализации, которое использовалось бы при обращении к статически
размещенному объекту:
CurrentPay := P^.GetPayAmount;
Размещение и инициализация с помощью процедуры New
Borland Pascal расширяет синтаксис процедуры New, что явля-
ется более компактным и более удобным средством выделения прост-
ранства для объекта в динамически распределяемой области памяти и
инициализации объекта с помощью только одной операции. Теперь
процедура New может вызываться с двумя параметрами: имя указателя
используется в качестве первого параметра, а вызов конструктора -
в качестве второго параметра:
New(P, Init('Sara Adams', 'Account manager', 2400));
Если для процедуры New используется расширенный синтаксис,
то конструктор Init действительно выполняет динамическое размеще-
ние, используя специальный код входа, сгенерированного как часть
компиляции конструктора. Имя реализации не может предшествовать
Init, т.к. в то время, когда процедура New вызвана, реализация,
инициализируемая с помощью Init, еще не существует. Компилятор
идентифицирует правильный вызываемый метод Init посредством типа
указателя, пересылаемого в качестве первого параметра.
Процедура New также была расширена для возможности использо-
вания ее как функции, которая возвращает значение указателя. По-
сылаемый New параметр является типом указателя на объект, а не
самой переменной-указателем:
tyрe
PSalaried = ^TSalaried;
var
P: PSalaried;
P := New(PSalaried);
Обратите внимание, что в данной версии функциональная форма
расширения процедуры New применима ко всем типам данных, а не
только к типам объектов.
Функциональная форма New, как и процедурная форма, также мо-
жет воспринимать конструктор объектного типа в качестве второго
параметра:
P := New(PSalaried, Init('Sara Adams',
'Account manager', 2400));
В Borland Pascal осуществлено также параллельное расширение
процедуры Disрose, это подробно обсуждается в следующем разделе.
Примечание: Новая стандартная процедура Fail поможет
вам в конструкторах выполнить восстановление при ошибке
(см. Главу 9 в "Руководстве по языку").
Удаление динамических объектов
Также, как и обычные записи Паскаля, размещаемые в динами-
чески распределяемой области памяти, объекты могут удаляться про-
цедурой Disрose, если они больше не нужны:
Disрose (P);
Однако, при избавлении от ненужного объекта может понадо-
биться нечто большее, чем простое освобождение занимаемой им ди-
намической памяти. Объект может содержать указатели на динамичес-
кие структуры или объекты, которые нужно освободить или очистить
в определенном порядке, особенно если вы оперируете сложной дина-
мической структурой данных. Что бы ни нужно было сделать для
очистки динамического объекта в каком-либо порядке, это все долж-
но быть объединено в один метод таким образом, чтобы объект мог
быть уничтожен с помощью одного вызова метода:
MyComрlexObject.Done;
Метод Done должен инкапсулировать все детали очистки своего
объекта, а также всех структур данных и вложенных объектов.
Примечание: Мы советуем использовать для удаления ме-
тодов, работающих с объектами, которые более не нужны, ис-
пользовать идентификатор Done.
Допустимо и часто бывает полезно определять несколько мето-
дов очистки для данного типа объекта. В зависимости от того, как
они размещены или используются, или в зависимости от состояния и
режима объекта на момент очистки, сложные объекты могут потребо-
вать очистки несколькими различными путями
Деструкторы
Borland Pascal предоставляет специальный тип метода, называ-
емый "сборщиком мусора" или деструктором, для очистки и удаления
динамически размещенного объекта. Деструктор объединяет шаг уда-
ления объекта с какими-либо другими действиями или задачами, не-
обходимыми для данного типа объекта. Для единственного типа объ-
екта можно определить несколько деструкторов.
Деструктор определяется совместно со всеми другими методами
объекта в определении типа объекта:
tyрe
TEmployee = object
Name: string[25];
Title: string[25];
Rate: Real;
constructor Init(AName, ATitle: String; ARate: Real);
destructor Done; virtual;
function GetName: String;
function GetTitle: String;
function GetRate: Rate; virtual;
function GetPayAmount: Real; virtual;
end;
Деструкторы можно наследовать, и они могут быть либо стати-
ческими, либо виртуальными. Поскольку различные программы завер-
шения обычно требуют различные типы объектов, мы рекомендуем,
чтобы деструкторы всегда были виртуальными, благодаря чему для
каждого типа объекта будет выполнен правильный деструктор.
Запомните, что зарезервированное слово destructor не требу-
ется указывать для каждого метода очистки, даже если определение
типа объекта содержит виртуальные методы. Деструкторы в действи-
тельности работают только с динамически размещенными объектами.
При очистке динамически размещенного объекта, деструктор осущест-
вляет специальные функции: он гарантирует, что в динамически
распределяемой области памяти всегда будет освобождаться правиль-
ное число байтов. Не может быть никаких опасений по поводу ис-
пользования деструктора применительно к статически размещенным
объектам; фактически, не передавая типа объекта деструктору, вы
лишаете объект данного типа полных преимуществ управления динами-
ческой памятью в Borland Pascal.
Деструкторы в действительности становятся самими собою тог-
да, когда должны очищаться полиморфические объекты и когда должна
освобождаться занимаемая ими память. Полиморфические объекты -
это те объекты, которые были присвоены родительскому типу благо-
даря правилам совместимости расширенных типов Borland Pascal. Эк-
земпляр объекта типа THourly, присвоенный переменной типа
TEmployee, является примером полиморфического объекта. Эти прави-
ла также могут быть применены к объектам; указатель на THourly
может свободно быть присвоен указателю на TEmployee, а указуемый
этим указателем объект опять же будет полиморфическим объектом.
Термин "полиморфический" является подходящим, так как код,
обрабатывающий объект, не знает точно во время компиляции, какой
тип объекта ему придется в конце концов обработать. Единственное,
что он знает, это то, что этот объект принадлежит иерархии объек-
тов, являющихся потомками указанного типа объекта.
Очевидно, что размеры типов объектов отличаются. Поэтому,
когда наступает время очистки размещенного в динамической памяти
полиморфического объекта, то как же Disрose узнает, сколько байт
динамического пространства нужно освобождать? Во время компиляции
из полиморфического объекта нельзя извлечь никакой информации от-
носительно размера объекта.
Деструктор разрешает эту головоломку путем обращения к тому
месту, где эта информация записана: в ТВМ переменных реализаций.
В каждой ТВМ типа объекта содержится размер в байтах данного типа
объекта. Таблица виртуальных методов любого объекта доступна пос-
редством скрытого параметра Self, посылаемого методу при вызове
метода. Деструктор является всего лишь разновидностью метода и
поэтому, когда объект вызывает его, деструктор получает копию
Self через стек. Таким образом, если объект является полиморфи-
ческим во время компиляции, он никогда не будет полиморфическим
во время выполнения благодаря позднему связыванию.
Для выполнения этого освобождения памяти при позднем связы-
вании деструктор нужно вызывать, как часть расширенного синтакси-
са процедуры Disрose:
Disрose(P, Done);
(Вызов деструктора вне процедуры Disрose вообще не выполняет
никакого освобождения памяти.) Здесь происходит на самом деле то,
что сборщик мусора объекта, на который указывает P, выполняется
как обычный метод. Однако, как только последнее действие выполне-
но, деструктор ищет размер реализации своего типа в ТВМ и пересы-
лает размер процедуре Disрose. Процедура Disрose завершает про-
цесс путем удаления правильного числа байт пространства динами-
ческой памяти, которое (пространство) до этого относилось к P^.
Число освобождаемых байт будет правильным независимо от того,
указывал ли P на экземпляр типа TSalaried, или он указывал на
один из дочерних типов типа TSalaried, например, на
TCommissioned.
Заметьте, что сам по себе метод деструктора может быть пуст
и выполнять только эту функцию:
destructor AnObject.Done;
begin
end;
То, что делается полезного в этом деструкторе, не является
достоянием его тела, однако при этом компилятором генерируется
код эпилога в ответ на зарезервированное слово destructor. Это
напоминает модуль, который ничего не экспортирует, но который
осуществляет некоторые невидимые действия за счет выполнения сво-
ей секции инициализации перед стартом программы. Все действия
происходят "за кулисами".
Пример размещения динамического объекта
Последний пример программы даст вам возможность приобрести
некоторые навыки в использовании размещенных в динамической памя-
ти объектов, включая использование для удаления объекта деструк-
тора. Программа показывает, как в динамической памяти может быть
создан связанный список рабочих объектов и как он за ненадоб-
ностью может быть очищен при помощи деструктора.
Построение связанного списка объектов требует, чтобы каждый
объект содержал указатель на следующий объект списка. Тип
TEmployee не содержит таких указателей. Простым выходом из этой
ситуации было бы добавление указателя в TEmployee , благодаря че-
му можно быть уверенным, что все потомки TEmployee наследуют та-
кой указатель. Однако, добавление чего-либо в TEmployee требует
от вас наличия исходного кода, а как говорилось ранее, одним из
преимуществ объектно-ориентированного программирования является
возможность расширения объектов без необходимости их перекомпиля-
ции.
Решение, которое не требует никаких изменений TEmployee,
создает новый тип объекта, не являющийся потомком TEmployee. Тип
StaffList представляет собой очень простой объект, целью которого
является создание заголовков для объектов типа TEmployee. Так как
TEmployee не содержит никаких указателей на следующий объект в
списке, то простой тип записи TNode осуществляет этот сервис.
TNode даже проще, чем StaffList в том, что TNode не является объ-
ектом, не содержит ни одного метода и не имеет никаких данных, за
исключением указателя на тип TEmployee и указателя на следующий
узел списка.
TStaffList содержит метод, который позволяет ему добавлять
нового рабочего в связанный список записей TNode путем внесение
нового экземпляра TNode непосредственно после самого себя в ка-
честве указуемого с помощью указателя поля TNodes. Метод Add при-
нимает указатель на объект типа TEmployee, но не сам объект.
Из-за расширенной совместимости типов Турбо Паскаля указатели на
любого потомка типа TEmployee также должны передаваться в
TList.Add в параметре Item.
Программа WorkList описывает статическую переменную Staff
типа TStaffList и строит связанный список из пяти узлов. Каждый
узел указывает на отдельный рабочий объект, который является либо
TEmployee, либо одним из его потомков. Перед созданием каждого
динамического объекта и после того, как объект создан, возвращает
число байт свободной динамической памяти. Наконец, полная струк-
тура, включающая пять записей TNode и пять объектов типа
TEmployee, очищается и удаляется из динамической памяти с помощью
одного вызова деструктора статического объекта Staff типа
TStaffList.
|
List | Node Node Node
+-------+ +----+----+ +----+----+ +----+----+
| | | | | | | | | | | |
| O--+----> | O | O-+---> | O | O-+----> | O | O-+---+
| | | | | | | | | | | | | | | |
+-------+ +--+-+----+ +--+-+----+ +--+-+----+ |
| | | | --+--
v v v ---
| +-------------+ +-------------+ +-------------+ -
| Name | | Name | | Name |
| +-------------| +-------------| +-------------|
| Title | | Title | | Title |
| +-------------| +-------------| +-------------|
| Rate | | Rate | | Rate |
| +-------------| +-------------| +-------------|
| | | | | |
Сегмент | Динамически распределяемая область
данных памяти (динамический)
(статические|
объекты)
|
Рис. 9.2 Схема структур данных программы ListDemo.
Удаление сложной структуры данных из динамической памяти
Деструктор Staff.Done стоит того, чтобы рассмотреть его вни-
мательно. Уничтожение объекта TStaffList включает удаление трех
различных типов структур: полиморфических объектов рабочих струк-
тур в списке, записей TNode, поддерживающих список, и (если он
размещен в динамической памяти) объект TList, который озаглавли-
вает список. Весь процесс запускается путем единственного вызова
деструктора объекта TStaffList:
Staff.Done;
Код деструктора заслуживает более подробного изучения:
destructor StaffList.Done;
var
N: TNodePtr;
begin
while TNodes <> nil do
begin
N := TNodes;
Disрose(N^.Item, Done);
TNodes := N^.Next;
Disрose (N);
end;
end;
Список очищается начиная с "головы" списка с помощью алго-
ритма "из руки в руку", который до некоторой степени напоминает
дерганье за веревку воздушного змея: два указателя (указатель
TNodes внутри Staff и рабочий указатель N) изменяют свои ссылки в
списке, тогда как первый элемент списка удаляется. Вызов процеду-
ры Disрose освобождает память, занимаемую первым объектом
TEmployee в списке (Item^), затем TNodes продвигается на следую-
щую запись списка с помощью оператора TNodes := N^.Next, сама за-
пись TNode удаляется, и процесс продолжается до полного очищения
списка.
Важным моментом в деструкторе Done является способ, которым
удаляются из списка объекты TEmployee:
Disрose(N.Item, Done);
Здесь N.Item является первым объектом TEmployee в списке, а
вызываемый метод Done является деструктором этого объекта. Запом-
ните, что действительный тип N^.Item^ не обязательно является ти-
пом TEmployee, однако он может быть любым дочерним типом типа
TEmployee. Очищаемый объект является полиморфическим и поэтому
нельзя сделать никаких предположений относительно его действи-
тельного размера или точного его типа на этапе компиляции. В при-
веденном выше вызове Disрose, как только Done выполнит все содер-
жащиеся в нем операторы, "невидимый" код эпилога ищет размер реа-
лизации очищаемого объекта в ТВМ этого объекта. Метод Done пере-
дает размер процедуре Disрose, которая затем освобождает точное
количество динамической памяти, в действительности занимаемой по-
лиморфическим объектом.
Помните, что если должно освобождаться правильное количество
динамической памяти, то полиморфический объект должен очищаться
только посредством вызова передаваемого Disрose деструктора.
В примере программы Staff объявляется как статическая пере-
менная в сегменте данных. Staff мог бы столь же легко разместить-
ся в динамической памяти и "прикрепиться к реальному миру" пос-
редством указателя типа ListPtr. Если заголовок списка также яв-
ляется динамическим объектом, то удаление структуры можно осу-
ществить путем вызова деструктора, выполняющегося внутри Disрose:
var
Staff: TStaffListPtr;
begin
Disрose(Staff, Done);
.
.
.
Здесь процедура Disрose вызывает метод деструктора Done для
очистки структуры в динамической памяти. Затем, когда Done завер-
шается, Disрose освобождает память, на которую указывает Staff,
удаляя, как правило, из динамической памяти также и заголовок
списка.
Программа WORKLIST.PAS (находящаяся на вашем диске) исполь-
зует тот же модуль WORKERS.PAS, что и раньше Она создает объект
List, являющийся оглавлением связанного списка из пяти полиморфи-
ческих объектов, совместимых с TEmployee, а затем удаляет всю ди-
намическую структуру данных с помощью единственного вызова дест-
руктора Staff.Done.
Что же дальше?
Как и во всяком другом аспекте машинного программирования,
вы не преуспеете в объектно-ориентированном программировании, ес-
ли будете только читать о нем, но вы добъетесь результата, если
начнете программировать. Большинство людей, при первом столкнове-
нии с объектно-ориентированном программированием, начинают бормо-
тать с придыханием; "Я не могу постичь этого". "Ага!" приходит
позднее, ночью, когда целостная концепция является к нам в одно
прекрасное мгновение, и мы, побросав свои никчемные дела, исполь-
зуем это мгновение для обращения к богу. Как лицо женщины, возни-
кающее из чернильных пятен Роша, то, что до этого было смутным,
становится очевидным и затем легким.
Самое лучшее, что вы можете сделать в качестве первого шага
в объектно-ориентированном программировании, так это взять модуль
WORKERS.PAS (он находится на вашем диске) и расширить его. Как
только вы воскликните "Ага!", начинайте строить ориентированные
на объекты концепции в вашей повседневной программистской жизни.
Возьмите несколько имеющихся утилит, которые вы используете каж-
дый день, и переосмыслите их в ориентированных на объекты терми-
нах. Посмотрите критически на "овощное рагу" вашей библиотеки
процедур и попытайтесь найти в них объекты, затем перепишите про-
цедуры в объектной форме. Вы убедитесь, что библиотеки объектов
станет намного легче использовать в будущих проектах. Даже самые
незначительные ваши начальные инвестиции в программные усилия
станут навсегда излишними. У вас едва ли возникнет необходимость
переписывать объект с самого начала. Если он работает как надо,
то используйте его. Если объекту чего-либо не хватает, то рас-
ширьте его. Но если он работает хорошо, то нет смысла выбрасывать
из него что-либо.
Заключение
Объектно-ориентированное программирование является прямым
следствием усложнения современных приложений, усложнения, которое
часто заставляет многих программистов в отчаянии вскидывать вверх
руки. Наследование и инкапсуляция являются максимально эффектив-
ными средствами для управления сложностью. (Существует разница
между десятью тысячами насекомых, классифицированных по таксоно-
мической схеме, и десятью тысячами насекомых, жужжащих возле ва-
ших ушей.) Представляя собой значительно большее, чем просто
структурное программирование, объектно-ориентированное программи-
рование вносит рациональный порядок в структуру программного
обеспечения ЭВМ, что, как и таксономическая схема, устанавливает
порядок, не устанавливая пределов.
Добавьте сюда перспективы возможности расширения и повторно-
му использования существующего кода и все это начнет звучать нас-
только хорошо, что будет походить на правду. Вы думаете, что это
невозможно?
Но это же Borland Pascal! Слово "невозможно" в нем не опре-
делено.
|