Borland Pascal. Руководство пользователя

         

Что же дальше?


Как и во всяком другом аспекте машинного программирования, вы не преуспеете в объектно-ориентированном программировании, ес- ли будете только читать о нем, но вы добъетесь результата, если начнете программировать. Большинство людей, при первом столкнове- нии с объектно-ориентированном программированием, начинают бормо- тать с придыханием; "Я не могу постичь этого". "Ага!" приходит позднее, ночью, когда целостная концепция является к нам в одно прекрасное мгновение, и мы, побросав свои никчемные дела, исполь- зуем это мгновение для обращения к богу. Как лицо женщины, возни- кающее из чернильных пятен Роша, то, что до этого было смутным, становится очевидным и затем легким.

Самое лучшее, что вы можете сделать в качестве первого шага в объектно-ориентированном программировании, так это взять модуль WORKERS.PAS (он находится на вашем диске) и расширить его. Как только вы воскликните "Ага!", начинайте строить ориентированные на объекты концепции в вашей повседневной программистской жизни. Возьмите несколько имеющихся утилит, которые вы используете каж- дый день, и переосмыслите их в ориентированных на объекты терми- нах. Посмотрите критически на "овощное рагу" вашей библиотеки процедур и попытайтесь найти в них объекты, затем перепишите про- цедуры в объектной форме. Вы убедитесь, что библиотеки объектов станет намного легче использовать в будущих проектах. Даже самые незначительные ваши начальные инвестиции в программные усилия станут навсегда излишними. У вас едва ли возникнет необходимость переписывать объект с самого начала. Если он работает как надо, то используйте его. Если объекту чего-либо не хватает, то рас- ширьте его. Но если он работает хорошо, то нет смысла выбрасывать из него что-либо.



Деструкторы


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. Это напоминает модуль, который ничего не экспортирует, но который осуществляет некоторые невидимые действия за счет выполнения сво- ей секции инициализации перед стартом программы. Все действия происходят "за кулисами".




Динамические объекты


Все приведенные до сих пор объекты имели статические реали- зации типов объектов, которым в объявлении var присваивались име- на и которые размещались в сегменте данных или в стеке.

var ASalaried: TSalaried;

Примечание: Использование здесь слова "статический" не имеет отношения к статическим методам.

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

Объекты могут размещаться, как области памяти, на которые ссылается указатель, с помощью процедуры New:

var CurrentPay: Real; P: ^TSalaried;

New(P);

Как и для типов записей, процедура New выделяет в динамичес- кой памяти пространство, достаточное для размещения реализации указателя базового типа и возвращает адрес этого пространства в указателе.

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

P^.Init('Sara Adams', 'Account manager', 2400);

Затем вызовы методов могут происходить в обычном порядке, с использованием имени указателя и ссылочного символа вместо имени реализации, которое использовалось бы при обращении к статически размещенному объекту:

CurrentPay := P^.GetPayAmount;



Объектно-ориентированное программирование


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

1. Инкапсуляция. Комбинирование записей с процедурами и функциями, манипулирующими полями этих записей, формирует новый тип данных - объект.

2. Наследование. Определение объекта и его дальнейшее ис- пользование для построения иерархии порожденных объектов с возможностью для каждого порожденного объекта, относя- щегося к иерархии, доступа к коду и данным всех порождаю- щих объектов.

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

Языковые расширения Borland Pascal предоставляют вам все средства объектно-ориентированного программирования: большую структурированность и модульность, большую абстрактность и встро- енную непосредственно в язык возможность повторного использова- ния. Все эти характеристики соответствуют коду, который является более структурированным, более гибким и более легким для обслужи- вания.

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


Дадим хороший совет тому, кто уже имел дело с объектно-ори- ентированным программированием на других языках. Оставьте в сто- роне ваши прежние впечатления об объектно-ориентированном прог- раммировании и изучайте объектно-ориентированные характеристики Borland Pascal в их собственных терминах. Объектно-ориентирован- ное программирование не является единственным путем, оно предс- тавляет собой континуум идей. По своей объектной философии Borland Pascal больше напоминает С++, чем Smalltalk. Smalltalk является интерпретатором, тогда как Borland Pascal с самого нача- ла был чистым компилятором реального кода. Компилятор реального кода выполняет работу иначе (и значительно быстрее), чем интерп- ретатор. Borland Pascal был сконструирован, как инструмент разра- ботки продуктов, а не как инструмент исследования.
Для тех, кто не имеет об этом ни малейшего понятия, мы не будем подробно объяснять, что такое объектно-ориентированное программирование. В этот вопрос и так уже внесено достаточно пу- таницы. Поэтому забудьте о том, что люди говорили вам об объектно -ориентированное программировании. Наилучший способ (и, фактичес- ки, единственный) изучить что-либо полезное об объектно-ориенти- рованное программировании - это сделать то, что вы уже почти сде- лали: сесть и попытаться узнать все самостоятельно.

