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




Область действия (domain) полей данных объекта неявно распространяется на тела процедур и функций, реализующих методы этого объекта. В примере на рис. 13.1 метод Init (процедура) работает с полями Line и Col объекта типа ObjPos, обращение к которым не требует указания имени объекта. Можно сказать, что внутри методов объекта действует неявный оператор WITH. Следствием этого является то, что формальные параметры метода (если присутствуют) не могут совпадать по имени ни с одним из полей данных соответствующего объекта. {273}

Мы не раз подчеркивали, что внешне объекты очень похожи на записи. Так же, как и записи, они могут явно записываться в теле оператора WITH:

VAR

ObjPosVar: ObjPos; { экземпляр объекта }

...

with ObjPosVar do { оператор присоединения }

begin

Init(1,1); { имя экземпляра опущено }

...

Init(GetLine+1, GetCol+1);

...

end; { with }

Всякий раз, когда вызывается метод какого-либо объекта, в него, кроме фактических параметров, передается невидимый параметр Self («свой», «внутренний»). Он указывает, какому объекту принадлежит метод. Так, метод Init из примера на рис. 13.1 воспринимается компилятором так, как если бы он был описан следующим образом:

PROCEDURE ObjPos.Init(init_line, init_col: Word);

BEGIN

Self.Line:= init_line; { метод задания номера строки }

Self.Col:= init_col { метод задания номера столбца }

END;

Компилятор автоматически обрабатывает параметр Self, поэтому не стоит использовать его явно. Исключением является случай, когда идентификаторы начинают «конфликтовать» в пределах методов. На рис. 13.2 показано, как, используя в методе объекта параметр Self, разрешить конфликт между полями самого объекта и формального параметра (записи).

TYPE PosRec = RECORD { запись } Line, Col: Word; { номера строки и столбца } END; ObjPos = OBJECT { объект } Line, Col: Word; { номера строки и столбца } PROCEDURE Init2(Pos: PosRec); { метод } END;

Рис. 13.2 {274}

PROCEDURE ObjPos.Init2(Pos: PosRec); BEGIN with Pos do begin { присоединение для записи Pos } Self.Line:= Line; { Self развязывает одинаковые имена} Self.Col:= Col end {with} END;

Рис. 13.2 (окончание)

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

При помощи объекта типа ObjPos (см. рис. 13.1) определяется положение какого-либо символа в тексте на дисплее, но сам символ в нем не определен. Объявим объект с именем ObjSym, добавляющий символ и выполняющий определенные действия с ним (рис. 13.3).

USES CRT; { в примере используется системный модуль CRT } TYPE ObjSym = OBJECT Line: Word; { номер строки с Sym } Col: Word; { номер столбца с Sym } Sym: Char; { поле-значение символа } PROCEDURE Init(init_line,init_col: Word; init_sym: Char); FUNCTION GetLine: Word; { опрос Line } FUNCTION GetCol: Word { опрос Col } PROCEDURE Print { вывод Sym } END; PROCEDURE ObjSym.Init; { инициализация полей объекта } BEGIN Line:= init_line; { метод задания номера строки } Col:= init_col; { метод задания номера столбца } Sym:= init_sym { задание значения символа } END; FUNCTION ObjSym.GetLine: Word; BEGIN GetLine:= Line { метод опроса номера строки } END;

Рис. 13.3 {275}

FUNCTION ObjSym.GetCol: Word; BEGIN GetCol:= Col { метод опроса номера столбца } END; PROCEDURE ObjSym.Print; BEGIN CRT.GotoXY(Col,Line); { процедура из библиотеки CRT } Write(Sym) { вывод символа в позиции } END;

Рис. 13.3 (окончание)

Обратите внимание на то, что в задании нового объекта использовались поля данных и два метода GetLine и GetCol, идентичные полям и методам ранее описанного объекта ObjPos. Метод Init переписан заново, а поле Sym и, по сути, метод Print просто добавлены. Можно сказать, что более сложный объект, описывающий символ в тексте, унаследовал свойства и методы объекта-позиции. Методология ООП строится как раз на построении такого ряда объектов, в котором можно было бы проследить развитие и наследование свойств от простых структур к сложным.

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

