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;
Конструктор является специальным типом процедуры, которая выполняет некоторую установочную работу для механизма виртуальных методов. Более того, конструктор должен вызываться перед вызовом любого виртуального метода. Вызов виртуального метода без предварительного вызова конструктора может привести к блокированию системы, а у компилятора нет способа проверить порядок вызова методов.
Каждый тип объекта, имеющий виртуальные методы, обязан иметь конструктор.
Предупреждение
Конструктор должен вызываться перед вызовом любого другого виртуального метода. Вызов виртуального метода без предыдущего обращения к конструктору может вызвать блокировку системы, и компилятор не сможет проверить порядок, в котором вызываются методы.
Примечание
Для конструкторов объекта предлагается использовать идентификатор Init.
Каждый отдельный экземпляр объекта должен инициализироваться отдельным вызовом конструктора. Недостаточно инициализировать один экземпляр объекта и затем присваивать этот экземпляр другим. Другие экземпляры, даже если они могут содержать правильные данные, не будут инициализированы оператором присваивания и заблокируют систему при любых вызовах их виртуальных методов. Например:
var
FBee, GBee: Bee; { создать два экземпляра Bee }
begin
FBee.Init(5, 9) { вызов конструктора для FBee }
GBee := FBee; { Gbee недопустим! }
end;
Что же именно создает конструктор? Каждый тип объекта содержит нечто, называемое таблицей виртуального метода (ТВМ) в сегменте данных. ТВМ содержит размер типа объекта и для каждого виртуального метода указатель на код, выполняющий данный метод. Конструктор устанавливает связь между вызывающей его реализацией объекта и ТВМ типа объекта.
Важно помнить, что имеется только одна ТВМ для каждого типа объекта. Отдельные экземпляры типа объекта (т. е. переменные этого типа) содержат только соединение с ТВМ, но не саму ТВМ. Конструктор устанавливает значение этого соединения в ТВМ. Именно благодаря этому нигде нельзя запустить выполнение перед вызовом конструктора.
5. Поля данных объекта и формальные параметры метода
Выводом из того факта, что методы и их объекты разделяют общую область действия, является то, что формальные параметры метода не могут быть идентичными любому из полей данных объекта. Это является не каким-то новым ограничением, налагаемым объектно-ориентированным программированием, а, скорее, теми же самыми старыми правилами области действия, которые Pascal имел всегда. Это то же самое, что и запрет для формальных параметров процедуры быть идентичными локальным переменным этой процедуры:
procedure CrunchIt(Crunchee: MyDataRec, Crunchby,
ErrorCode: integer);
var
A, B: char;
ErrorCode: integer;
begin
.
.
.
Локальные переменные процедуры и ее формальные параметры совместно используют общую область действия и поэтому не могут быть идентичными. Будет получено сообщение «Error 4: Duplicate identifier» (Ошибка 4; Повторение идентификатора), если попытаться компилировать что-либо подобное, та же ошибка возникает при попытке присвоить формальному параметру метода имени поля объекта, которому даннёый метод принадлежит.
Обстоятельства несколько отличаются, так как помещение заголовка процедуры внутрь структуры данных является намеком на новшество в Turbo Pascal, но основные принципы области действия Pascal не изменились.
ЛЕКЦИЯ № 13. Совместимость типов объектов
Объединение в объекте кода и данных называется инкапсуляцией. В принципе, возможно предоставить достаточное количество методов, благодаря которым пользователь объекта никогда не будет обращаться к полям объекта непосредственно. Некоторые другие объектно-ориентированные языки, например 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;
Здесь присутствуют только четыре поля данных: Name, Title, Rate и Time. Методы GetName и GetTitle выводят фамилию работающего и его должность соответственно. Метод GetPayAmount использует Rate, а в случае работающего THourly и Time для вычисления суммы выплат работающему. Здесь уже нет необходимости обращаться непосредственно к этим полям данных.
Предположив существование экземпляра AnHourly типа THourly, мы могли бы использовать набор методов для манипулирования полями данных AnHourly например:
with AnHourly do
begin
Init (Aleksandr Petrov, Fork lift operator' 12.95, 62);
{Выводит на экран фамилию, должность и сумму выплат}
Show;
end;
Следует обратить внимание, что доступ к полям объекта осуществляется не иначе, как только с помощью методов этого объекта.
К сожалению, стандартный Pascal не предоставляет никаких возможностей для создания гибких процедур, позволяющих работать с абсолютно разными типами данных. Объектно-ориентированное программирование решает эту проблему с помощью наследования: если определен порожденный тип, то методы порождающего типа наследуются, однако при желании они могут переопределяться. Для переопределения наследуемого метода попросту описывается новый метод с тем же именем, что и наследуемый метод, но с другим телом и (при необходимости) с другим множеством параметров.
Определим дочерний по отношению к TEmployee тип, представляющий работника, которому платится часовая ставка, в следующем примере:
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;
Человек, которому платится часовая ставка, является работающим: он обладает всем тем, что используется для определения объекта TEmployee (фамилией, должностью, ставкой), и лишь количество получаемых почасовиком денег зависит от того, сколько часов он отработал за период, подлежащий оплате. Таким образом, для THourly требуется еще и поле времени Time.
Так как THourly определяет новое поле Time, его инициализация требует нового метода Init, который инициализирует и время, и наследованные поля. Вместо того, чтобы непосредственно присвоить значения наследованным полям, таким как Name, Title и Rate, почему бы не использовать вновь метод инициализации объекта TEmployee (иллюстрируемый первым оператором THourly Init).
Вызов метода, который переопределяется, не является лучшим стилем. В общем случае возможно, что TEmployee.Init выполняет важную, однако скрытую инициализацию.
Вызывая переопределяемый метод, необходимо быть уверенным в том, что порожденный тип объекта включает функциональность родителя. Кроме того, любое изменение в родительском методе автоматически оказывает влияние на все порожденные.
После вызова TEmployee.Init, THourly.Init может затем выполнить свою собственную инициализацию, которая в этом случае состоит только в присвоении значения, переданного в ATime.
Другим примером переопределяемого метода является функция THourly.GetPayAmount, вычисляющая сумму выплат работающему на почасовой ставке. В действительности, каждый тип объекта TEmployee имеет свой метод GetPayAmount, так как тип работающего зависит от того, как производится расчет. Метод THourly.GetPayAmount должен учитывать, сколько часов работал сотрудник, были ли сверхурочные работы, каков коэффициент увеличения за сверхурочные работы и т. д.
Метод TSalaried. GetPayAmount должен лишь делить ставку работающего на число выплат в каждом году (в нашем примере).
unit Workers;
interface
const
PayPeriods = 26; {в год}
OvertimeThreshold = 80; {за каждый период оплаты}
OvertimeFactor =1.5; {увеличение против обычной оплаты}
type
TEmployee = object