Хорошая и плохая техника программирования


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

Примечание: Borland Pascal позволяет вам делать поля объекта и его методы частными. Подробнее об этом рассказы- вается ниже.

Итак, как же обращаться к полям объекта? Как читать их и как присваивать им значения?

Примечание: Поля данных объекта - это то, что объект "знает", а методы объекта - это то, что объект "делает".

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



Инкапсуляция


Объединение в объекте кода и данных называется инкапсуляци- ей. Возможно вы сможете предоставить достаточное количество мето- дов, благодаря чему пользователь объекта никогда не будет обра- щаться к полям объекта непосредственно. Некоторые другие объектно -ориентированные языки, например 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;

Обратите внимание, что доступ к полям объекта осуществляется не иначе, как только с помощью методов этого объекта.



Экземпляры объектных типов


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

type PHourly = ^THourly; var StatHourly: THourly; { готово } DynaHourly: PHourly; { перед использованием память должна выделяться с помощью New }



Методы


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

TEmployee = object Name: string[25]; Title: string[25]; Rate: Real; end;

Большинство программистов использовали бы оператор with для присвоения полям Name, Title и Rate начальных значений:

var MyEmployee: Employee; with MyEmployee do begin Name := 'Sanderson, Arthur'; Title := 'Word processor'; Rate := 9.45; end;

Это тоже неплохо, но здесь мы жестко ограничены одной специ- фическим экземпляром записи - MyEmployee. Если потребуется иници- ализировать более одной записи типа Employee, то вам придется ис- пользовать большее число операторов with, которые выполняют в точности те же действия. Следующим естественным шагом является создание инициализирующей процедуры, которая обобщает оператор with применительно к любому экземпляру типа TEmployee, пересылае- мой в качестве параметра:

procedure InitTEmployee(var Worker: TEmployee; AName, ATitle: String; ARate: Real); begin with Worker do begin Name := NewName ; Title := NewTitle; Rate := NewRate; end; end;

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

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

Такой путь имеется и называется методом. Метод - это проце- дура или функция, объединенная с данным типом столь тесно, что метод является как бы окруженным невидимым оператором with, что делает экземпляр данного типа доступными изнутри для метода. Оп- ределение типа включает заголовок метода. Полное определение ме- тода квалифицируется в имени типа. Тип объекта и метод объекта являются двумя лицами этой новой разновидности структуры, именуе- мой методом.


type TEmployee = object Name, Title: string[25]; Rate: Real; procedure Init (NewName, NewTitle: string[25]; NewRate: Real); end;

procedure TEmployee.Init (NewName, NewTitle: string[25]; NewRate: Real); begin Name := NewName ; { Поле Name объекта TEmployee } Title := NewTitle; { Поле Tutle объекта TEmployee } Rate := NewRate; { Поле Rate объекта TEmployee } end;

Теперь для инициализации экземпляра типа TEmployee вы просто вызываете его метод, словно метод является полем записи, что име- ет вполне реальный смысл:

var AnEmployee: TEmployee; AnEmployee.Init('Sara Adams, Account manager, 15000'); {пpосто, не так ли?}


Методы: никакого ухудшения


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

Замечание по поводу абстрактности данных: Имеется громадное преимущество в возможности полностью отсоединить THourly от гло- бальных ссылок. Если ничто вне объекта не "знает" о представлении его внутренних данных, то программист, контролирующий объект, мо- жет изменять детали внутреннего представления данных до тех пор, пока не изменится заголовок метода.

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



Наследование


Целью науки является описание взаимодействий во вселенной. Большая часть работы в науке при продвижении к цели заключается просто в построении генеалогических деревьев. Когда энтомолог возвращается с Амазонки с неизвестным ранее насекомым в банке, то главной его заботой является определение, где это насекомое рас- полагается в гигантской схеме, на которой собраны научные назва- ния всех других насекомых. Аналогичные схемы составлены для рас- тений, рыб, млекопитающих, рептилий, химических элементов, эле- ментарных частиц и космических галактик. Все они выглядят как ге- неалогические деревья: с единой всеобщей категорией в вершине и все увеличивающимися число категорий, которые лежат ниже этой единственной категории, и разворачиваются веером по мере прибли- жения к границам разнообразия.

Внутри категории "насекомые" имеется два подразделения: на- секомые с видимыми крыльями и насекомые со спрятанными крыльями или вообще бескрылые. Среди крылатых имеется большее число кате- горий; мотыльки, бабочки, мухи и т.д. Каждая категория содержит большое число подкатегорий, а ниже этих подкатегорий может иметь- ся даже еще большее число подкатегорий (см. Рис. 9.1).

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

Вопросами, которые задает ученый при попытке классификации некоторого животного или объекта, являются следующие. В чем этот объект похож на другие объекты из общего класса? В чем он отлича- ется от других объектов? Каждый конкретный класс имеет множество свойств поведения и характеристик, определяющих этот класс. Уче- ный начинает с вершины конкретного генеалогического дерева и про- ходит по дочерним областям, задавая себе эти вопросы. Наивысший уровень самый общий, а вопросы самые простые, например, крылатое или бескрылое? Каждый последующий уровень является более специфи- ческим, чем предыдущий, и менее общим. В конце концов, ученый до- бирается до точки подсчета волосков на третьем сегменте задней ноги насекомого - что воистину специфично. (И скорее всего о нем следует сказать, что он не энтомолог.)


+-----------+ | насекомые | +-+-------+-+ +---------------+ | | | +----+-----+ +---+-------+ | крылатые | | бескрылые | ++---+---+-+ +-----------+ +--------+ | +-------+ +---+------++----+----++-----+----+ | мотыльки бабочки мухи | +----------++---------++----------+

Рис. 9.1 Таксономическая схема насекомых.

Важно помнить то, что если характеристика однажды определе- на, то все категории, расположенные ниже данного определения, со- держат эту характеристику. Таким образом, как только вы определи- ли насекомое, как члена отряда diptera (мухи), то вам не следует отмечать снова, что у мух имеется одна пара крыльев. Разновид- ность насекомых, которую мы зовем мухи, наследует эту характерис- тику от своего отряда.

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


Наследование статических методов


Все показанные до сих пор методы, связанные с типами объек- тов 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.

Если в типе объекта не определен метод с таким именем, то компилятор поднимается выше к непосредственному родительскому ти- пу в поисках метода с указанным именем. Если метод с таким именем найден, то адрес родительского метода замещает имя в исходном ко- де дочернего метода. Если метод с таким именем не найден, то ком- пилятор продолжает продвигаться вверх по родительским объектам в поисках метода. Если компилятор наталкивается на самый первый (высший) тип объекта, то он выдает сообщение об ошибке, указываю- щее, что ни одного такого метода не определено.

Однако, если статический наследуемый метод найден и исполь- зуется, то вы должны помнить, что вызываемый метод является в точности таким, как он определен и компилирован для родительского типа. Если родительский метод вызывает другие методы, то вызывае- мые методы будут также родительскими методами, даже если дочерний объект содержит методы, которые переопределяют родительские.


Объекты


Посмотрите вокруг себя... и вы обнаружите яблоко, которое вы купили к завтраку. Допустим, что вы намерены описать яблоко в терминах программирования. Первое, что вы, возможно, попытаетесь сделать, так это рассмотреть его по частям; пусть S представляет площадь кожуры, J представляет содержащийся в яблоке объем жидко- го сока, F представляет вес фрукта внутри кожуры, D - число семе- чек...

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

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

Примечание: Объекты содержат вместе все свои характе- ристики и особенности поведения.

Яблоко можно разрезать на части, но как только оно будет разрезано, оно больше не будет яблоком. Отношения частей к целому и взаимоотношения между частями становятся понятнее тогда, когда все содержится вместе в одной упаковке. Это называется инкапсуля- цией и является очень важным понятием. Немного позже мы к нему вернемся.

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



Объекты, экспортируемые модулями


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

Примечание: Экспортируемый - означает "определенный в интерфейсной части модуля".

Модули могут иметь свои собственные приватные (частные) оп- ределения типов объектов внутри выполняемой секции, и эти типы подвержены тем же ограничениям, как и всякие другие типы, опреде- ленные в секции реализации. Типы объектов, определенные в интер- фейсной части модуля, могут иметь дочерние типы объектов, опреде- ленные в секции реализации модуля. В том случае, когда модуль B использует модуль A, модуль B также может определять дочерние ти- пы любого типа объекта, экспортируемого модулем A.

Описанные ранее типы объектов и методы можно определить в модуле, как показано в программе WORKERS.PAS на дистрибутивном диске. Чтобы использовать типы объектов и методы, определенные в модуле Workers, вы можете просто использовать этот модуль в своей программе и описать экземпляр типа THourly в секции переменных программы:

program HourPrt;

uses WinCrt, Workers;

var AnHourly: THourly; . . .

Для создания и вывода фамилии pабочего-почасовика, его долж- ности и pазмеpа выплаты, пpедставленной пеpеменной AnHourly, вы просто вызываете методы AnHourly, используя следующий синтаксис:

AnHourlye.Init('Sara Adams', 'Account manager', 1400); { записывает в экземпляp THourly } { данные для Саpы Адамс: фамилию, } { должность и pазмеp выплаты. } AnHourly.Show;

Примечание: Объектами могут быть также типизированные константы.

Объекты, будучи очень схожими с записями, могут использо- ваться внутри оператора with. В этом случае указание имени объек- та, являющегося собственником методов, не является необходимым:

with AnHourly do begin Init('Sara Adams', 'Account manager', 1400); Show; end;

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



Объекты: наследующие записи


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

TEmployee = record Name: string[25]; Title: string[25]; Rate: Real; end;

Примечание: По соглашению все типы начинаются с буквы T. Вы также можете следовать этому правилу.

Здесь TEmployee является типом записи, т.е. шаблоном, ис- пользуемым компилятором для создания переменных типа запись. Пе- ременная типа TEmployee является экземпляром этого типа. Термин "экземпляр" будет вам нередко встречаться в Паскале. Он постоянно применяется теми, кто использует методы объектно-ориентированного программирования, поэтому будет хорошо, если вы начнете мыслить в терминах типов и экземпляров этих типов.

Вы можете оперировать с типом TEmployee двояко. Вы можете рассматривать поля Name, Title и Rate по отдельности, а когда о полях, как о работающих одновременно для описания конкретного рабочего, вы можете рассматривать их совокупность, как TEmployee.

Предположим, что на вашей фирме работает несколько типов рабочих. Одни из них имеют почасовую оплату, другие - оклад, третьи - тарифную ставку и так далее. Ваша программа должна учи- тывать все эти типы. Вы можете создать другой тип записи для каж- дого типа рабочего. Например, для получения данных о том, сколько должен получить рабочий с почасовой оплатой, нужно знать, сколько часов он отработал. Можно построить запись THourly вида:

THourly = record Name: string[25]; Title: string[25]; Rate: Real; end;

Вы можете также оказаться несколько догадливее и сохранить тип TEmployee путем создания поля типа TEmployee внутри типа THourly:

THourly = record Worker: TEmployee; Time: integer; end;

Такая конструкция работает, и программисты, работающие на Паскале, делают это постоянно. Единственное, чего этот метод не делает, так это то, что он заставляет вас думать о том, с чем вы работаете в вашем программном обеспечении. Вам следует задаться вопросом типа; "Чем почасовик отличается от дpугих pабочих?" От- вет прост: почасовик - это pабочий, котоpому платится за коли- чество часов pаботы. Продумайте снова первую часть предложения; почасовик - это pабочий...


Теперь вы поняли!

Запись для pаботника- почасовика hourly должна иметь все за- писи, котоpые имеются в записи employee. Tип THourly является до- черним типом для типа TEmployee. THourly наследует все, что при- надлежит TEmployee, и кроме того имеет кое-что новое, что делает THourly уникальным.

Этот процесс, с помощью которого один тип наследует характе- ристики другого типа, называется наследованием. Наследник называ- ется порожденным (дочерним) типом, а тип, которому наследует до- черний тип, называется порождающим (родительским) типом.

Ранее известные типы записей Паскаля не могут наследовать. Однако Borland Pascal расширяет язык Паскаль для поддержки насле- дования. Одним из этих расширений является новая категория струк- туры данных, связанная с записями, но значительно более мощная. Типы данных в этой новой категории определяются с помощью нового зарезервированного слова object. Тип объекта может быть определен как полный, самостоятельный тип в манере описания записей Паска- ля, но он может определяться и как потомок существующего типа объекта путем помещения порождающего (родительского) типа в скоб- ки после зарезервированного слова object.

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

type TEmployee = object Name: string[25]; Title: string[25]; Rate : Real; end;

THourly = object(TEmployee) Time : Integer; end;

Примечание: Обратите внимание, что здесь использование скобок означает наследование.

Здесь TEmployee является родительским типом, а THourly - до- черним типом. Как вы увидите чуть позднее, этот процесс может продолжаться неопределенно долго. Вы можете определить дочерний тип THourly, дочерний к типу THourly тип и т.д. Большая часть конструирования объектно-ориентированных прикладных программ сос- тоит в построении такой иерархии объектов, являющейся отражением генеалогического дерева объектов в приложениях.

Все возможные типы, наследующие тип TEmployee, называются дочерними типами типа TEmployee, тогда как THourly является не- посредственным дочерним типом типа TEmployee. Наоборот, TEmployee является непосредственным родителем типа THourly. Тип объекта (в точности как подкаталог в DOS) может иметь любое число непосредс- твенных дочерних типов, но в то же время только одного непосредс- твенного родителя.

Как показали данные определения, объекты тесно связаны с за- писями. Новое зарезервированное слово object является наиболее очевидным различием, но как вы увидите позднее, имеется большое число других различий, некоторые из которых довольно тонкие.

Например, поля Name, Title и Rate в типе TEmployee не указа- ны явно в типе THourly, но в любом случае тип THourly содержит их благодаря свойству наследования. Вы можете говорить о величине Name типа THourly в точности так же, как о величине Name типа TEmployee.


Область действия метода и параметр Self


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

Именно поэтому один из методов TEmployee может содержать оператор GetTitle := Title без какого-либо квалификатора перед Title. И именно поэтому Title принадлежит тому объекту, который вызывает метод. Если объект вызывает метод, то выполняется неяв- ный оператор with myself do method, связывающий объект и его ме- тоды в области действия.

Неявный оператор with выполняется путем передачи невидимого параметра методу всякий раз, когда этот метод вызывается. Этот параметр называется Self и в действительности является 32-разряд- ным указателем на экземпляр объекта, осуществляющего вызов мето- да. Относящийся к TEmployee метод GetRate приблизительно эквива- лентен следующему:

function TEmployee.GetRate(var Self: TEmployee): integer; begin GetRate := Self.Rate; end;

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

Но важно ли вам знать о существовании параметра Self? Обычно нет. Генерируемый Borland Pascal код выполняет все это автомати- чески. Однако в некоторых немногочисленных случаях вы можете за- хотеть проникнуть внутрь метода и использовать параметр Self яв- но.

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

Параметр Self является частью физического кадра стека при всех вызовах методов. Методы, используемые как внешние на языке Ассемблера, должны учитывать Self при получении доступа к пара- метрам метода в стеке.

Примечание: Более подробно об использовании методом границ стека рассказывается в Главе 22 "Руководства по язы- ку".



Определение методов


Процесс определения методов объектов напоминает модули Borland Pascal. Внутри объекта метод определяется заголовком про- цедуры или функции, действующей как метод:

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; end;

Примечание: Поля данных должны быть описаны перед пер- вым описанием метода.

Как и описания процедур и функций в интерфейсной секции мо- дуля (interface), описание методов внутри объекта говорит, что методы делают, но не говорит, как.

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

procedure TEmployee.Init(AName, ATitle: string;ARate: Real); begin Name := AName; Title := ATitle; Rate := ARate; end;

function TEmployee.GetName: String; GetName := Name; end;

function TEmployee.GetTitle: String; begin GetTitle := Title; end;

function TEmployee.GetRate: Real; begin GetRate := Rate; end;

Метод опpеделения следует методу интуитивного pазделения точками для указания поля записи. Кpоме наличия опpеделения TEmployee.GetName можно было бы опpеделить пpоцедуpу с именем GetName, в имени котоpой нет пpедшествующего идентификатоpа TEmployee. Однако, такая "внешняя" GetName не будет иметь никакой связи с объектом типа TEmployee и будет только запутывать смысл пpогpаммы.



Полиморфические объекты


При чтении предыдущего раздела вы, возможно, задали себе вопрос: "Если любой порожденный от типа параметра тип может пере- даваться в качестве параметра, то как же пользователь параметра узнает, какой тип объекта он получил?" Фактически, пользователь явно этого и не знает. Точный тип фактического параметра не из- вестен во время компиляции. Фактический параметр может быть объ- ектом любого дочернего от параметра-переменной типа, и именно по- этому он называется полиморфическим объектом.

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

Предположим, что вы написали инструментальное средство для вычерчивания графиков, поддерживающее многочисленные типы фигур: точки, окружности, квадраты, прямоугольники, кривые и т.д. В ка- честве части этого инструментального средства вы хотите написать программу, которая будет перемещать графическую фигуру по экрану с помощью устройства типа "мышь".

При старом способе необходимо было написать отдельную проце- дуру перемещения для каждого типа графической фигуры, поддержива- емой инструментальным средством. Вы должны были бы написать 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) инструментального средства, который был написан и оттранс- лирован год назад. Объектно-ориентированное программирование дает такую возможность, а виртуальные методы являются ключом к ней.

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


Поля данных объекта и формальные параметры метода


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

procedure CrunchIt(Crunchee: MyDataRec, Crunchby, ErrorCode: integer); var A, B: char; ErrorCode: integer; begin . . .

Локальные переменные процедуры и ее формальные параметры совместно используют общую область действия и поэтому не могут быть идентичными. Вы получите сообщение "Error 4: Duplicate identifier" (Ошибка 4; Повторение идентификатора), если попытае- тесь компилировать что-либо подобное, та же ошибка возникает при попытке присвоить формальному параметру метода имени поля объек- та, которому данный метод принадлежит.

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



Поля объектов


Вы можете обратиться к полю объекта в точности так же, как к полю обычной записи, либо с помощью оператора with, либо путем уточнения имени с помощью точки. Например:

AnHourly.Rate := 9.45;

with AnHourly do begin Name := 'Sanderson, Arthur'; Title := 'Word processor'; end;

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

Именно сейчас вы должны запомнить (в конце концов это придет само собой), что наследуемые поля являются столь же доступными, как если бы они были объявлены внутри типа объекта. Например, да- же если Name, Title и Rate не являются частью описания типа THourly (они наследованы от типа TEmployee), то вы можете ссы- латься на них, словно они описаны в THourly:

AnHourly.Name := 'Arthur Sanderson';



Пример размещения динамического объекта


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

Построение связанного списка объектов требует, чтобы каждый объект содержал указатель на следующий объект списка. Тип 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.

Программирование в "действительном залоге"


Большая часть из того, что говорилось об объектах до сих пор, исходит из удобств и перспектив Borland Pascal, поскольку наиболее вероятно, что это именно то, с чего вы начнете. Теперь начнутся изменения, поскольку мы подошли к концепциям объект- но-ориентированного программирования с помощью некоторых принци- пов программирования на стандартном Паскале. Объектно-ориентиро- ванное программирование имеет свое собственное отдельное множест- во понятий, частично благодаря началам объектно-ориентированного программирования (до некоторой степени ограниченным) в научных кругах, однако также и потому, что эта концепция действительно является радикально отличной от других.

Примечание: Объектно-ориентированные языки однажды ме- тафорично назвали "языками актеров".

Одним, часто забавным, следствием этого явилось то, что объ- ектно-ориентированное программирование фанатично "одушевляет" свои объекты. Отныне данные для вас не емкости, которые вы можете наполнять значениями. С точки зрения нового взгляда на вещи, объ- екты выглядят как актеры на подмостках со множеством заученных ролей (методов). Если вы (директор) даете им слово, то актеры на- чинают декламировать в соответствии со сценарием.

Было бы полезно представить функцию AnHourly.GetPayAmount как, например, дающую распоряжение объекту AnHourly "Вычислить размер вашей ежедневной платы". Центральной концепцией здесь яв- ляется объект. Этот объект обслуживают как список методов, так и список полей данных, содержащихся в объекте. И ни код, ни данные не являются здесь "директором".

Чтобы быть совсем привлекательным, объект не может быть опи- сан как актер на сцене. Образцу объектно-ориентированного прог- раммирования с большим трудом удается моделировать составляющие проблемы как компоненты, а не как логические абстракции. Случай- ности и закономерности, наполняющие нашу жизнь (от тостеров до телефонных звонков по поводу махровых полотенец) все имеют харак- теристики (данные) и линии поведения (методы). Характеристики тостера могут включать требуемое напряжение, число гренок, кото- рые он может поджарить одновременно, установку слабого или силь- ного уровней поджаривания, цвет тостера, его фабричную марку и т. д. Его поведение может включать загрузку кусков хлеба, поджарива- ние этих кусков и автоматическое выталкивание готовых гренок на- ружу.


Если мы хотим написать программу имитации кухни, то какой же имеется наилучший способ смоделировать различные приспособления, кроме объектов, с их характеристиками и линиями поведения, зако- дированными в поля данных и в методах? Фактически, это уже сдела- но: один из первых объектно-ориентированных языков (Симула-67) был создан как язык для написания таких имитаций.

Есть также причина того, что объектно-ориентированное прог- раммирование довольно крепко связано в традиционном смысле с ори- ентированной на построение графиков средой. Объекты должны быть моделями, и есть ли лучший способ смоделировать объект, чем нари- совать его изображение? Объекты в Borland Pascal должны имитиро- вать компоненты проблему, которую вы пытаетесь разрешить. Примите это во внимание, если в дальнейшем вы намерены эксплуатировать новые объектно-ориентированные расширения Borland Pascal.

| |


Проверка диапазонов при вызове виртуальных методов


В процессе разработки программы вам, возможно, захочется по- высить меры безопасности, которая снижается из-за вызовов вирту- альных методов Borland Pascal. Если директива $R находится во включенном состоянии, {$R+}, то все вызовы виртуальных методов будут проверяться на состояние инициализации для выполняющих вы- зовы реализаций. Если выполняющая вызов реализация еще не была инициализирована конструктором, то произойдет ошибка проверки ди- апазона исполняющей системы.

Примечание: Состоянием по умолчанию является {$R-}.

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

Виртуальный однажды - виртуальный всегда

Вы уже вероятно обратили внимание, что как TWinged, так и TBee содержат методы, называемые Show и Hide. Все заголовки мето- дов для Show и Hide объявлены виртуальными и снабжены зарезерви- рованным словом virtual. Как только родительский тип объекта объ- являет метод виртуальным, все его потомки также должны объявить этот метод виртуальным. Другими словами, статический метод никог- да не сможет переопределить виртуальный метод. Если вы попытае- тесь сделать это, то компилятор выдаст сообщение об ошибке.

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

Это целый новый мир.



Раннее связывание против позднего связывания


Различие между вызовом статического метода и динамического метода является различием между решением сделать немедленно и ре- шением отложить. Когда вы кодируете вызов статического метода, вы по существу говорите компилятору; "Ты знаешь, чего я хочу. Пойди и вызови это." С другой стороны, применение вызова виртуального метода, подобно разговору с компилятором; "Ты не знаешь пока, че- го я хочу. Когда придет время, задай вопрос о конкретном экземп- ляре."

Подумайте об этой метафоре в терминах проблемы 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.

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

Сущность механизма интересна и тонка, и немного позднее вы увидите, как он работает.



Расширяемость объекта


Важным замечанием, касающимся модулей типа WORKERS.PAS, яв- ляется то, что типы объектов и методы, определенные в модуле, мо- гут поставляться пользователю в форме .TPU, .TPW или .TPP т.е. в форме, способной к непосредственной компоновке, без исходного ко- да. (Нужно просмотреть только листинг интерфейсной части модуля.) Используя полиморфические объекты и виртуальные методы, пользова- тель файла .TPU, .TPW или .TPP сможет добавлять характеристики для приспособления модуля к своим нуждам.

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



Расширяющиеся объекты


Люди, которые впервые сталкиваются с Паскалем, зачастую счи- тают само собой разумеющейся гибкость стандартной процедуры 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;

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


Размещение и инициализация с помощью процедуры 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 в "Руководстве по языку").



Секция private


В некоторых случаях у вас могут иметься части описаний объ- ектов, которые экспортировать нежелательно. Например, вы можете предусмотреть объекты для других программистов, которые они могут использовать, но не могут непосредственно манипулировать с данны- ми объекта. Чтобы облегчить это, Borland Pascal позволяет зада- вать внутри объектов приватные (закрытые) поля и методы.

Приватные поля и методы доступны только внутри того модуля, в котором описан объект. В предыдущем примере, если бы тип THourly содержал приватные поля, то доступ к ним можно было бы получить только в модуле THourly. Даже если другие части объекта THourly можно было бы экспортировать, (части, описанные, как при- ватные, были бы недоступными.

Приватные поля и методы описываются непосредственно после обычных полей и методов, вслед за зарезервированным словом private. Таким образом, полный синтаксис описания объекта будет следующим:

type NewObject = object(родитель) поля; { общедоступные } методы; { общедоступные } private поля; { приватные } методы; { приватные } end;



Совмещенные код и данные


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

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

О том, что действительно существует вместе, Паскаль нигде не сообщает. Если это не отмечено комментарием или не то, о чем вы все время помните, то вы играете с судьбой.

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

Однако, Borland Pascal не вынуждает вас делать это. Как вся- кое структурное программирование, объектно-ориентированное прог- раммирование является дисциплиной, которую вы должны навязать се- бе, используя предоставляемые языком средства. Borland Pascal позволяет вам обращаться к полям объекта непосредственно извне объекта, однако он поощряет вас использовать преимущества объект- но-ориентированного программирования и создавать методы для мани- пулирования полями объекта внутри самого объекта. Borland Pascal позволяет задать принудительную инкапсуляцию с помощью использо- вания описания private в объявлении объекта.

Примечание: Подробнее о принудительной инкапсуляции рассказывается ниже в разделе "Секция private".



Совместимость типов объектов


Наследование до некоторой степени изменяет правила совмести- мости типов в 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.


Статические методы или виртуальные методы?


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

Предположим, что вы описываете объект с именем Ancestor и внутри этого объекта вы описываете метод с именем Action. Как вы определяете, каким должен быть метод, виртуальным или статичес- ким? Здесь приводится правило большого пальца: сделайте метод Action виртуальным, если имеется вероятность, что будущие наслед- ники объекта Ancestor будут переопределять Action, а вы хотите, чтобы будущий код был доступен Ancestor.

С другой стороны, помните, что если у объекта имеются любые виртуальные методы, то для этого объекта в сегменте данных будет создана таблица виртуальных методов (ТВМ) и каждый экземпляр это- го объекта будет иметь связь с ТВМ. Каждый вызов виртуального ме- тода должен проходить через ТВМ, тогда как статические методы вы- зываются непосредственно. Хотя просмотр ТВМ является весьма эф- фективным, вызов статического метода все равно остается немного более быстрым, чем вызов виртуального. И если в вашем объекте нет виртуальных методов, то и ТВМ отсутствует в сегменте данных и (что более важно) в каждом экземпляре объекта отсутствуют связи с ТВМ.

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



Удаление динамических объектов


Также, как и обычные записи Паскаля, размещаемые в динами- чески распределяемой области памяти, объекты могут удаляться про- цедурой Disрose, если они больше не нужны:

Disрose (P);

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

MyComрlexObject.Done;

Метод Done должен инкапсулировать все детали очистки своего объекта, а также всех структур данных и вложенных объектов.

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

Допустимо и часто бывает полезно определять несколько мето- дов очистки для данного типа объекта. В зависимости от того, как они размещены или используются, или в зависимости от состояния и режима объекта на момент очистки, сложные объекты могут потребо- вать очистки несколькими различными путями



Виртуальные методы


Метод становится виртуальным, если за его объявлением в типе объекта стоит новое зарезервированное слово 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 предоставляет эти механизмы за счет поддержки им виртуальных методов.

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

Уже описанная простая иерархия крылатых насекомых являет со- бой хороший пример полиморфизма в действии, предоставляемого пос- редством виртуальных методов.

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

Что является особым для каждого типа объекта, так это спо- соб, которым он должен показать самого себя на экране. Например, у пчелы на экране должны рисоваться черные полоски на туловище. Можно показать на экране любой тип крылатых насекомых, но меха- низм рисования каждого является сугубо специфическим. Одно слово "нарисовать" используется для рисования (буквально) многих крыла- тых насекомых. Аналогично, если вернуться к нашему примеру пла- тежной ведомости, то слово "GetPayAmount" вычисляет размер выплат для нескольких категорий работающих.

Это были примеры полиморфизма, а виртуальными методами явля- ется то, что реализует его в Borland Pascal.



ориентированное программирование является прямым следствием


Объектно- ориентированное программирование является прямым следствием усложнения современных приложений, усложнения, которое часто заставляет многих программистов в отчаянии вскидывать вверх руки. Наследование и инкапсуляция являются максимально эффектив- ными средствами для управления сложностью. (Существует разница между десятью тысячами насекомых, классифицированных по таксоно- мической схеме, и десятью тысячами насекомых, жужжащих возле ва- ших ушей.) Представляя собой значительно большее, чем просто структурное программирование, объектно-ориентированное программи- рование вносит рациональный порядок в структуру программного обеспечения ЭВМ, что, как и таксономическая схема, устанавливает порядок, не устанавливая пределов.
Добавьте сюда перспективы возможности расширения и повторно- му использования существующего кода и все это начнет звучать нас- только хорошо, что будет походить на правду. Вы думаете, что это невозможно?
Но это же Borland Pascal! Слово "невозможно" в нем не опре- делено.
| |