TYPE

ИмяОбъектаНаследника = OBJECT(ИмяОбъектаПрародителя)

НовыеПоляОбъектаНаследника;

НовыеМетодыОбъектаНаследника;

END;

Пример на рис. 13.3 по правилам ООП и Турбо Паскаля должен выглядеть, как показано на рис. 13.4 (на нем не показаны, но используются определения, введенные ранее на рис. 13.1).

Тип «наследник» иногда называется производным типом, а тип, от которого производится наследование («прародитель») — прародительским типом. Таким образом, отличие объекта от записи состоит не только в объединении полей и методов «под одной крышей», но и в способности объектов к наследованию. Поля и методы прародителя могут появляться в телах методов наследника, как если бы они были объявлены явно. На рис. 13.4 это иллюстрируется методом Init, который для инициализации полей {276}

USES CRT; { в примере используется системный модуль CRT } {*3десь должны быть определения, введенные на рис.13.1*} TYPE ObjSym = OBJECT(ObjPos) { объявление наследования } Sym: Char; { поле-значение символа } PROCEDURE Init(init_line, init_col: Word; init_sym: Char); PROCEDURE Print { метод вывода символа } END; PROCEDURE ObjSym.Init; BEGIN ObjPos.Init(init_line, init_col); {задание позиции } Sym:= init_sym { задание значения символа } END; PROCEDURE ObjSym.Print; BEGIN CRT.GotoXY(Col, Line); { процедура из библиотеки CRT } Write(Sym) { вывод символа в позиции } END;

Рис. 13.4

Line и Col, «по наследству» перешедших к объекту ObjSym, пользуется наследуемой процедурой ObjPos.Init.

Продолжая ряд наследования, можно ввести объект, описывающий символ совместно с его цветовым атрибутом. Для этого достаточно описать объект, производный от ObjSym и добавляющий поле данных для атрибута и методы его назначения или опроса. Такой объект будет третьим уровнем наследования, если считать от базового объекта ObjPos. Но можно объявить и объект того же уровня наследования по отношению к ObjPos, что и ObjSym. Определим, к примеру, объект «подстрока» (он пригодится в последующих примерах). Для задания подстроки надо указать координаты ее начала номер строки и столбца) и само содержимое подстроки, которое содержит в себе указание на ее длину. Можно создать объект типа ObjString, описывающий подстроку, как производный от объекта-позиции (рис. 13.5).

Процесс наследования является транзитивным: если тип объекта TypeB наследуется от типа объекта TypeA, а тип TypeC наследуется от TypeB, то тип объекта TypeC также считается наследником типа TypeA. Таким образом, можно получить иерархию объектов, связан-{277}

TYPE ObjString = OBJECT(ObjPos) SubSt: String; { поле-значение подстроки } PROCEDURE Init(init_line, init_col:Word; init_ss:String); PROCEDURE Print { вывод SubSt в позиции } END; PROCEDURE ObjString.Init; { инициализация полей объекта } BEGIN ObjPos.Init(init_tine, init_col); {задание позиции } SubSt:= init_ss { задание значения подстроки } END; PROCEDURE ObjString.Print; BEGIN CRT.GotoXY(Col, Line); { процедура из библиотеки CRT } Write(SubSt) { печать подстроки в позиции } END;

Рис. 13.5

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

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

Несколько слов о «хорошем тоне» ООП. В случае, если в производном типе описывается новая процедура инициализации, в ней обычно вначале вызывается процедура инициализации непосредственного прародителя. Так удобно поступать еще и потому, что это самый естественный способ проинициализировать наследуемые поля предназначенным для этого методом. {278}

Присваивание объектов

Из «умения» объектов наследовать вытекают новые правила присваивания для переменных типа «объект» (экземпляров). Им можно присваивать не только значения этого же типа, но и любого производного от него. Аналогичное правило совместимости типов действует при передаче в процедуру или функцию параметров типа «объект». Например, если в программе объявлены переменные типа «объект»:

VAR

ObjPosVar: ObjPos; { переменная - позиция в тексте }

ObjSymVar: ObjSym; { переменная - символ в позиции }

то для копирования позиции, записанной в ObjSymVar, в переменную ObjPosVar достаточно выполнить присваивание

