ГЛАВА 4. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ. Объектно-ориентированное программирование (ООП) - это метод программирования, имитирующий способы, какими, по нашим представлениям, выполнены предметы. Это естественная эволюция из более ранних новшеств в проектировании языков программирования: оно более структурировано, чем предыдущие попытки в структурном программировании; оно более модульно и абстрактно, чем предыдущие попытки в абстрагировании данных и сокрытии деталей. Три основных свойства характеризуют язык объектно-ориентированного программирования: - инкапсуляция: объединение записей с процедурами и функциями, что превращает их в новый тип данных - объект; - наследование: задание объекта, затем использование его для построения иерархии порожденных объектов, с наследованием доступа каждого из порожденных объектов к коду и данным предка; - полиморфизм: задание одного имени действию, которое передается вверх и вниз по иерархии объектов, с реализацией этого действия способом, соответствующим каждому объекту в иерархии. Расширение языка Turbo Pascal предоставляет все возможности объектно-ориентированного программирования: наибольшую структурность, абстрактность, модульность, встроенные непосредственно в язык. Все эти свойства присущи и коду, являющемуся наиболее структурным, расширяемым и легким для поддержки. Обращение к объектно-ориентированному программированию заставит Вас изменить привычки и способы мышления в программировании, которые были стандартными в течение многих лет. Однако ООП является прямым открытым и незаурядным для решения проблем, особенно трудных для традиционного программирования. Замечания тем, кто имеет опыт объектно-ориентированного программирования в других языках: забудьте все сложившиеся штампы в ООП и изучайте объектно-ориентированные свойства Turbo Pascal в его терминах. ООП - это не единственный метод; это множество идей. По философии объектов, Turbo Pascal стоит ближе к С++, чем к SmallTаlk. SmallTalk - это интерпретатор, в то время как с самого начала Turbo Pascal имел встроенный собственный компилятор. Встроенные компиляторы выполняют работу иначе, чем интерпретаторы (и гораздо быстрее). Turbo Pascal проектировался как средство разработки продукта, а не как инструмент исследования. И замечания тем, кто не имеет никакого понятия о том, что такое ООП: сколько путаницы произошло из-за того, что люди рассуждают о том, в чем не разбираются. Забудьте все, что Вам наговорили об ООП. Лучший способ (и на самом деле единственный) для изучения ООП - это сесть и пытаться изучить его самим. Объекты? Да, объекты. Посмотрите вокруг себя... Кругом они: например, яблоко, которое Вы взяли на завтрак. Предположим, что Вы захотели описать яблоко в терминах программного обеспечения. Сразу появится искушение расчленить его: пусть S представляет площадь кожуры; пусть J представляет объем сока, который оно содержит; пусть F представляет вес фрукта; пусть D представляет число семечек.... Не рассуждайте таким образом. Думайте как художник. Вы смотрите на яблоко, и Вы рисуете яблоко. Изображение яблока не есть само яблоко; это символ на плоской поверхности. Но оно не абстрагируется в несколько чисел, стоящих отдельно и независимо друг от друга где-то в сегменте данных. Все компоненты остаются вместе, со своими взаимосвязями. Объекты моделируют свойства и поведение элементов мира, в котором мы живем. Они являются окончательной абстракцией данных. Примечание: Объекты хранят все свои свойства и поведение вместе. Яблоко можно расчленить, но в таком виде оно яблоком уже не будет. Взаимосвязи между частями и целым становятся очевидными, когда все хранится вместе, в одной "оболочке". Это называется инкапсуляцией, и это очень важное понятие. Мы вернемся к инкапсуляции немного позднее. Не менее важен факт, что объекты могут наследовать свойства и поведение от того, что мы называем родительскими объектами. Это интуитивный скачок; наследование, возможно, является единственным крупным отличием объектно-ориентированного Паскаля от программирования на Turbo Pascal в настоящее время. Наследование. Цель науки состоит в том, чтобы описать вселенную. В достижении этой цели работа науки, в основном, заключается просто в создании генеалогических деревьев. Когда энтомологи вернулись с берегов Амазонки с неизвестным ранее насекомым в банке, их задача состояла в том, чтобы определить, куда следует поместить это насекомое в гигантской диаграмме, в которой собраны все научные названия всех насекомых. Такие же диаграммы существуют и для растений, рыб, млекопитающих, пресмыкающихся, химических элементов, внутриатомных частиц и других галактик. Все они выглядят как генеалогические деревья: единственная общая категория в вершине, с увеличивающимся числом расходящихся веером категорий ниже этой единственной категории. Внутри категории "насекомое", например, есть две ветви: насекомые с видимыми крыльями и насекомые со скрытыми крыльями или вообще без них. Ниже крылатых насекомых находится большое число категорий: мотыльки, бабочки, мухи и т.д. Каждая категория имеет подкатегории, а ниже этих подкатегорий находится еще больше подкатегорий (см. рис.4.1) Рисунок 1.1. Часть диаграммы категории насекомые. ┌───────────┐ │ насекомые │ └─┬───────┬─┘ ┌───────────────┘ │ │ │ ┌────┴─────┐ ┌───┴───────┐ │ крылатые │ │ бескрылые │ └┬───┬───┬─┘ └───────────┘ ┌────────┘ │ └───────┐ ┌───┴──────┐┌────┴────┐┌─────┴────┐ │ мотыльки ││ бабочки ││ мухи │ └──────────┘└─────────┘└──────────┘ Этот процесс классификации называется систематикой. Это хорошая метафора для механизма наследования объектно-ориентированного программирования. Вопросы, какие задают себе ученые при попытке классифицировать какое-то новое животное или объект: Насколько оно похоже на животное или объекты общего класса? Насколько оно отличается? Каждый различный класс имеет множество свойств и черт поведения, которые характеризуют его. Ученые начинают с верхушки семейного дерева образцов и спускаются по его ветвям, задавая себе эти два вопроса на протяжении всего пути. Наивысшие уровни всегда самые общие, и вопросы простейшие: с крыльями или без? Каждый уровень более специфичен по сравнению с предыдущим и является менее общим. Очевидно, ученые добрались до точки высчитывания волосинок на третьем суставе задних лапок насекомых - в самом деле, специфичное свойство (и основательная причина, возможно, по которой энтомологом лучше не быть). Важно запомнить, что как только свойство задано, все категории ниже этого определения содержат это свойство. Поэтому как только насекомое идентифицировано как член расположения diptera (муха), не нужно указывать заново, что муха имеет одну пару крыльев. Особи насекомых, которых мы называем мухами, наследуют это свойство из расположения в семейном дереве. Вскоре Вы узнаете, что объектно-ориентированное программирование очень напоминает процесс построения генеалогических деревьев для структур данных. Одной из важнейших вещей, добавляемых объектно-ориентированным программированием к традиционным языкам программирования таким, как Паскаль, является механизм наследования свойств для типов данных от более простых, более общих типов. Этим механизмом является наследование. Объекты: записи, которые наследуют. В терминах Паскаля, объект очень напоминает запись, которая служит "оболочкой" для соединения нескольких связанных между собой элементов данных под одним именем. В графической среде мы должны взять вместе координаты X и Y позиции на графическом экране и назвать это типом записи с именем Location: Location = record X, Y : Integer; end; Location здесь является типом запись; это шаблон, который используется компилятором для создания переменных типа запись. Переменная типа Location - это экземпляр типа Location. Термин "экземпляр" используется сейчас в окружении Паскаля, но он всегда используется людьми, занимающимися ООП, и будет хорошо, если Вы начнете думать в терминах типов и экземпляров этих типов. О типе Location можно думать двумя способами: когда координаты X и Y нужны отдельно и когда Вы думаете о них, как о полях X и Y в записи. С другой стороны, если Вам нужно думать о координатах X и Y, действующих вместе для нахождения точки на экране, то Вы можете думать о них совокупно как о Location. Предположим, Вы хотите высветить точку в позиции, описанной на экране с помощью Location записи. В Паскале Вы можете добавить булевское поле, указывающее, светится ли пиксел в данной позиции, и сделать это новым типом записи: Point = record X, Y : Integer; visible : Вoolean; end; Если быть поумнее, то можно сохранить тип записи Location посредством создания поля типа Location внутри типа Point: Point = record Position : Location; Visible : Вoolean; end; Это работает, и программисты на Паскале занимаются этим все время. Одна вещь, которую этот метод не делает: он не заставляет Вас думать о сущности того, чем Вы манипулируете в своем программном обеспечении. Вам нужно задавать себе вопросы типа: "Чем точка на экране отличается от позиции на экране?" Ответ такой : Точка - это позиция на экране, которая светится. Подумайте еще раз о первой части этого утверждения: точка - это позиция ... В определении точки неявно присутствует позиция для этой точки (пикселы существуют только на экране). В объектно-ориентированном программировании мы признаем эту особую взаимосвязь. Поскольку все точки должны содержать позицию, мы говорим, что тип Point - это тип, порожденный типом Location. Point наследует все, что имеет Location, и добавляет нечто новое к Point, чтобы сделать Point тем, чем оно должно быть. Процесс, посредством которого один тип наследует свойства другого типа, называется наследованием. Наследник называется порожденным типом; тип, от которого наследует порожденный тип, называется родительским типом. Типы записей обычного Паскаля наследовать не могут. Turbo Pascal, однако, расширяет язык Паскаль для поддержки наследования. Одним из этих расширений является новая категория структуры данных, имеющая отношение к записям, но гораздо более мощная. Типы данных в этой новой категории задаются новым зарезервированным словом : object (объект). Тип объект может быть задан как законченный, автономный тип в стиле записей Паскаля, или может быть задан как порождение существующего типа объекта, посредством помещения имени родительского типа в круглых скобках после зарезервированного слова object. В графическом примере, который мы только что рассмотрели, два типа объекта можно задать соответственно таким способом: type Location = object X, Y : Integer; end; Point = object(Location) visible : Вoolean; end; Примечание: Отметим использование здесь круглых скобок для обозначения наследования. Здесь Location является родительским типом, а Point - порожденным типом. Как Вы увидите позднее, процесс может продолжаться бесконечно: можно задать наследников типа Point, наследников типа, порожденного от Point и т.д. Большая часть применений объектно-ориентированного проектирования состоит в построении этой иерархии объектов, выражающей семейное дерево объектов. Все возможные типы, порожденные от Location, называются Location порожденными типами, а Point - один из непосредственных наследников Location. Обратно, Location - непосредственный родитель для Point. Тип объект (как подсправочник DOS) может иметь любое число непосредственных наследников, но только одного непосредственного предка. Объекты тесно связаны с записями, как показывают эти определения. Новое зарезервированное слово object - наиболее очевидное отличие, но существует множество других отличий, некоторые из них очень тонкие, как Вы увидите позднее. Например: X и Y поля в Location не пишутся явно в типе Point, но Point их имеет, во всяком случае, благодаря наследованию. Можно говорить о значении Х типа Point, точно так же, как можно говорить о значении Х типа Location. Экземпляры объектного типа. Экземпляры типа объект объявляются точно так же, как в Паскале объявляется любая переменная, либо как статические переменные, либо как указатель на переменные, размещенные в куче: type PointPtr = ^Point; var StatPoint : Point; {готово к использованию} DynaPoint : PointPtr; {перед использованием нужно разместить с помощью New} Поля объекта. Доступ к полям данных объекта можно получить так же, как и в обычной записи; либо используя оператор With, либо используя точку. Например: MyPoint.Visible := false; With MyPoint do begin X := 341; Y := 42; end; Примечание: Не забывайте! Поля, наследуемые объектом, не обрабатываются особым образом просто потому, что они унаследованы. Сейчас, прежде всего, Вы должны запомнить (возможно, понимание этого придет естественно), что унаследованные поля доступны так же, как поля, объявленные внутри данного типа объекта. Например, хотя X и Y не являются частью объявления Point (они наследуются из типа Location), можно задавать их, как будто бы они объявлены внутри Point: MyPoint.X := 17; Хорошая практика и плохая практика. Хотя к полям объекта можно получить прямой доступ, поступать так - не слишком хорошая идея. Принципы объектно-ориентированного программирования требуют, чтобы поля объекта оставались нетронутыми столько, насколько это возможно. Это ограничение вначале может показаться произвольным и грубым, но это - часть большой картины ООП, которую мы создаем в этой главе. Со временем Вы увидите смысл этой новой декларации для хорошей практики программирования, однако по ряду причин мы не можем раскрыть его сейчас полностью и пока примите на веру: избегайте прямого доступа к полям данных объекта. Примечание: Turbo Pascal сейчас позволяет Вам сделать поля и методы объекта личными (private). См. ниже "Раздел private". Итак - как получить доступ к полям объекта? Что устанавливает их и читает их? Примечание: Поля данных объекта - это то, что объект знает; его методы - это то, что объект делает. Ответ такой: для доступа к полям данных объекта нужно использовать методы объекта, где только возможно. Метод - это процедура или функция, объявленная внутри объекта и тесно связанная с этим объектом. Методы. Методы - это одни из наиболее привлекательных атрибутов объектно-ориентированного программирования, и они очень выгодны для использования. Вернемся назад к обоснованию старой необходимости структурного программирования, инициализации структур данных. Рассмотрим задачу инициализации записи с таким определением: Location = record X, Y : Integer; end; Большинство программистов будут использовать оператор with для присваивания исходных значений полям X и Y: var MyLocation : Location; with MyLocation do begin X := 17; Y := 42; end; Это работает хорошо, но это тесно связано с одним определенным экземпляром, MyLocation. Если потребуется инициализировать более, чем одну запись Location, Вам нужно будет больше операторов with, выполняющих, по существу, одну и ту же вещь. Следующим естественным шагом является построение процедуры инициализации, которая обобщает оператор with, чтобы включить любой экземпляр типа Location, передаваемый как параметр: procedure InitLocation(var Target : Location; NewX, NewY : Integer); begin with Target do begin X := NewX; Y := NewY; end; end; Это работает, все хорошо - но если у Вас появилось чувство, что это немного более глупо, чем должно быть, то Вы чувствуете такую же вещь, которую нащупывают более ранние теории объектно-ориентированного программирования. Это чувство подразумевает, что Вы спроектировали процедуру InitLocation специально для обслуживания типа Location. Но зачем Вы должны хранить задание типа записи и экземпляра, над которым InitLocation выполняет действия? Должно быть несколько способов объединения типа запись и кода, обслуживающего ее, в единое целое. Здесь рассматривается такой способ. Он называется методом. Метод - это процедура или функция настолько тесно связанные с данным типом, что метод окружается невидимым оператором with, делая доступными экземпляры этого типа изнутри метода. Определение типа включает заголовок метода. Полное определение метода задается с именем типа. Тип объекта и метод объекта - это две стороны этого нового вида структуры, называемой объектом: type Location = object X,Y : Integer; procedure Init(NewX, NewY : Integer); end; procedure Location.Init(NewX, NewY : Integer); begin X := NewX; {поле Х объекта Location } Y := NewY; {поле Y объекта Location } end; Теперь, чтобы инициализировать экземпляр типа Location, просто вызовите его метод, как если бы метод был полем записи: var MyLocation : Location; MyLocation.Init(17, 42); {легко, нет?} Код и данные вместе. Одним из важнейших принципов объектно-ориентированного программирования является то, что программист должен думать о коде и данных вместе во время проектирования программы. Ни код, ни данные не существуют в вакууме. Данные управляют потоком кода, а код манипулирует формой и значением данных. Когда код и данные существуют отдельно, всегда есть опасность вызова правильной процедуры с неверными данными или неверной процедуры с правильными данными. Проверка этого соответствия является задачей программиста, и в то время, как Паскаль оказывает помощь при проверке строгого соответствия типов, лучшее, что он может сказать, это то, чего нельзя сделать "вместе". Паскаль ничего не говорит о том, что можно сделать "вместе", если этого нет в комментариях или в Вашей голове, то Вы в воле случая. Посредством объединения объявления данных и кода объект помогает хранить их синхронно. Обычно, для того, чтобы получить значение одного из полей объекта, Вы вызываете метод, принадлежащий этому объекту, который возвращает значение требуемого поля. Чтобы установить значение поля, Вы вызываете метод, присваивающий новое значение этому полю. Как и многие аспекты объектно-ориентированного программирования, инкапсуляция данных - это дисциплина, которой Вы всегда должны следовать. Лучший способ обращаться к данным объекта с использованием его методов вместо прямого чтения данных. Turbo Pascal позволяет Вам усилить инкапсуляцию, используя объявление private в объявлении объекта (см. ниже "Раздел private"). Задание методов. Процесс задания метода напоминает модули Turbo Pascal. Вне объекта метод задается заголовком процедуры или функции, действующей как метод: type Location = object X,Y : Integer; procedure Init(InitX, InitY : Integer); function GetX : Integer; function GetY : Integer; end; Примечание: Все поля данных должны быть объявлены перед первым объявлением метода. Как и объявление процедур и функций в интерфейсном разделе модуля, объявления метода внутри объекта говорят, что метод делает, но не как. Как - задается вне определения объекта в отдельном определении процедуры или функции. Когда методы полностью определены вне объекта, имени метода должно предшествовать название типа объекта, которому метод принадлежит, сопровождаемое точкой: procedure Location.Init(InitX, InitY : Integer); begin X := InitX; Y := InitY; end; function Location.GetX : Integer; begin GetX := X; end; function Location.GetY : Integer; begin GetY := Y; end; Определение метода следует интуитивно методу задания поля записи с помощью точки. Вдобавок к определению Location.GetX, будет вполне законно задать процедуру, названную GetX, без предшествующего ее имени идентификатора Location. Однако, "внешняя" GetХ не будет иметь связи с типом объекта Location и может привести к путанице в программе. Сфера действия метода и Self параметр. Заметим, что нигде в предыдущих методах нет явной with объект do ... конструкции. Поля данных объекта доступны этим методам объекта. Разделенные в исходном коде, тела методов и поля данных объекта на самом деле разделяют одну и ту же сферу действия. Это является объяснением того факта, что один из Location методов может содержать оператор GetY := Y; без квалификатора для Y. Это происходит потому, что Y принадлежит тому же объекту, что и этот метод. Когда объект определяет метод, при этом присутствует неявный оператор with MySelf do метод, связывающий объект и его метод в сфере действия. Этот неявный with оператор выполняется посредством передачи неявного параметра методу каждый раз, когда какой-либо метод вызывается. Этот параметр называется Self и фактически является полным 32 битовым указателем на экземпляр объекта, делающий вызов для метода. GetY метод, принадлежащий Location, грубо эквивалентен следующему: function Location.GetY(var Self : Location) : Integer; begin GetY := Self.Y; end; Примечание: Этот пример синтаксически не совсем правилен; он приводится здесь просто для того, чтобы дать более полную оценку особой связи между объектами и его методами. Важно ли для Вас знать больше о Self? Обычно нет. Генерируемый Turbo Pascal код обрабатывает его автоматически. Однако есть несколько случаев, когда Вы можете вмешаться внутрь метода и сделать явным использование Self параметра. Примечание: Явное использование Self законно, но Вы должны избегать ситуаций, требующих этого. Фактически Self является автоматически объявленным идентификатором, и если Вам случилось обнаружить конфликт идентификаторов внутри метода, Вы можете разрешить его путем использования Self идентификатора в качестве квалификатора к любому полю данных, принадлежащих объекту метода: type MouseStat = record Active : Boolean; X, Y : Integer; LButton, RButton : Boolean; Visible : Boolean; end; procedure Location.GoToMouse(MousePos : MouseStat); begin Hide; with MousePos do begin Self.X := X; Self.Y := Y; end; Show; end; Примечание: Методы, реализованные как внешние на языке ассемблер, должны принимать в расчет Self, когда они получают доступ к параметрам метода в стеке. Более детальное описание структуры стека вызова метода см. главу 18 Руководства программиста. Этот пример слишком прост и использование Self можно избежать путем отказа от использования with оператора внутри Location.GoToMouse. Вы можете оказаться в ситуации, когда внутри сложного метода необходимость использования оператора with упрощает логику в достаточной степени для отказа от использования параметра Self. Self параметр - это часть физической структуры стека для всех вызовов метода. Поля данных объекта и формальные параметры метода. Следствием того факта, что объекты и их методы разделяют одну и ту же сферу действия, является то, что формальные параметры метода не могут быть идентичны полям данных объекта. Это не новые ограничения, вводимые объектно-ориентированным программированим, а скорее старые правила сферы действия, которые всегда были в Паскале. Это те же самые правила, не позволяющие формальным параметрам процедуры быть идентичными локальными переменными процедуры: procedure CrunchIt(Crunchee : MyDataRec; CrunchBy, ErrorCode : Integer); var A, B : Char; ErrorCode : Integer; {это объявление приведет к ошибке} begin ... Локальные переменные процедуры и ее формальные параметры разделяют одну и ту же сферу действия и поэтому не могут быть идентичными. Вы получите "Error 4: Duplicate identifier", если попытаетесь откомпилировать что-то подобное этому; случится та же самая ошибка, если Вы попытаетесь задать методу формальный параметр, идентичный какому-либо полю в объекте, которому принадлежит этот метод. Последствия немного отличаются, так как помещение заголовка процедуры внутри структуры данных - это новое в Turbo Pascal, но ведущие принципы сферы действия Паскаля в целом не изменились. Объекты, экспортируемые модулями. Имеет смысл задать объекты в модуле с объявлением типа объекта в интерфейсном разделе модуля и телами процедур методов типа объект, заданными в разделе реализации модуля. Примечание: Экспортируемый означает "заданный в интерфейсном разделе модуля". Модули могут иметь собственные определения типа объект внутри раздела реализации, и такие типы подчиняются таким же ограничениям, что и любые типы, заданные в разделе реализации модуля. Тип объект, заданный в интерфейсном разделе модуля, может иметь порожденные типы объекта, заданные в разделе реализации модуля. В случае, когда модуль В использует модуль А, модуль В так же может задавать порожденные типы любого объекта, экспортируемого из модуля А. Типы объектов и методы, описанные выше, можно задать внутри модуля, как показано в POINTS.PAS на Вашем диске. Чтобы использовать типы объектов и методы, определенные в модуле Points, можно просто использовать этот модуль в своей программе и объявить экземпляр типа Point в var разделе своей программы: program MakePoints; uses Graph, Points; var APoint : Point; ... Для того, чтобы создать и показать точку, представленную переменной APoint, нужно просто вызвать методы APoint, используя синтаксис с точкой: APoint.Init(151, 82); {исходные значения X, Y 151 и 52} APoint.Show; {APoint включается} APoint.MoveTo(163, 101); {APoint перемещается в 163, 101} APoint.Hide; {APoint выключается} Примечание: Объекты так же могут быть типированными константами. Объекты имеют много общего с записями и их так же можно использовать внутри with операторов. В этом случае название объекта, которому принадлежит метод, необязательно: with APoint do begin Init(151, 82); {исходные значения X, Y 151 и 52} Show; {APoint включается} MoveTo(163, 101); {APoint перемещается в 163, 101} Hide; {APoint выключается} end; Как и записи, объекты могут передаваться в процедуры как параметры и (как Вы увидите позднее) могут размещаться в "куче". Раздел private. В ряде случаев у Вас могут быть части объявления объекта, которые Вы не хотите экспортировать. Например, Вам может понадобиться предоставить объекты для использования другими программистами, не позволяя им манипулировать данными объекта напрямую. Чтобы позволить это, Turbo Pascal разрешает указывать личные (private) поля и методы внутри объекта. Личные поля и методы доступны только внутри модуля, в котором объявлен объект. В предыдущем примере, если, например, тип Point имел личные поля, к ним можно обратиться только изнутри модуля Points. Даже, хотя другие части Point будут экспортироваться, части, объявленные как личные, будут недоступны. Личные поля и методы объявляются сразу после обычных полей и методов, следуя за зарезервированным словом private. Полный синтаксис объявления объекта: type НовыйОбъект = object(предок) поля; методы; private поля; методы; end; Непосредственное программирование. Многое из того, что было сказано об объектах, демонстрировало удобные перспективы развития Turbo Pascal, с которым Вы, вероятно, знакомы. При переходе к ООП от опыта программирования на стандартном Паскале многое меняется. ООП имеет свой особый набор принципов, так как своим происхождением обязян довольно узкой области исследований, и так же просто потому, что действительно имеет радикальные отличия. Примечание: Объектно-ориентированные языки однажды были метафорически названы "актерскими языками". Забавно выглядит, как фанатики ООП алгоритмизируют свои объекты. Структуры данных не являются больше пассивными корзинами, куда Вы забрасываете значения. С новой точки зрения, объект рассматривается как актер на сцене, с набором заученных наизусть черт поведения (методов). Когда Вы (режиссер) даете команду, актер декламирует по сценарию. Может оказаться полезным порассуждать так, что оператор APoint.MoveTo(242, 118) отдает приказ объекту APoint, говоря: "передвинься в позицию 242, 118". Здесь объект является основным понятием. Как список методов, так и список полей данных, содержащихся в объекте, служат объекту. Ни код, ни данные не являются главными. Объекты не описываются как актеры на сцене, что как раз и привлекательно. Объектно-ориентированное программирование старается очень строго моделировать компоненты проблемы как компоненты, а не как логические абстракции. Самые различные вещи, встречающиеся нам в жизни, от тостера, телефонов до махровых полотенец, все имеют свойства (данные) и поведение (методы). Свойства тостера могут включать требуемое напряжение, количество ломтиков, которое он может поджарить одновременно, установку уровня свет/темнота, его цвет, марку и т.д. Его поведение включает прием ломтиков хлеба, поджаривание их и выдача поджаренных ломтиков обратно. Если мы хотим написать программу моделирования кухни, что может быть лучше способа моделирования различных приспособлений в виде объектов со своими свойствами и поведением, закодированным в полях данных и методах? Фактически это было сделано; самый первый объектно-ориентированный язык (Simula-67) был создан как язык для такого моделирования. Это является причиной того, что объектно-ориентированный язык так тесно привязан в обычном здравом смысле к графическо-ориентированной среде. Объекты должны быть имитациями, а что может быть лучше способа смоделировать объект, чем нарисовать его изображение? Объекты Turbo Pascal должны моделировать компоненты проблемы, которую Вы пытаетесь решить. Запомните это, так как Вы в дальнейшем будете изучать новые объектно-ориентированные расширения Turbo Pascal. Инкапсуляция. Объединение кода и данных вместе в объектах называется инкапсуляцией. Если Вы любите докапываться до сути, то Вы можете обеспечить такое количество методов, чтобы пользователю объекта никогда не потребовался прямой доступ к полям объекта. Подобно Smalltalk и другим объектно-ориентированным языкам, Turbo Pascal дает Вам возможность принудительной инкапсуляции через использование директивы private. В этом примере мы не хотим указывать раздел private для полей и методов, вместо этого, мы ограничимся непосредственным использованием методов для доступа к данным. Примечание: Объявление полей как private означает возможность доступа к ним только через методы. Location и Point написаны так, что прямой доступ к любым внутренним полям данных совершенно не нужен: type Location = object X,Y : Integer; procedure Init(InitX, InitY : Integer); function GetX : Integer; function GetY : Integer; end; Point = object (Location) Visible : Boolean; procedure Init(InitX, InitY : Integer); procedure Show; virtual; procedure Hide; virtual; function IsVisible : Boolean; procedure MoveTo(NewX, NewY : Integer); end; Здесь есть только три поля данных: X, Y и Visible. MoveTo метод присваивает новые значения X и Y, GetX и GetY методы возвращают значения X и Y. Поэтому прямого доступа к X, Y в дальнейшем не потребуется. Show и Hide переключают булевскую переменную Visible между True и False, а IsVisible функция возвращает текущее состояние Visible. Предположив, что экземпляр типа Point называется APoint, Вы можете использовать этот набор методов для непрямого управления полями данных APoint, например: with APoint do begin Init(0, 0); {инициализирует новую точку в 0,0} Show; {делает точку видимой} end; Заметим, что к полям данных объекта доступа нет вообще, за исключением методов объекта. Методы: обратная сторона. Добавление этих методов немного увеличивает в объеме исходную форму Point, но суровый редактор связей Turbo Pascal удаляет любой код метода, который ни разу не вызывается в программе. Следовательно не нужно бояться задать типу объекта метод, который может и не использоваться в каждой программе, использующей этот тип объекта. Неиспользуемые методы не будут Вам стоить ничего при выполнении и не отразяться на размере .EXE файла - если они не используются, их там просто нет. Примечание: Замечание об абстракции данных. Есть много выгодных сторон в возможности полностью исключить Point из глобальных ссылок. Если никто вне объекта "не знает" представления его внутренних данных, то программист, управляющий этим объектом, может изменять детали внутреннего представления данных - до тех пор, пока заголовок метода остается прежним. Внутри некоторых объектов данные можно представить как массив, но позднее (возможно, по мере роста области действия приложения и расширения объема данных) более эффективным представлением можно признать двоичное дерево. Если объект полностью инкапсулирован, то изменение в представлении данных из массива в двоичное дерево не изменит использование объекта в целом. Интерфейс к объекту остается полностью прежним, позволяя программисту изящно настраивать реализацию объекта без разбиения какого-то бы ни было кода, использущего этот объект. Расширяемые объекты. Люди, впервые столкнувшиеся с Паскалем, считают само собой разумеющимся гибкость стандартной процедуры Writeln, позволяющей одной процедуре обрабатывать параметры нескольких различных типов: Writeln(CharVar); {выводит значение символа} Writeln(IntegerVar); {выводит целочисленное значение} Writeln(RealVar); {выводит значение с плавающей точкой} К сожалению стандартный Паскаль не предоставляет возможности для создания Ваших собственных в равной степени гибких процедур. Объектно-ориентированное программирование решает эту проблему посредством наследования: когда задается родительский тип, методы родительского типа наследуются, но при необходимости они так же могут быть перекрыты. Чтобы перекрыть родительский метод, просто задайте новый метод с тем же именем, что и родительский метод, но с другим телом и (если необходимо) с другим набором параметров. Рассмотрим простой пример. Давайте зададим тип, порожденный от Point, который рисует окружность вместо точки на экране: type Circle = object (Point) Radius : Integer; procedure Init(InitX, InitY : Integer; InitRadius : Integer); procedure Show; procedure Hide; procedure Expand(ExpandBy : Integer); procedure Contract(ContractBy : Integer); end; constructor Circle.Init(InitX, InitY : Integer; InitRadius : Integer); begin Point.Init(InitX, InitY); Radius := InitRadius; end; procedure Circle.Show; begin Visible := True; Graph.Circle(X, Y, Radius); end; procedure Circle.Hide; var TempColor : Word; begin TempColor := Graph.GetColor; Graph.SetColor(GetBkColor); Visible := False; Graph.Circle(X, Y, Radius); Graph.SetColor(TempColor); end; procedure Circle.Expand(ExpandBy : Integer); begin Hide; Radius := Radius + ExpandBy; if Radius < 0 then Radius := 0; Show; end; procedure Circle.Contract(ContractBy : Integer); begin Expand(-ContractBy); end; procedure Circle.MoveTo(NewX, NewY : Integer); begin Hide; X := NewX; Y := NewY; Show; end; Окружность, в некотором смысле, является жирной точкой: она имеет все, что имеет точка (позицию X,Y, состояние видимый/невидимый) плюс радиус. Объектный тип Circle появился для того, чтобы иметь единственное поле Radius, но не забывает о всех других полях, которые Circle наследует как наследник типа Point. Circle имеет X,Y и Visible, даже если Вы не видите их в определении типа для Circle. Так как Circle задает новое поле Radius, то его инициализация требует нового Init метода, который инициализирует Radius, так же как и наследуемые поля. Вместо прямого присваивания значений наследуемым полям, почему бы не использовать повторно Point метод инициализации (иллюстрируемый первым оператором в Circle.Init). Синтаксис для вызова наследуемого метода: Ancestor.Method, где Ancestor - это идентификатор родительского типа объекта, а Method - идентификатор метода этого типа. Заметим, что вызов перекрывающего метода является не очень хорошим стилем; вполне возможно, что Point.Init (или Location.Init для этого случая) выполняет более важную, скрытую инициализацию. Путем вызова перекрывающего метода Вы обеспечите включение в тип объекта, являющегося потомком, функциональности родителя. Вдобавок, любые изменения, сделанные в родительском методе, автоматически влияют на всех его потомков. После вызова Point.Init, Circle.Init может затем выполнить свою собственную инициализацию, которая в данном случае состоит только в присвоении Radius значения, передаваемого в InitRadius. Вместо изображения и удаления окружности по точке, можно использовать BGI Circle процедуру. Если Вы поступите так, то потребуются новые Show и Hide методы, перекрывающие такие же методы в Point. Такие переписанные заново Show и Hide методы появились в приведенном выше примере. Уточнение имени с помощью точки разрешает возможную проблему освобождения от имени типа объекта, являющегося таким же, что и имя BGI программы, рисующей тип объекта на экране. Graph.Circle - это совершенно однозначный способ сказать Turbo Pascal, что Вы ссылаетесь на Circle программу в GRAPH.TPU, а не на Circle тип объекта. Важно! В то время, как методы можно перекрывать, поле данных перекрывать нельзя. Если однажды Вы задали поле данных в иерархии объектов, порожденный тип не может задать поле данных с точно таким же идентификатором. Наследуемые статические методы. Один из Point методов перекрывается методом MoveTo в определении типа Circle. Если Вы внимательны, то посмотрев на MoveTo, Вы можете удивиться, почему MoveTo не использует поле Radius и не делает вызова BGI или других библиотек для изображения окружностей. В то же время GetX и GetY методы наследуются на протяжении всего пути из InitLocation без изменений. Также, Circle. MoveTo полностью идентичен Point.MoveTo. Ничего не изменилось, произошло только копирование программы и перед идентификатором MoveTo появился квалификатор Circle. Этот пример демонстрирует проблему с объектами и методами, появляющуюся при работе в таком стиле. Все методы, показанные в связи с Location, Point и Circle типами объектов, являются статическими методами. (Термин "статические" был выбран для того, чтобы описать метод, не являющийся виртуальным, термин, который мы скоро введем. Виртуальные методы фактически являются решением этой проблемы, но для того, чтобы понять решение, Вам нужно вначале понять саму проблему). Симптомы этой проблемы таковы: до тех пор, пока копия MoveTo метода не будет помещена в сфере действия Circle для того, чтобы перекрыть Point.MoveTo, метод будет работать неправильно при вызове из объекта типа Circle. Если Circle вызывает Point.MoveTo метод, то на экране будет передвигаться скорее точка, а не окружность. Только когда Circle вызовет копию MoveTo метода, заданного в своей сфере действия, то окружности будут удаляться и рисоваться путем вложенных вызовов Show и Hide. Почему так? Это было сделано в соответствии со способом, которым компилятор решает вызовы методов. Когда компилятор компилирует Point методы, он сначала наталкивается на Point.Show и Point.Hide и компилирует коды для них обоих в сегмент кода. Немного ниже в файле он встречает Point.MoveTo, который вызывает Point.Show и Point.Hide. Как и при вызове любой процедуры, компилятор заменяет исходный код, ссылающийся на Point.Show и Point.Hide, адресами сгенерированного кода в сегменте кода. Таким образом, когда вызывается код для Point.MoveTo, он вызывает код для Point.Show и Point.Hide и все в порядке. Этот сценарий является классикой Turbo Pascal и поддерживается (кроме терминологии) с версии 1.0. Однако с появлением наследования все меняется. Когда Circle наследует метод от Point, Circle использует метод точно так, как он был откомпилирован. Посмотрим опять, что будет унаследовано Circle, если он унаследовал Point.MoveTo: procedure Point.MoveTo(NewX, NewY : Integer); begin Hide; {вызывает Point.Hide} X := NewX; Y := NewY; Show; {вызывает point.Show} end; Комментарии были добавлены для того, чтобы подчеркнуть факт, что когда Circle вызывает Point.MoveTo, он так же вызывает Point.Show и Point.Hide, а не Circle.Show и Circle.Hide. Point.Show рисует точку, а не окружность. Поскольку Point.MoveTo вызывает Point.Show и Point.Hide, Point.MoveTo не может быть наследован. Вместо этого он должен быть заменен второй копией самого себя, которая вызывает Show и Hide, определенные внутри своей сферы действия; т.е., Circle.Show и Circle.Hide. Логика компилятора в решении вызовов методов работает следующим образом: при вызове метода компилятор вначале ищет метод с этим именем, определенный внутри типа объект. Circle тип определяет методы, названные Init, Show, Hide, Expand, Contract и MoveTo. Если будет вызов одного из этих пяти методов Circle, то компилятор заменит вызов адресом одного из собственных Circle методов. Если метод с таким именем не определен внутри типа объект, то компилятор обращается к непосредственному родительскому типу и ищет внутри этого типа метод с таким именем. Если метод с этим именем найден, то адрес родительского метода заменяет имя в исходном коде метода потомка. Если метод с данным именем не найден, то компилятор поднимается выше к следующему предку в поисках названного метода. Если компилятор достиг самого первого типа объект (верхушки), то он выдаст сообщение об ошибке, указывающее, что этот метод неопределен. Но когда статический наследуемый метод найден и используется, Вы должны помнить, что вызванный метод - это метод точно такой, как он определен и откомпилирован для родительского типа. Если родительский метод вызывает другие методы, то вызываемые методы будут родительскими методами, даже если потомок имеет методы, которые отвергают родительские методы. Виртуальные методы и полиморфизм. Методы, обсуждаемые выше являются статическими. Они являются статическими по той же причине, по которой статические переменные являются статическими: компилятор размещает их и разрешает ссылки на них во время компиляции. Как Вы видите, объекты и статические методы могут быть мощным средством для организации сложных программ. Однако, иногда они являются далеко не лучшим способом для обработки методов. Проблемы, описанные в предыдущем разделе, обусловлены разрешением ссылок на методы во время компиляции. Способ решения этой проблемы - стать динамическими - и разрешать такие ссылки во время выполнения. Для того, чтобы такое стало возможным, нужны особые механизмы, а Turbo Pascal обеспечивает эти механизмы для поддержки виртуальных методов. Важно! Виртуальные методы реализуют чрезвычайно мощное средство для обобщения, называемого полиморфизмом. Полиморфизм - это слово из греческого языка, означающее "много форм", и здесь оно означает вот что: это способ задания одноименного действия, которое распределяется вверх и вниз по иерархии объектов, с выполнением этого действия способом, соответствующим каждому объекту в иерархии. Уже описанная простая иерархия графических фигур представляет собой хороший пример полиморфизма в действии, реализованного через виртуальные методы. Каждый тип объекта в нашей иерархии представляет различный тип фигуры на экране: точку или окружность. Определенно имеет смысл сказать, что Вы можете показать точку на экране или показать окружность. Позднее, если Вы захотите определить объекты для представления других фигур, таких как линии, квадраты, дуги и т.д., Вы можете написать метод для каждой фигуры, который изобразит этот объект на экране. В терминах нового объектно-ориентированного мышления можно сказать, что все эти типы графических фигур имеют способность изображения самих себя на экране. То, каким способом тип объекта должен изображать самого себя на экране, является различным для каждого типа объекта. Точка рисуется с помощью программы изображения точки, которой не требуется ничего, кроме X, Y позиции и возможно значения цвета. Для изображения окружности требуется совершенно отдельная графическая программа, принимающая в расчет не только X и Y, но так же и радиус. Немного дальше мы увидим, что дуге требуется начальный и конечный угол и более сложный алгоритм вырисовки, принимающий их в расчет. Можно изобразить любую графическую фигуру, но механизм, посредством которого изображается каждая фигура предопределен. Одно слово "Show", используется для изображения многих форм. Это хороший пример того, что представляет собой полиморфизм, а виртуальные методы это то, как он реализуется в Turbo Pascal. Раннее связывание по сравнению с поздним связыванием. Различие между вызовом статического метода и вызовом виртуального метода - это различие между решением, сделанными сейчас и задержанным решением. Когда Вы кодируете вызов статического метода, Вы, по существу, говорите компилятору: "Ты знаешь, что я хочу. Делай вызов". Вызов виртуального метода, с другой стороны, подобен тому, что Вы говорите компилятору: "Ты не знаешь что я хочу - пока. Когда придет время - запроси образец". Подумайте над этой метафорой в терминах MoveTo проблемы, упомянутой в предыдущем разделе. Вызов Circle.MoveTo может идти только в одно место: ближайшую реализацию MoveTo вверх по иерархии объектов. В этом случае Circle.MoveTo все еще вызовет Point определение для MoveTo, поскольку Point - это ближайший вверх по иерархии объект для Circle. Предполагая, что порожденный тип не определяет свой собственный MoveTo, чтобы перекрыть MoveTo из Point, любой порожденный от Point тип будет все еще вызывать ту же самую реализацию MoveTo. Решение осуществляется во время компиляции и это все, что требуется сделать. Однако, когда MoveTo вызывает Show, это другая история. Каждый тип фигуры имеет свою собственную реализацию Show, потому-то, какая реализация Show вызывается в MoveTo, должно полностью зависеть от того, какой экземпляр объекта вызвал MoveTo. Это и является причиной того, почему вызов Show метода внутри реализации MoveTo должен быть задержанным решением: при компиляции кода для MoveTo не решить, вызов какого Show сделать. Информация недоступна во время компиляции, потому решение откладывается до времени выполнения, когда можно запросить экземпляр объекта, вызывающий MoveTo. Процесс, посредством которого вызовы статического метода разрешаются однозначно (одним методом) компилятором во время компиляции, называется ранним связыванием. Во время раннего связывания, вызываемый метод и вызывающий метод связываются при первом удобном случае, т.е. во время компиляции. При позднем связывании вызываемого метода и вызывающего метода они не могут быть связаны во время компиляции, поэтому механизм связывания проявляется позднее, когда вызов будет фактически сделан. Природа этого механизма интересна и хитроумна, и Вы увидите, как она работает немного позднее. Совместимость типов объектов. Наследование отчасти изменяет правила совместимости типов в Turbo Pascal. В добавление ко всему прочему, порожденный тип наследует совместимость типов со всеми типами предка. Эта расширенная совместимость типов имеет три формы : - между экземплярами объектов - между указателями на экземпляры объектов - между формальными и фактическими параметрами Однако во всех трех формах необходимо запомнить, что совместимость типов распространяется от потомка к предку. Другими словами, порожденные типы можно свободно использовать вместо типов предка, но не наоборот. Рассмотрим следующие объявления: type LocationPtr = ^Location; PointPtr = ^Point; CirclePtr = ^Circle; var Alocation : Location; APoint : Point; ACircle : Circle; PLocation : LocationPtr; PPoint : PointPtr; PCircle : CirclePtr; При наличии этих объявлений следующие присваивания являются законными: Alocation := APoint; APoint := ACircle; Alocation := ACircle; Обратные присваивания незаконны. Примечание: Родительскому объекту можно присваивать экземпляр любого порожденного им типа. Это понятие новое для Паскаля и, возможно, вначале трудно запомнить какой путь используется для совместимости типов. Думайте об этом таким образом : Источник должен полностью заполнять объект назначения. Порожденные типы содержат все, что содержат родительские типы посредством наследования. Следовательно, порожденный тип или имеет такой же размер, или (обычно) размер, больший, чем его предок, но никогда не меньший размер. Присваивание родительского объекта порожденному объекту может оставить некоторые поля порожденного объекта неопределенными после этого присваивания, что опасно и, следовательно, незаконно. В операторе присваивания только те поля, которые являются общими для обоих типов будут копироваться от источника в объект назначения. В предложении присваивания Alocation := ACircle; только поля X и Y ACircle будут копироваться в Alocation, так как только X и Y - это общие типы, которые имеют Circle и Location. Совместимость типов так же действует между указателями на типы объектов по тем же общим правилам, как и для экземпляров типов объектов. Указатели на потомков можно присваивать указателям на родителей. Эти присваивания указателей являются законными: PPoint := PCircle; PLocation := PPoint; PLocation := PCircle; Запомните, что обратные присваивания являются незаконными. Формальный параметр (или значение или переменная) заданного типа объекта может использовать в качестве фактического параметра объекта такого же типа или любого порожденного типа. Пусть процедура имеет следующий заголовок: procedure DragIt (Target : Point); тогда фактические параметры могут иметь тип Point или Circle, но не тип Location. Target может так же быть параметром типа var; применяются одни и те же правила совместимости типов. Примечание: Однако запомните, что есть коренное отличие между параметром типа значение и var. Var параметр - это указатель на фактический объект, передаваемый в качестве параметра, в то время как параметр типа значение является только копией фактического параметра. Более того, эта копия включает только поля и методы, включенные в тип, являющийся формальным значением параметра. Это означает, что фактический параметр буквально преобразуется в тип формального параметра. Параметр типа var более подобен приведению типов, при этом фактический параметр остается неизменным. Аналогично, если формальный параметр является указателем на тип объекта, то фактический параметр может быть указателем на такой же тип объекта или указателем на любой тип объекта, который порождается данным типом объекта. Пусть эта процедура имеет следующий заголовок: procedure figure.Add (NewFigure : Pointer); тогда фактические параметры должны иметь тип Pointer или CirclePtr, но не могут быть типа LocationPtr. Полиморфные объекты. При чтении предыдущего раздела у Вас мог возникнуть вопрос: если в качестве параметра можно передавать любой тип из типов, порождаемых типом параметра, то как тот, кто использует этот параметр, определяет какой тип объекта получен. Фактически пользователь не знает этого явно. Точный тип фактического параметра неизвестен во время компиляции. Он может быть любым типом объекта, порожденным от типа параметра var и поэтому называется полиморфным объектом. В чем же преимущество полиморфных объектов? Самое главное в том, что полиморфные объекты допускают обработку объектов, тип которых неизвестен во время компиляции. Это является новым способом мышления для Паскаля, когда пример необязательно должен проявляться сразу же. (Затем Вы удивитесь как это будет казаться естественным. Это будет тогда, когда Вы действительно станете программистом, использующим принципы объектно-ориентированного программирования). Предположим, что Вы написали пакет графических чертежных средств, который поддерживает различные типы фигур, точки, окружности, квадраты, прямоугольники, кривые и т.п. Может возникнуть необходимость написать в качестве части этого пакета подпрограмму, которая будет изображать графическую фигуру на экране с помощью указателя "мышь". Старый способ заключается в написании отдельных процедур для изображения графических фигур всех типов, поддерживаемых пакетом. Нужно написать процедуры DragCircle, DragSquare, DragRectangle и т.д. Даже если строгое типирование Паскаля разрешает это (и не забывайте, что всегда есть способы обойти строгое типирование) различие между типами графических фигур будет, по-видимому, препятствовать написанию общей подпрограммы изображения этих фигур. Кроме того, окружность не имеет сторон, квадрат имеет одну длину стороны, прямоугольник - две различные длины сторон и т.д. В этом случае искусные программисты на Turbo Pascal идут дальше и выполняют вышеописанную задачу следующим образом: передают запись графической фигуры процедуре DragIt как содержимое общего указателя. Внутри DragIt просматривают поле признака при фиксированном смещении внутри записи графической фигуры, чтобы определить какой тип имеет данная фигура и затем осуществляют вставку с помощью предложения case: case FigureIdTage of Point : DragPoint; Circle : DragCircle; Square : DragSquare; Rectangle : DragRectangle; Curve : DragCurve; Конечно, включение семнадцати небольших кусков программы внутрь одного куска является небольшим шагом вперед, но при этом остается нерешенной следующая проблема: что делать, если пользователь пакета определит несколько новых графических фигур, например, пользователь разрабатывает дорожные знаки и хочет работать с восьмиугольником для знака остановки. Пакет не имеет типа Octagon (восьмиугольник), поэтому DragIt не имеет метку этого типа в предложении case и, следовательно, откажется нарисовать новую восьмиугольную фигуру. Если эту запись передать DragIt, то фигура типа Octagon должна попасть в часть else предложения case как нераспознанная фигура. Очевидно, что построение программ пакета для продажи без исходного кода страдает из-за следующей проблемы: пакет может работать только типами данных, которые он "знает", т.е. с типами, которые были определены разработчиками пакета. Пользователь пакета не может расширить функции пакета в направлениях, не использованных разработчиками пакета. Что пользователь покупает, то он и получает. Выход состоит в использовании для объектов правил расширенной совместимости типов Turbo Pascal и проектировании прикладной задачи с использованием полиморфных объектов и виртуальных методов. Если процедура пакета DragIt написана так, чтобы она могла работать с полиморфными объектами, то она будет работать с любыми объектами, определенными внутри пакета - и с любыми порожденными объектами, которые Вы определили сами. Если типы объектов пакета используют виртуальные методы, то объекты и подпрограммы пакета могут работать с обычными графическими фигурами в собственных терминах этой фигуры. Виртуальный метод, который Вы определите сейчас, может вызываться файлом модуля .TPU пакета, который был написан и откомпилирован годом позже. Объектно-ориентированное программирование делает это возможным, а виртуальные методы являются ключом к решению этой проблемы. Понимание того, как виртуальные методы делают такие вызовы полиморфных методов возможными, требует знания некоторых основ, касающихся объявления и использования виртуальных методов. Виртуальные методы Метод становится виртуальным, если за его объявлением в типе объекта следует новое зарезервированное слово virtual. Запомните, если Вы объявили метод в родительском типе виртуальным, то все методы с таким же именем в любом порожденном типе должны быть так же объявлены виртуальными, чтобы избежать ошибки компиляции. Ниже приведены объекты графической формы, которые как Вы видите виртуализированы надлежащим образом. type Location = Object; X,Y : Integer; procedure Init (InitX, InitY : integer); function GetX : Integer; function GetY : Integer; end; Point = Object(Location) Visuble : Boolean; Constructor Init (InitX, InitY : Integer); procedure Show : virtual; procedure Hide : Boolean; procedure MoveTo (NewX, NewY : Integer); end; Circle = Object (Point) Radius : Integer; Constructor Init(InitX, InitY : Integer; InitRadius : Integer); procedure Show; virtual; procedure Hide; virtual; procedure Expand (ExpandBy : Integer); virtual; procedure Contract (ContractBy : integer); virtual; end; Прежде всего заметим, что метод MoveTo, показанный в последней итерации типа Circle, берется из определения типа Point. Для Circle больше нет необходимости перекрывать метод MoveTo из Point с помощью немодифицированной копии, откомпилированной внутри сферы действия Circle. Вместо этого MoveTo может быть наследована из Point, при этом все вызовы вложенных методов являются методами Circle, а не Point, что было бы верно в иерархии, состоящей только из статических объектов. Кроме того, заметим, что новое зарезервированное слово constructor заменяет зарезервированное слово procedure для процедур Point.Init и Circle.Init. Констрактор - это процедура специального типа, которая делает некоторую работу по настройке для механизма обработки виртуальных методов. Примечание: Каждый тип объекта, который имеет виртуальные методы, должет иметь констрактор (constructor). Более того, констрактор должен вызываться перед вызовом любого виртуального метода. Вызов виртуального метода без предварительного вызова констрактора может вызывать блокирование системы, кроме того, компилятор не имеет способа проверки порядка, в котором вызываются методы. Мы предполагаем использование идентификатора Init для констракторов объектов. Каждый отдельный экземпляр объекта должен инициализироваться с помощью отдельного вызова констрактора. Недостаточно проинициализировать один образец объекта, а затем присвоить этот образец дополнительным образцам. Хотя дополнительные образцы могут содержать правильные данные, они не будут проинициализированы с помощью предложений присваивания и будут вызывать блокирование системы при вызове их виртуальных методов. Например: var QCircle, RCircle : Circle {создает два экземпляра Circle} begin QCircle.Init(600, 100, 30); {вызов констрактора для Circle} RCircle := QCircle; {неверно} end; Что создают констракторы? Каждый тип объекта имеет в сегменте данных таблицу виртуальных методов (ВМТ). ВМТ содержит размер типа объекта и для каждого виртуального метода указатель на код, реализующий этот метод. Констрактор устанавливает связь между экземпляром, вызывающим констрактор и таблицей виртуальных методов данного типа объекта. Важно запомнить: для каждого типа объекта существует только одна таблица виртуальных методов. Отдельные экземпляры типа объекта (т.е. переменная этого типа) содержит связь с таблицей виртуальных методов - они не содержат эту таблицу. Констрактор устанавливает значение этой связи с таблицей виртуальных методов. Вот почему можно запустить выполнение в никуда, если вызвать виртуальный метод до вызова констрактора. Вызовы проверки допустимого диапазона для виртуальных методов. При разработке программы может возникнуть необходимость использовать преимущества сети безопасности, которую Turbo Pascal помещает ниже вызовов виртуальных методов. Если опция $R находится в активном состоянии {$R+}, то для вызовов всех виртуальных методов проверяется статус инициализации экземпляра, делающего вызов. Если экземпляр, делающий вызов, не инициализируется констрактором, то при выполнении происходит ошибка выхода за допустимый диапазон. Примечание: По умолчанию опция R находится в неактивном состоянии {$R-}. После того, как Вы отладили программу и уверены, что нет вызовов методов из неинициализированных экземпляров, можно в некоторой степени увеличить скорость выполнения программы, установив опцию $R в неактивное состояние {$R-}. Тогда вызовы методов из неинициализированных экземпляров больше проверяться не будут и, возможно, будет происходить блокирование системы, если такие вызовы обнаружены. Однажды виртуальный, всегда виртуальный. Заметим, что и Point и Circle имеют методы с именами Show и Hide. Все заголовки методов для Show и Hide помечены как виртуальные методы с помощью зарезервированного слова virtual. После того, как родительский тип объекта помечает метод, как виртуальный, все порожденные типы, которые реализуют метод с таким же именем должны помечать этот метод так же виртуальным. Другими словами, статический метод никогда не сможет перекрыть виртуальный метод. Если Вы попытаетесь это сделать, компилятор выдаст ошибку. Нужно всегда помнить, что заголовок метода нельзя изменять каким бы то ни было способом вниз по иерархии объектов, если метод сделан виртуальным. Каждое определение виртуального метода можно представлять воротами для всех таких методов. По этой причине заголовки для всех реализаций одного и того же виртуального метода должны быть идентичными вплоть до количества и типов параметров. Это не распространяется на статические методы. Статический метод, перекрывающий другой метод, может иметь другое количество параметров и другие типы параметров, если это необходимо. Пример позднего связывания. Чтобы показать как использовать полиморфные объекты с поздним связыванием в программе на Turbo Pascal, вернемся к модулю графических фигур, описанному ранее. Цель состоит в создании модуля, который экспортирует несколько объектов графических фигур (например Point и Circle) и обобщенные методы черчения любой из этих фигур на экране. Модуль, имеющий имя Figures, будет простой реализацией графического пакета, описанного ранее. Чтобы продемонстрировать Figures, построим простую программу, которая определяет новый тип объекта, неизвестный в модуле Figures, а затем использует виртуальные методы для изображения нового типа фигуры на экране. Подумаем о том, чем похожи графические фигуры и чем они отличаются. Различия очевидны, а что касается сходства, то все они включают очертания, углы и кривые, изображенные на экране. В простой графической программе, которую мы будем описывать, фигуры, изображаемые на экране, имеют следующие атрибуты: - они имеют позицию, задаваемую X,Y. Точка внутри фигуры, находящаяся в этой позиции X,Y называется точкой привязки фигуры. - они могут быть или видимыми, или невидимыми, что задается булевским значением True (видимый) или False (невидимый). Если посмотреть ранее приведенные примеры, то перечисленные атрибуты являются точной характеристикой типов объектов Location и Point. Point фактически представляет собой разновидность "дедушкиного" типа, который порождает все объекты графических фигур. Логическое обоснование, разумное объяснение демонстрирует важный принцип объектно-ориентированного программирования: определяя иерархию типов объектов, соберите все общие атрибуты в один тип, и определите иерархию типов так, чтобы все общие элементы наследовались из этого типа. Примечание: Замечание об абстрактных объектах. Тип Point действует как шаблон, из которого все порожденные типы объектов могут взять элементы, общие для всех типов в иерархии. В этом примере объект типа Point не будет фактически изображаться на экране, хотя от этого не будет никакого вреда. (Вызов Point.Show вызовет появление точки на экране). Тип объекта, специально определенного для обеспечения наследования характеристик порожденными объектами, называется абстрактным типом объекта. Главная суть абстрактного типа в том, что он должен иметь порожденные типы, а не экземпляры. Прочитаем Point еще раз, на этот раз как резюме всех атрибутов, которые являются общими для графических фигур. Point наследует X и Y из еще более раннего типа Location, но тем не менее Point содержит X и Y и может передавать их порожденным типам. Заметим, что методы Point не обращаются к форме фигуры, но все фигуры могут быть видимыми или невидимыми и перемещаться по экрану. Point так же имеет важную функцию "передающей станции" для изменений в иерархии объектов в целом. Если добавляются некоторые новые свойства, которые применяются ко всем графическим фигурам (поддержка цвета, например), то их можно добавлять ко всем типам объектов, происходящим от Point, просто добавив эти новые свойства к Point. Тогда новые свойства можно вызывать из любого типа, порожденного Point. Метод для перемещения фигуры в текущую позицию с помощью манипулятора "мышь", можно, например, добавить к Point, не изменяя методы, специфические для каждой фигуры, так как такой метод будет действовать только на поля X и Y. Очевидно, что если новое свойство должно реализовываться различным образом для различных фигур, то должен быть целый набор виртуальных методов, специфических для каждой фигуры, добавляемый в иерархию, при этом каждый метод перекрывает метод, принадлежащий непосредственному родителю. Цвет, например, должен требовать минимальные изменения в процедурах Show и Hide, так как синтаксис многих чертежных подпрограмм Graph.TPU зависит от того, как задается цвет изображения. Процедура или метод? Главная цель разработки модуля Figures.PAS состоит в предоставлении пользователям возможности расширять типы объектов, определенные в модуле и еще использовать все свойства модулей. Интересная задача состоит в том, чтобы создать несколько способов изображения произвольной графической фигуры на экране в ответ на ввод пользователя. Есть два способа реализации этой цели. Способ, который первый приходит в голову традиционному программисту на Паскале - заставить FIGURES.PAS экспортировать процедуру, которая берет полиморфный объект в качестве параметра var, а затем изображает данный объект на экране. Такая процедура приведена ниже: procedure DragIt (var AnyFigure : Point; DragBy : integer); var DeltaX, DeltaY : Integer; FigureX, FigureY : Integer; begin AnyFigure.Show {чтобы изображаемая фигура была видна на экране} FigureX := AnyFigure.GetX; {получаем начальные значения X и FigureY := AnyFigure.GetY; Y для данной фигуры } {Ниже приведен цикл, в котором чертится фигура} while GetDelta (DeltaX, DeltaY) do begin {применим Delta к X,Y} FigureX := FigureX + (DeltaX + DragBy); FigureY := FigureY + (DeltaY + DragBy); {затем передвигаем фигуру} end; end; DragIt вызывает дополнительную процедуру GetDelta, которая получает от пользователя изменения X и Y в некоторой форме. Ввод можно осуществлять с помощью клавиатуры, манипулятора "мышь" или джойстика. Для простоты в нашем примере ввод осуществляется с помощью клавиш-стрелок на клавиатуре. Что касается DragIt, то нужно отметить, что любой объект типа Point или объект любого типа, порожденного Point, можно передать в var параметр AnyFigure. Экземпляры типа Point или Circle, или экземпляры любого порожденного Point или Circle типа, который будет определен в будущем, могут передаваться без компиляции в AnyFigure. Как код DragIt узнает, какой тип объекта фактически передан? DragIt не знает тип и это хорошо. DragIt только делает ссылку на идентификаторы, определенные в типе Point. Благодаря наследованию, эти идентификаторы определены так же в любом типе, порожденном Point. Методы GetX, GetY, Show и MoveTo фактически присутствуют в типе Circle так же, как в типе Point и будут присутствовать в любом будущем типе, определенном в качестве потомка или Point или Circle. GetX, GetY и MoveTo статические методы, это означает, что DragIt знает адрес каждой процедуры во время компиляции. С другой стороны, Show - виртуальный метод. Есть различные реализации Show для Point и Circle - и DragIt не знает во время компиляции какая реализация будет вызываться. Короче говоря, когда DragIt вызывается, она ищет адрес правильной реализации Show в таблице виртуальных методов, переданного в AnyFigure. Если экземпляр имеет тип Circle, то DragIt вызывает Circle.Show. Если экземпляр имеет тип Point, то DragIt вызывает Point.Show. Решение о том, какая реализация Show будет вызываться не принимается вплоть до времени выполнения, фактически, вплоть до того момента в программе, когда DragIt должна вызвать виртуальный метод Show. Теперь DragIt работает достаточно хорошо и, если она экспортируется модулем пакета, то она может изобразить любой тип, порожденный Point, на экране независимо от того, существует этот тип при компиляции пакета или нет. Но нужно думать немного дальше: если любой объект может быть изображен на экране, то почему не сделать вычерчивание свойством самого графического объекта? Другими словами, почему не сделать DragIt методом? Сделаем DragIt методом! Действительно. Почему нужно передавать объект процедуре, чтобы изобразить объект на экране? Это старый способ мышления. Если можно написать процедуру для вычерчивания всех объектов графических фигур на экране, то тогда объекты графических фигур должны быть в состоянии изобразить себя на экране. Другими словами, процедура DragIt действительно должна быть методом Drag. Чтобы добавить новый метод к существующей иерархии объектов, нужно немножко подумать, где расположить метод в иерархии. Подумайте об утилите, предоставляемой методом и решите насколько широко она будет применяться. Вычерчивание фигуры включает изменение позиции фигуры в ответ на ввод пользователя. Метафорически можно считать, что метод Drag аналогичен методу MoveTo c внутренним источником энергии. В терминах наследования он расположен непосредственно рядом с MoveTo - любой объект, которому подходит MoveTo, должен так же наследовать и Drag. Таким образом, Drag должен добавляться к нашему абстрактному типу объектов Point, так чтобы все потомки Point могли использовать этот метод. Нужно ли, чтобы Drag был виртуальным методом? Лакмусовый тест на необходимость виртуализации метода состоит в том, ожидается или нет изменение функциональности метода где-нибудь ниже в иерархическом дереве. Drag - это свойство замкнутого-законченного типа. Drag манипулирует только с позицией X,Y фигуры и трудно предположить, что для Drag потребуются какие-нибудь изменения. Следовательно, вероятно, что этот метод не нужно делать виртуальным. Будьте осторожны при любом таком решении. Если Вы не сделаете Drag виртуальным, то Вы исключите все возможности для пользователей FIGURES.PAS изменить этот метод при расширении FIGURES.PAS. Возможно, что Вы не сможете представить обстоятельства, при которых пользователь может захотеть переписать Drag. Но это не означает, что такие обстоятельства не могут возникнуть. Например, Drag имеет непредвиденное обстоятельство, которое перевешивает баланс в пользу виртуализации метода. Drag имеет дело с обработкой событий, т.е. перехват ввода от устройств, подобных клавиатуре и манипулятору "мышь", происходит в непредсказуемые моменты времени, эти события должны тем не менее обрабатываться в тот момент, когда они произошли. Обработка событий - беспорядочное дело и очень зависит от аппаратуры. Если пользователь имеет несколько устройств ввода, которые не объявлены в Drag в том виде, как он представлен, то пользователь будет не в состоянии переписать Drag. Не сжигайте все мосты. Сделайте Drag виртуальным методом. Процесс преобразования DragIt в метод и добавление метода к Point является почти тривиальным. Внутри определения объекта Point Drag - это просто еще один заголовок метода: Point = Object (Location) Visible : boolean Constructor Init (InitX, InitY : integer); procedure Show : virtual; procedure Hide : virtual; function IsVisible : Boolean; procedure MoveTo (NewX, NewY : integer); procedure Drag (DragBy : integer) : virtual; end; Позиция заголовка метода Drag в определении объекта Point не имеет значения. Запомните, что методы могут объявляться в любом порядке, а поля данных должны определяться перед первым объявлением метода. Сущность преобразования процедуры DragIt в метод Drag практически полностью состоит в применении сферы действия Point к DragIt. В процедуре DragIt нужно определить методы AnyFigure.Show и AnyFigure.GetX и т.д. Drag теперь является частью Point, поэтому квалифицировать имена методов больше нет необходимости. AnyFigure.GetX - это теперь просто GetX и т.д. И, конечно, var параметр AnyFigure исключается из строки параметров. Неявный параметр Self теперь дает информацию о том, какой экземпляр объекта называется Drag. Примечание: Полный исходный код для FIGURES.PAS, включающий Drag, реализованный как виртуальный метод, есть на Вашем диске. До сих пор Вы должны были думать в терминах построения функциональности в объектах в форме методов, а не в терминах построения процедур и передачи объектов процедурам в качестве параметров. В конце концов Вы придете к построению программ в терминах деятельности, которую могут выполнять объекты, а не к построению программ в виде наборов вызовов процедур, которые имеют дело с пассивными данными. Это целый новый мир. Расширяемость объектов. Нужно отметить важную особенность, касающуюся модулей, подобных FIGURES.PAS - типы объектов и методы, определенные в модуле, могут передаваться пользователям только в отредактированной форме .TPU без исходного кода. Только список интерфейсной части модуля необходимо разъединить. Используя полиморфные объекты и виртуальные методы, пользователь .TPU файла может добавить к нему свойства, отвечающие его потребностям. Это новое замечание о взятии кода программы, написанного кем-либо и добавлении к нему функциональности без помощи исходного кода называется расширяемостью. Расширяемость - это естественный результат наследования. Вы наследуете все, что имеют все родительские типы, а затем добавляете новые возможности, которые Вам необходимы. Позднее связывание допускает объявления новых объектов вместе со старыми объектами во время выполнения, поэтому расширение существующего кода не нарушает его целостности и затраты на его осуществление не больше, чем при быстром проходе по таблице виртуальных методов. FIGDEMO.PAS (на Вашем диске) использует модуль Figures и расширяет его, создавая новый объект графической фигуры Arc в качестве типа, порожденного Circle. Объект Arc может быть написан намного позднее компиляции FIGURES.PAS, кроме того, объект типа Arc может использовать наследуемые методы, подобно MoveTo или Drag без специального рассмотрения. Позднее связывание и виртуальные методы Arc позволяют методу Drag вызывать методы Arc.Show и Arc.Hide даже если эти методы написаны намного позднее компиляции Point.Drag. Статические или виртуальные методы. В общем нужно сделать все методы виртуальными. Используйте статические методы только, когда нужно оптимизировать скорость и эффективность использования памяти. Компромисc, как Вы видите, заключается в расширяемости. Предположим, что Вы объявили объект с именем Ancestor, а внутри объекта Ancestor Вы объявили метод с именем Action. Как определить нужно ли делать Action виртуальным или статическим? Здесь используется правило : Сделаем Action виртуальным, если есть возможность, что некоторые будущие потомки объекта Ancestor будут перекрывать Action и Вы хотите, чтобы будущий код был доступным для Ancestor. Теперь применим это правило к графическим объектам, которые Вы видели в этой главе. В этом случае Point - родительский тип объекта и Вы должны решить делать его методы статическими или виртуальными. Рассмотрим методы Point : Show, Hide и MoveTo. Так как каждый различный тип фигуры имеет свои средства изображения и удаления с экрана, то Show и Hide будут перекрываться любой порожденной фигурой, однако перемещение графической фигуры, по-видимому, будет одинаковым для всех потомков: вызов Hide, чтобы удалить фигуру, изменение координат X и Y, а затем вызов Show, чтобы повторно вывести фигуру на экран в новой позиции. Так как этот алгоритм MoveTo можно применить к любой фигуре с одной точкой отсчета Х,Y, то разумно сделать Point.MoveTo статическим методом, который будет наследоваться всеми потомками Point, но Show и Hide будут перекрываться и должны быть виртуальными так, чтобы Point.MoveTo могла вызвать методы Show и Hide, имеющиеся у потомков Point. С другой стороны, запомним, что если объект имеет какие-нибудь виртуальные методы, то в сегменте данных будет создана таблица виртуальных методов для этого типа объекта и каждый экземпляр объекта будет иметь связь с этой таблицей. Каждый вызов виртуального метода должен передаваться таблице виртуальных методов, в то время как статические методы вызываются непосредственно. Хотя просмотр таблицы виртуальных методов и осуществляется очень эффективно, вызов статического метода происходит несколько быстрее, чем вызов виртуального метода. А если в Вашем объекте нет виртуальных методов, то в сегменте данных таблицы виртуальных методов нет - и что более важно - нет связи с этой таблицей в каждом экземпляре объекта. Дополнительная скорость и эффективность использования памяти у статических методов должна сопоставляться относительно гибкости, которую предоставляют виртуальные методы: расширение существующего кода после компиляции этого кода. Помните, что пользователи Вашего типа объектов могут думать о таких способах использования этого объекта, о которых Вы можете никогда и не предполагать, что в конце концов очень важно. Динамические объекты. Все примеры объектов, рассмотренные до сих пор, имели статические экземпляры типов объектов, которые были перечислены в объявлениях var и размещены в сегменте данных и в стеке. var ACircle : Circle; Примечание: Использование слова "статический" не связано никаким образом со статическими методами. Объекты могут быть размещены в куче и можно ими манипулировать с помощью указателей, это же имеет место и для тесно связанных с ними типов записей, которые всегда были в Паскале. Turbo Pascal включает несколько мощных расширений для обеспечения более легкого и эффективного размещения и освобождения объектов. Объекты можно разместить как указатель, на который ссылается процедура New: var PCircle := ^Circle; New(PCircle); Так же как и для типов записей, New выделяет объем памяти в куче для размещения экземпляра базового типа указателя и возвращает адрес выделенного объема в указателе. Если динамический объект содержит виртуальные методы, то его нужно затем проинициализировать с помощью вызова констрактора перед любыми вызовами методов этого объекта: PCircle^.Init(600,100,30); Вызовы методов затем могут осуществляться обычным образом с помощью имени указателя и символа ссылки ^(знак вставки), используемых вместо имени экземпляра, которое применялось бы в вызове статически размещенного объекта: OldXPosition := PCircle^.GetX; Размещение и инициализация с помощью New. Turbo Pascal расширяет синтаксис New, чтобы использовать более компактные и удобные способы распределения памяти для объекта, находящегося в куче и инициализации объекта с помощью одной операции. New может теперь вызываться с помощью двух параметров: в качестве первого параметра используется имя указателя, а второго параметра - вызов констрактора: New(PCircle, Init(600,100,30)); Когда для New используется такой расширенный синтаксис, констрактор Init фактически выполняет динамическое распределение, используя специальный код ввода, генерируемый как часть компиляции констрактора. Имя экземпляра не может предшествовать Init, так как во время вызова New экземпляр, инициализируемый с помощью Init, еще не существует. Компилятор идентифицирует правильный метод Init, который нужно вызывать, с помощью указателя, передаваемого в качестве первого параметра. New так же была расширена, чтобы она действовала как функция, возвращающая значение указателя. Параметр, передаваемый New, является типом указателя на объект, а не переменной указателя. type ArcPtr = ^Arc; var PArc : ArcPtr; PArc := New(ArcPtr); Заметим, что расширение New до функциональной формы применяется ко всем типам данных, а не только к типам объектов. type CharPtr = ^Char; {Char - это не тип объекта ...} var PChar : CharPtr; PChar := New(CharPtr); Функциональная форма New, так же как и процедурная форма, может принимать констрактор типа объекта в качестве второго параметра: PArc := New(ArcPtr, Init(600,100,25,0,90)); В Turbo Pascal было параллельно определено расширение Dispose, которое объясняется в следующих разделах. Примечание: Новая стандартная процедура Fail поможет Вам обнаружить ошибку в констракторах. См. раздел "Восстановление ошибок констрактора" в главе 17. Освобождение динамических объектов. Так же как и традиционные записи Паскаля объекты, размещенные в куче, могут быть освобождены с помощью Dispose, когда они уже больше не нужны: Dispose(PCircle); Тем не менее избавление от ненужного динамического объекта может включать не только освобождение его пространства в куче. Объект может содержать указатель на динамические структуры или объекты, которые необходимо освободить или "очистить" в определенном порядке, особенно когда они включают сложные динамические структуры данных. То, что нужно сделать для того, чтобы очистить динамический объект в определенном порядке, нужно объединить в один метод так, чтобы объект можно было исключить с помощью одного вызова метода: MyComplexObject.Done; Метод Done должен объединять все детали исключенного объекта и все структуры данных и объекты, вложенные в данный объект. Примечание: Идентификатор Done используется для методов очистки, которые "закрывают магазин" после того, как объект становится ненужным. Это законно и часто используется, чтобы определить многочисленные методы очистки для заданного типа объекта. Возможно, что сложные объекты нужно будет очищать различными способами в зависимости от того, как они были размещены или использованы, или в зависимости от того, в каком режиме или состоянии был объект, когда он удаляется. Дестракторы. Turbo Pascal предоставляет специальный тип метода, называемый дестрактором, для очистки и удаления динамически распределенных объектов. Дестрактор комбинирует шаг освобождения памяти в куче с некоторыми другими задачами, которые необходимы для данного типа объекта. Аналогично другим методам для одного типа объектов, можно определить несколько дестракторов. Дестрактор определяется вместе со всеми другими методами объекта в определении типа объекта: Point = object(Location) Visible : Boolean; Next : PointPtr; constructor Init(InitX, InitY : Integer); destructor Done; virtual; procedure Show; virtual; procedure Hide; virtual; function IsVisible : Boolean; procedure MoveTo(NewX, NewY : Integer); procedure Drag(DragBy : Integer); virtual; end; Дестракторы можно наследовать и они могут быть или статическими, или виртуальными. Так как для различных типов объектов обычно требуются различные задачи освобождения памяти, мы рекомендуем, чтобы дестракторы всегда были виртуальными так, чтобы в любом случае выполнялся бы соответствующий для данного типа объекта дестрактор. Запомните, что зарезервированное слово destructor нужно для любого метода освобождения памяти даже, если определение данного типа объекта содержит виртуальные методы. Дестракторы фактически действуют только на динамически распределенные объекты. При очистке динамически распределенного объекта дестрактор выполняет следующее: он гарантирует, что всегда будет освобождаться правильное количество байтов в куче. Тем не менее нет вреда при использовании дестракторов со статически распределенными объектами; фактически, если тип объекта не имеет дестрактора, то объекты этого типа лишаются преимущества динамического управления памятью Turbo Pascal. Действие дестракторов наиболее проявляется когда очищаются полиморфные объекты и когда освобождается память, занимаемая этими объектами в куче. Полиморфный объект - это объект, который был назначен родительскому типу на основании правил расширенной совместимости типов Turbo Pascal. В примере с графическими фигурами экземпляр объекта типа Circle, присвоенный переменной типа Point, является примером полиморфного объекта. Эти правила применяются так же и для указателей. Указатель на Circle можно свободно присвоить указателю на тип Point и содержимое этого указателя так же будет полиморфным объектом. Термин "полиморфный" является подходящим для этого случая, потому что код, использующий такой объект, не знает точно во время компиляции какой тип объекта находится в конце строки, а знает только то, что этот объект будет одним из объектов, порожденных заданным типом. Очевидно, что размеры типов объектов различаются. Поэтому когда приходит время очищать полиморфный объект, размещенный в куче, то возникает вопрос, как Dispose узнает сколько байтов в куче нужно освободить? Во время компиляции из полиморфного объекта нельзя получить никакой информации о размере объекта. Дестрактор решает проблему посредством обращения к месту, где эта информация хранится: в переменных таблицы виртуальных методов данного экземпляра. В каждой таблице виртуальных методов есть размер в байтах типа объекта, к которому относится эта таблица. Таблица виртуальных методов для любого объекта доступна с помощью неявного параметра Self, передаваемого методу при любом его вызове. Дестрактор - это просто особый тип метода и при вызове он получает копию Self в стеке. Поэтому в то время, как объект может быть полиморфным во время компиляции, он никогда не может быть полиморфным во время выполнения благодаря позднему связыванию. Чтобы выполнить освобождение памяти при позднем связывании, дестрактор нужно вызвать как часть расширенного синтаксиса для процедуры Dispose: Dispose(PPoint, Done); (Вызов дестрактора за пределами вызова Dispose не выполняет автоматическое освобождение). В приведенном примере дестрактор объекта, определенного указателем PPoint, выполняется как обычный вызов метода. Тем не менее при выполнении дестрактор ищет размер типа экземпляра в таблице виртуальных методов этого экземпляра и передает этот размер Dispose. Dispose завершает выполнение посредством освобождения правильного количества байтов в куче, принадлежащих ранее PPoint^. Освобождаемое количество байтов будет правильным, если PPoint указывает на экземпляр типа Point или экземпляр любого типа, порожденного Point, например, Circle или Arc. Заметим, что метод дестрактора сам может быть пустым и все равно выполнять свою функцию: destructor AnObject.Done; begin end; Полезную работу в этом дестракторе выполняет не тело метода, а эпилоговый код, генерируемый компилятором в ответ на зарезервированное слово destructor. В этом дестрактор подобен модулю, который не экспортирует ничего, но выполняет некоторую "неявную" функцию посредством выполнения раздела инициализации перед запуском программы. Все действие происходит неявно. Пример распределения динамического объекта. Последний пример программы дает некоторый практический опыт в использовании объектов, размещаемых в куче, включая использование дестракторов для освобождения объектов. Программа показывает как в куче может быть создан связанный список графических объектов и как можно освобождать память посредством использования вызовов дестрактора, когда объект уже не нужен. Построение связанного списка объектов требует, чтобы каждый объект содержал указатель на следующий объект в списке. Тип Point не содержит такого указателя. Наиболее легкий выход из положения состоит в добавлении указателя к Point, это обеспечивает то, что все типы, порожденные Point так же наследуют этот указатель. Тем не менее, добавление указателя к Point требует, чтобы был исходный код Point, а как упоминалось выше, одно из преимуществ объектно-ориентированного программирования состоит в возможности расширения существующих объектов без необходимости перекомпиляции исходного кода. Решение, которое не требует изменения в Point, заключается в создании нового типа объекта, не порождаемого Point. Тип List - это очень простой объект, назначение которого находится в вершине списка объектов Point. Так как Point не содержит указателя на следующий объект в списке, простой тип записи Node выполняет эту функцию. Node еще проще, чем List, так как это не объект и Node не имеет методов и не содержит данные, за исключением указателя на тип Point и указателя на следующий узел в списке. List имеет метод, который позволяет добавлять новые фигуры в связанный список записей Node посредством вставки нового экземпляра Node сразу после него в виде содержимого полей указателей на записи Node данного экземпляра. Метод Add использует в качестве параметра указатель на объект Point, а не сам объект Point. Благодаря расширенной совместимости типов в Turbo Pascal указатели на любой тип, порожденный Point, так же могут передаваться в параметр Item метода List.Add. Программа ListDemo объявляет статическую переменную AList типа List и создает связанный список с тремя узлами. Каждый узел указывает на определенную графическую фигуру, которая или имеет тип Point, или является одним из потомков Point. Количество байтов в свободном пространстве кучи сообщается перед созданием любого динамического объекта и затем еще раз после того, как все объекты будут созданы. В заключение, вся структура, включающая три записи Node и три объекта Point очищается и удаляется из кучи с помощью единственного вызова дестрактора для AList статического объекта типа List. Рисунок 1.2 Схема структур данных программы ListDemo. │ Список │ Узел Узел Узел ┌───────┐ ┌────┬────┐ ┌────┬────┐ ┌────┬────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ O──┼────Ў │ O │ O─┼───Ў │ O │ O─┼────Ў │ O │ O─┼───┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └───────┘ └──┼─┴────┘ └──┼─┴────┘ └──┼─┴────┘ │ │ │ │ │ ───┴─── ∙ ∙ ∙ ───── │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐─── │ X │ │ X │ │ X │ ─ │ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ Y │ │ Y │ │ Y │ │ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ Visible │ │ Visible │ │ Visible │ │ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ │ │ │ │ │ Сегмент │ "Куча" данных (динамические (статические │ объекты) объекты) │ Освобождение сложной структуры данных в куче. List.Done заслуживает более подробного рассмотрения. Исключение объекта типа List включает освобождение трех различных типов структур: полиморфных объектов графических фигур в списке, записей Node, которые связывают список вместе и если List расположен в куче, то еще и объект List, который находится в вершине списка. Единственный вызов дестрактора AList порождает целый процесс: AList.Done; Код для дестрактора заслуживает рассмотрения: destructor List.Done; var N : NodePtr; begin while Nodes <> Nil do begin N := Nodes; Dispose(N^.Item, Done); Nodes := N^.Next; Dispose(N); end; end; Список очищается, начиная с вершины с помощью "быстрого" алгоритма, метафорически подобного втягиванию бечевки воздушного змея. Два указателя: указатель Nodes внутри AList и рабочий указатель N изменяют свое содержимое, когда первый элемент списка удаляется. Вызов Dispose освобождает память для первого объекта Point в списке (Item^); тогда Nodes перемещается к следующей записи Node в списке с помощью оператора Nodes := N^.Next; Сама запись Node освобождается и процесс повторяется до тех пор, пока список не закончится. В дестракторе Done следует обратить особое внимание на способ, каким удаляются в списке объекты Point: Dispose(N^.Item, Done); Здесь N^.Item - первый объект Point в списке и вызываемый метод Done представляет собой дестрактор этого объекта. Запомните, что фактический тип N^.Item не обязательно тип Point, он может быть любым типом, порожденным Point. Освобождаемый объект является полиморфным объектом и нельзя сделать никаких предположений о его действительном размере или точном типе во время компиляции. При вызове Dispose, когда Done выполнит все операторы, которые он содержит, "неявный" эпилоговый код Done находит размер освобождаемого экземпляра объекта в таблице виртуальных методов данного экземпляра. Done передает этот размер Dispose, которая затем освобождает в памяти кучи точное количество байтов, которые действительно занимал данный полиморфный объект. Запомните, что полиморфные объекты должны очищаться таким же способом посредством вызова дестрактора, передаваемого Dispose, если необходимо надежно освободить память в куче соответствующего размера. В приведенном примере программы AList объявляется статической переменной в сегменте данных. AList могла бы легко расположить себя в куче и ссылаться на нее можно было бы с помощью указателя типа ListPtr. Если вершина списка была динамическим объектом, то удаление структуры нужно выполнить с помощью вызова дестрактора, выполняемого внутри Dispose: var PList : ListPtr; ... Dispose(PList, Done); Здесь Dispose вызывает метод дестрактора Done, чтобы освободить структуру в куче. Затем, когда Done завершается, Dispose освобождает память, занимаемую содержимым PList, удаляя вершину списка из кучи. LISTDEMO.PAS (на Вашем диске) использует модуль FIGURES.PAS, описанный выше. Она реализует тип Arc как потомок Point, создает объект List, определяет его в качестве вершины связанного списка из трех полиморфных объектов, совместимых с Point, а затем освобождает всю динамическую структуру данных с помощью единственного вызова дестрактора AList.Done. Куда теперь? Как и для любого аспекта программирования освоить объектно-ориентированное программирование лучше не читая про него, а занимаясь им. От большинства людей при первом их обращении к объектно-ориентированному программированию можно услышать ворчание: "Мне это не нужно". Удовлетворение приходит позже, когда человек сосредотачивается и вся концепция собирается вместе в виде определенного момента, который мы называем прозрением. Это подобно тому, как лицо женщины, появляющееся из чернильного пятна Роршаха, которое вначале было скрыто, затем сразу же становится очевидным и после этого Вам становится легко и спокойно. Наилучшее, что можно сделать для первого объектно-ориентированного проекта - взять модуль FIGURES.PAS, (Вы имеете его на диске) и расширить его. Точки, окружности и дуги никоим образом не достаточны. Создайте объекты для линий, прямоугольников и квадратов. Когда Вы почувствуете себя более честолюбивым, создайте объект "круговая диаграмма", используя связанный список индивидуальных фигур, представляющих собой сектора круга. Наиболее подходящим способом решения данной задачи является реализация объектов с относительными позициями. Относительная позиция - смещение от некоторой базовой точки, выраженное ввиде положительной или отрицательной разности. Точка с относительными координатами -17,42 находится на 17 пикселов слева от базовой точки и на 42 пиксела ниже от базовой точки. Относительные позиции необходимы, чтобы эффективно скомбинировать фигуры в одну большую фигуру, так как фигуры, состоящие из нескольких фигур, никогда нельзя связать вместе в любой точке отсчета фигуры. Лучше дополнительно к точке отсчета X,Y определить поле RX и RY, тогда окончательная позиция объекта на экране будет равна сумме точек отсчета и относительных координат. После того, как Вы "прозрели", начните построение объектно-ориентированных понятий при программировании каждый день. Возьмите несколько существующих утилит, которые Вы используете каждый день и обдумайте их в терминах объектно-ориентированного программирования. Затем посмотрите на Ваши беспорядочные библиотеки процедур и постарайтесь увидеть в них объекты - тогда перепишите процедуры в объектной форме. Вы обнаружите, что библиотеки объектов легче использовать в будущих проектах. Очень немного из Вашего начального вклада в программирование пропадет зря. У Вас редко будет возникать необходимость переписать объект. Если он будет служить, как он есть, то используйте его. Если у объекта что-то отсутствует, то расширьте его. Но если объект работает хорошо, то нет причин выбрасывать что-либо из него. Заключение. Объектно-ориентированное программирование - это прямой ответ на сложность современных прикладных задач, их сложность часто заставляет многих программистов вскидывать руки в отчаянии. Наследование и инкапсуляция являются очень эффективными способами для управления сложностью. (Есть разница между десятью тысячами насекомых, классифицированных в таксономической схеме и десятью тысячами насекомых, жужжащих около Ваших ушей). В большей степени, чем структурное программирование объектно-ориентированное программирование обуславливает рациональную последовательность в программных структурах, что подобно таксономической схеме обуславливает такую последовательность, не налагая никаких ограничений. Добавим к этому обещание расширяемости и повторной используемости существующего кода, тогда все это начинает выглядеть слишком хорошо и возникают сомнения, правда ли это? Вы думаете, что это невозможно? Нет, это Turbo Pascal. Нет ничего невозможного.