ObjPosVar:= ObjSymVar;

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

Прародитель <-- Наследник.

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

Переменные типа «объект» могут быть динамическими, т.е. объявляться как ссылки:

VAR

ObjPosVarPtr: ^ObjPos; { ссылка на позицию в тексте }

ObjSymVarPtr: ^ObjSym; { ссылка на символ в позиции }

После создания динамических объектов процедурой или функцией New ссылки (как разыменованные, так и сами по себе) могут присваиваться друг другу. Правила совместимости останутся теми же: корректными будут только присваивания ссылок на наследников ссылкам на прародителей: {279}

ObjPosVarPtr:= ObjSymVarPtr; { передача ссылки }

ObjPosVarPtr^:= ObjSymVarPtr^; { передача полей }

Полиморфизм

Подобно расширенным правилам присваивания, совместимость типов при передаче в процедуру или функцию параметров типа «объект» также понимается полнее. Формальному параметру типа «объект» может соответствовать фактический параметр не только этого же типа, но и типа, производного от него. Такое свойство совместимости объектов носит название полиморфизма. Практическим примером его использования могла бы стать процедура, выводящая либо строку, либо символ на экран. Несмотря на различие входных параметров (строка или символ), процедура будет одна — но она должна работать с объектами. В самом деле, ранее мы ввели объект-позицию ObjPos и производные от него типы ObjSym и ObjString. Если описать в процедуре формальный параметр как имеющий тип ObjPos, то по правилу совместимости в процедуру можно будет передавать фактические параметры других, производных от него типов, т.е. типов ObjSym и ObjString, и даже производных от них! Такая процедура приведена на рис. 13.6.

{ Считаются описанными типы ObjPos, ObjSym и ObjStr (см. рис. 13.1, 13.4 и 13.5) Процедура выводит полиморфный объект (строку или символ)} PROCEDURE PrintObj(VAR Obj: ObjPos); BEGIN Obj.Print END; VAR ObjSymVar: ObjSym; { экземпляр типа ObjSym } ObjStringVar: ObjString; { экземпляр типа ObjString } BEGIN { пример инициализации и вывода } ObjSymVar.Init(10, 10, '*'); ObjStringVar.Init(20, 20, '...ПОДСТРОКА...'); PrintObj(ObjStringVar); PrintObj(ObjSymVar); END.

Рис. 13.6 {280}

Обращаем внимание на легкость реализации процедуры. Она не зависит от процессов внутри объектов, а только вызывает нужный нам метод обработки полей данных (разнотипных). Поскольку мы предварительно сами позаботились о том, чтобы у всех наших объектов был одинаковый по написанию метод Print, его запись в процедуре встречается один раз. Но здесь необходимо сделать оговорку. Несмотря на внешнюю правильность примера, он будет работать не совсем корректно: всегда будет срабатывать метод Print для типа объекта ObjPos, независимо от типа фактического параметра. Это происходит потому, что во всех объявленных выше объектах используются статические методы.

Статические методы

Такое название методов связано с тем, что размещение соответствующих ссылок на них осуществляется еще на этапе компиляции.

Обработка компилятором статических методов похожа на обработку статических переменных. Действия компилятора при обработке методов объектов, составляющих некую иерархию, таковы:

1. При вызове метода компилятор устанавливает тип объекта, вызывающего метод.

2. Установив тип, компилятор ищет метод в пределах типа объекта. Найдя его, компилятор назначает вызов этого метода.

3. Если указанный метод не найден, то компилятор начинает рассматривать тип непосредственного прародителя и ищет метод, имя которого вызвано, в пределах этого прародительского типа. В случае, если метод с таким именем найден, вызов заменяется на вызов метода прародителя.

4. Если же искомый метод отсутствует в типе ближайшего прародителя, то компилятор переходит к типу следующего прародителя («дедушке») нашего типа, где и осуществляет дальнейший поиск. Процесс продолжается до тех пор, пока вызванный метод не будет найден, иначе компилятор, дойдя до самого последнего типа («родоначальника») и не найдя метод, имя которого вызывается, выдаст сообщение об ошибке компиляции номер 44 Field identifier expected («Ожидается идентификатор поля»). Это говорит о том, что метод с таким именем не определен.

Из всего этого следует одна важная особенность. Если метод прародителя вызывает другие методы, то последние также будут методами прародителя, даже если потомки имеют свои собственные методы. {281}

Применительно к примеру на рис. 13.6 сработают правила 1 и 2, и будет выполнен метод, объявленный в типе формального параметра (т.е. ObjPos.Print). Для того чтобы процедура заработала правильно, необходимо объявить метод Print виртуальным.

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

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

Метод становится виртуальным, когда за его определением в типе объекта ставится служебное слово VIRTUAL:

PROCEDURE ИмяМетода(параметры); VIRTUAL;

или

FUNCTION ИмяМетода(параметры): ТипЗначения; VIRTUAL;

При виртуализации методов должны выполняться следующие условия:

1. Если прародительский тип объекта описывает метод как виртуальный, то все его производные типы, которые реализуют метод с тем же именем, должны описать этот метод тоже как виртуальный. Другими словами, нельзя заменять виртуальный метод статическим. Если же это произойдет, компилятор сообщит об ошибке номер 149 VIRTUAL expected («Ожидается служебное слою VIRTUAL»).

2. Если переопределяется реализация виртуального метода, то заголовок заново определяемого виртуального метода в производном типе не может быть изменен. Иначе говоря, должны остаться неизменными порядок расположения, количество и типы формальных параметров в одноименных виртуальных методах. Если этот метод реализуется функцией, то не должен изменяться и тип результата. При изменении заголовка метода компилятор выдаст сообщение об ошибке номер 131 Header does not match previous definition («Заголовок не соответствует предыдущему определению»). {282}

3. В описании объекта должен обязательно описываться специальный метод, инициализирующий объект (обычно ему дают название Init). В этом методе служебное слово PROCEDURE в объявлении и реализации должно быть заменено на слово CONSTRUCTOR. Это служебное слово обозначает особый вид процедуры — конструктор, который выполняет установочную работу для механизма виртуальных методов. Все типы объектов, которые имеют хотя бы один виртуальный метод, должны иметь конструктор. Конструктор всегда вызывается до первого вызова виртуального метода. Вызов виртуального метода без предварительного вызова конструктора может привести систему к тупиковому состоянию, а компилятор не проверяет порядок вызовов методов. Помните об этом!

13.5.2.2. Конструкторы и таблица виртуальных методов. Каждый экземпляр (переменная) типа «объект», содержащий виртуальные методы, должен инициализироваться отдельным вызовом конструктора. Если переменная А инициализирована вызовом конструктора, а переменная В того же типа не инициализирована, то присваивание В:=А не инициализирует переменную В и при вызове ее виртуальных методов программа может «зависнуть».

Чтобы понять, что делает конструктор, разберемся в механизме реализации виртуальных методов. Каждый объектный тип (именно тип, а не экземпляр) имеет «таблицу виртуальных методов» (VMT), которая содержит размер типа объекта и адреса кодов процедур или функций, реализующих каждый из его виртуальных методов. При вызове виртуального метода каким-либо экземпляром местонахождение кода реализации этого метода определяется по таблице VMT для типа этого экземпляра. Конструктор же устанавливает связь между экземпляром, который вызывает конструктор, и таблицей виртуальных методов данного объектного типа. Если же конструктор не будет вызван до обращения к виртуальному методу, то перед компьютером станет вопрос, где же искать этот метод. Это и приведет к тупиковой ситуации.

Важно помнить, что таблица виртуальных методов — одна для каждого типа, а не у каждой переменной типа «объект». Переменная лишь держит связь с таблицей своего типа, и эта связь устанавливается конструктором. В объекте может быть определено несколько конструкторов. Сами конструкторы могут быть только статическими, хотя внутри конструктора могут вызываться и виртуальные методы.

При передаче в процедуру или функцию полиморфного объекта, имеющего виртуальные методы, адреса этих методов передаются {283} через соответствующую объекту таблицу VMT. Это гарантирует, что сработают именно те методы, которые подразумевались при объявлении типа объекта. Кроме того, если объект Z наследует от объекта Y виртуальный метод, вызывающий другие методы, то последние вызовы будут относиться к методам объекта Z, а не Y. В случае статических методов все было бы наоборот (вызовы не «вернулись» бы в Z).

Мы начали этот раздел с примера полиморфной процедуры (см. рис. 13.6). Чтобы она заработала, надо сделать некоторые методы виртуальными и объявить конструкторы в объектах. Это проделано на рис. 13.7.

USES CRT; { в примере используется системный модуль CRT } TYPE ObjPos = OBJECT Line: Word; { номер строки } Col: Word; { номер столбца } {! } CONSTRUCTOR Init(init_line,init_col: Word); {! } PROCEDURE Print; VIRTUAL { зарезервировано } END; CONSTRUCTOR ObjPos.Init(init_line, init_col: Word); BEGIN Line:= init_line; { метод задания номера строки } Col:= init_col; { метод задания номера столбца } END; PROCEDURE ObjPos.Print; { пустая процедура вывода } BEGIN Write(#7); { это вызовет звуковой сигнал } END; TYPE ObjSym = OBJECT(ObjPos) { объявление наследования } Sym: Char; { поле-значение символа } {! }CONSTRUCTOR Init(init_line,init_col: Word; init_sym: Char); {!} PROCEDURE Print; VIRTUAL {метод вывода Sym } END; CONSTRUCTOR ObjSym.Init; BEGIN ObjPos.Init(init_line, init_col); { задание позиции } Sym:= init_sym { задание значения символа } END;

Рис. 13.7 {284}

PROCEDURE ObjSym.Print; BEGIN CRT.GotoXY(Col, Line); { процедура из модуля CRT } Write(Sym) { вывод символа в позиции } END; TYPE ObjString=OBJECT(ObjPos) SubSt: String; { поле-значение подстроки } {! } CONSTRUCTOR Init(init_line,init_col: Word; init_ss: String); {! } PROCEDURE Print; VIRTUAL {метод вывода SubSt } END; CONSTRUCTOR ObjString.Init; {инициализация полей объекта } BEGIN ObjPos.Init(init_line, init_col); {задание позиции } SubSt:= init_ss { задание значения подстроки } END; PROCEDURE ObjString.Print; BEGIN CRT.GotoXY(Col, Line }; {процедура из библиотеки CRT } Write(SubSt) {печать подстроки в позиции } END; {Вывод полиморфного объекта (строки или символа) } PROCEDURE PrintObj(VAR Obj: ObjPos); BEGIN Obj.Print END; { =========== ТЕЛО ОСНОВНОЙ ПРОГРАММ ================ } VAR ObjSymVar: ObjSym; { экземпляр типа ObjSym } ObjStringVar: ObjString; { экземпляр типа ObjString } BEGIN { Инициализация и вывод: } ClrScr; { очистка экрана } ObjSymVar.Init(10, 10, '*'); ObjStringVar.Init(20, 20, '...ПОДСТРОКА...'); PrintObj(ObjStringVar); { вывод строки } PrintObj(ObjSymVar); { вывод символа } END.

Рис. 13.7 (окончание) {285}

Весьма важным является наличие слова VAR перед формальным параметром в процедуре PrintObj. В этом случае мы передаем сам объект. Если бы в процедуре PrintObj формальный параметр был описан как параметр-значение (без слова VAR), то процедура работала бы с копией объекта, приведенной к типу формального параметра. В примере на рис. 13.7 это выразилось бы в том, что несмотря на виртуальность методов, вызывался бы метод ObjPos.Print из типа формального параметра.

Выбор вида метода

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

Другим критерием выбора может быть скорость выполнения программы. Если объект имеет хотя бы один виртуальный метод, то для него создается таблица виртуальных методов, и каждая переменная этого типа будет иметь связь с этой таблицей. Каждый вызов виртуального метода проходит через обращение к таблице VMT. С другой стороны, статические методы вызываются «напрямую», поэтому вызов статического метода происходит быстрее, чем виртуального. А если объект вообще не содержит виртуальных методов, то таблица виртуальных методов не будет создана и, как следствие этого, каждая переменная такого типа не будет поддерживать связь с таблицей VMT.

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

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



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2022-11-01 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: