Меню
Главная
Авторизация/Регистрация
 
Главная arrow Информатика arrow Алгоритмы и структуры данных

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В DELPHI

ОСНОВНЫЕ КОНЦЕПЦИИ ООП

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

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

Объекты в программе всегда являются экземплярами того или иного класса (подобно переменным определенного типа).

К основным концепциям ООП относятся следующие: инкапсуляция, наследование, полиморфизм.

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

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

В корне этого дерева находится базовый класс ЮЬдесЧ который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта. Чем дальше тот или иной класс отстоит в дереве от базового класса, тем большей специфичностью он обладает.

Пример объявления класса-потомка:

tAnyClass = class(tParentClass)

//Добавление к классу tParentClass новых

// и переопределение существующих элементов

end;

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

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

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

КЛАССЫ И ОБЪЕКТЫ

Классом в Delphi называется особая структура, состоящая из полей, методов и свойств.

type

tMyClass = class (TObject) //класс tMyClass

fMyField: Integer; //поле fMyField

function MyMethod(Data:tData): Integer; //метод MyMethod

end;

Все классы в Delphi являются наследниками базового класс TObject, который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта.

Поля класса — это данные, уникальные для каждого созданного в программе экземпляра класса. Они могут иметь любой тип, в том числе — тип класс. В Delphi перед именами полей принято ставить символ f (от field — поле): fLength, fWidth, fMyFileld и т.п.

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

Свойство можно определить как поле, доступное для чтения и записи не напрямую, а через соответствующие методы.

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

Методы класса реализуются в разделе описания процедур и функций программы или в разделе implementation модуля. При реализации метода указывается его полное имя, состоящее из имени класса, точки и имени метода класса:

function tMyClass.MyMethod(Data:tData): Integer;

//заголовок метода tMyClass.МуMethod

begin

// тело метода tMyClass.MyMethod

end;

Разрешено опережающее объявление классов единственным словом class с последующим полным описанием:

type

tFirstClass = class;

tSecondClass = class f1 : tFirstClass;

end;

tFirstClass = class f2 : tSecondClass;

end;

ОБЛАСТИ ВИДИМОСТИ

Области видимости — это возможности доступа к составным частям объекта. В Delphi поля и методы могут относиться к четырем группам: «общие» (public), «личные» (private), «защищенные» (protected) и «опубликованные» (published).

  • 1. Поля, свойства и методы, находящиеся в секции public, не имеют ограничений на видимость. Они доступны из других функций и методов объектов, как в данном модуле, так и во всех прочих, ссылающихся на него.
  • 2. Поля, свойства и методы, находящиеся в секции private, доступны только в методах класса и в функциях, содержащихся в том же модуле, что и описываемый класс. Такая директива позволяет скрыть детали внутренней реализации класса от всех. Элементы из секции private можно изменять, и это не будет сказываться на программах, работающих с объектами этого класса. Обратиться к ним можно, только переписав содержащий их модуль.
  • 3. Поля, свойства и методы, находящиеся в секции protected, доступны только внутри классов, являющихся потомками данного, в том числе и в других модулях. Такие элементы особенно необходимы дня разработчиков новых компонентов — потомков уже существующих.
  • 4. Область видимости published имеет особое значение для интерфейса визуального проектирования. В этой секции должны быть собраны те свойства объекта, которые будут видны не только во время исполнения приложения, но и из среды разработки. Все свойства компонентов, доступные через Инспектор объектов, являются их опубликованными свойствами. Во время выполнения такие свойства общедоступны, как и public.

Пример, иллюстрирующий первые три варианта областей видимости:

unit First; interface type

tClassI = class public

procedure Methodl; private

procedure Method2; protected

procedure Method3; end;

procedure TestProcI;

implementation

var

Obj1 : tClassI;

procedure TestProcI; begin

Obj1:= tClassI .Create;

Оbj1.Methodl; //допустимо Obj1.Method2; //допустимо Obj1.Method3; //недопустимо Objl.Free; end;

end. // unit First

unit Second; interface uses First;

type

tClass2 = class(tClassl) procedure Method4; end;

procedure TestProc2;

implementation

var Obj2 : tClass2;

procedure tClass2.Method4;

begin

Methodl; //допустимо Method2; //недопустимо Method3; //допустимо

end;

procedure TestProc2;

begin

Obj2 : =tClass2.Create;

Obj2 .Methodl; //допустимо Obj2 .Method2; //недопустимо Obj2 .Method3; //недопустимо Obj2 .Free;

end;

end. //unit Second

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

СОЗДАНИЕ И УНИЧТОЖЕНИЕ ОБЪЕКТОВ

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

Для выделения памяти экземпляру любой динамической переменной используется процедура New, для уничтожения — процедура Dispose. С объектами эти процедуры не используются: объект создается специальным методом — конструктором, а уничтожается специальным методом — деструктором:

AMyObject := tMyClass.Create; //создание объекта класса tMyClass

// действия с созданным объектом

AMyObject.Destroy; //уничтожение объекта AMyObject

Конструктор — это специальный метод, заголовок которого начинается зарезервированным словом constructor. Функция конструктора заключается в выделении памяти под экземпляр класса (объект) и в установлении связи между созданным объектом и специальной информацией о классе. В теле конструктора можно расположить любые операторы, которые необходимо выполнить при создании объекта, например, присвоить полям начальные значения. В Delphi конструкторов у класса может быть несколько. Общепринято называть конструктор Create.

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

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

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

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

constructor tMyClass. Create;

begin

inherited Create:

// собственный код конструктора

end;

Деструктор — это специальный виртуальный (см. ниже) метод, заголовок которого начинается зарезервированным словом destructor. Деструктор предназначен для удаления объектов. Типичное название деструктора — Destroy.

Для уничтожения объекта рекомендуется использовать метод Free (он есть у базового класса TObject), который первоначально проверяет указатель на объект (не равен ли он nil) и только затем вызывает деструктор Destroy:

AMyObject.Free;

Деструктор очень часто не переопределяется в классе-потомке. Но если его необходимо переопределить, то необходимо учитывать следующее. Деструктор Destroy любого класса перекрывает виртуальный деструктор базового класса TObject, поэтому деструктор потомка необходимо объявить с директивой override:

destructor Destroy: override;

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

destructor tMyClass.Destroy;

begin

// собственный код деструктора inherited Destroy; end;

ИНКАПСУЛЯЦИЯ. СВОЙСТВА

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

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

type

tMyClass = class

function GetAProperty: tSomeType;

procedure SetAProperty(ANewValue: tSomeType);

property AProperty: tSomeType read GetAProperty write SetAProperty;

end;

В контексте свойства слова read и write являются зарезервированными. Для доступа к значению свойства AProperty явно достаточно написать

AMyObject.AProperty := AValue;

AVariable := AMyObject.AProperty;

и компилятор оттранслирует эти операторы в вызовы методов.

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

Рассмотрим следующий пример:

tPropCIass = class(tObject) fValue: Integer; procedure DoSomething; function Correct(AValue: lnteger):Boolean;

procedure SetValue(NewValue: Integer); property AValue: Integer read fValue write SetValue;

end;

procedure tPropClass.SetValue(NewValue: Integer);

begin

if (NewValueofValue) and Correct (NewValue) then fValue := NewValue;

DoSomething;

end;

В этом примере чтение значения свойства AValue означает просто чтение поля fValue. Зато при присвоении ему значения внутри SetValue вызывается сразу два метода.

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

tMyClass = class

property AProperty: tSomeType read GetValue;

end;

Значение свойства AProperty можно лишь прочитать; попытки присвоить AProperty значение вызовет ошибку компиляции.

Для присвоения свойству значения по умолчанию используется ключевое слово default:

property Visible: Boolean read fVisible write SetVisible default True

Свойство может быть и векторным; в этом случае оно внешне выглядит как массив:

property APoints[lndex: IntegerftPoint read GetPoint write SetPoint;

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

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

function GetPoint(lndex: Integer): TPoint;

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

procedure SetPoint(lndex: Integer; NewPoint: tPoint);

У векторных свойств есть еще одна важная особенность. Некоторые классы в Delphi (списки, наборы строк) «построены» вокруг векторного свойства. Основной метод такого класса дает доступ к некоторому массиву, а все остальные методы являются вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано как default:

tMyClass = class

property Strings[lndex: Integer]: string read Get write Put; default;

end;

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

var AMyObject: tMyClass;

begin

AMyObject.Strings[1] := 'First'; //первый способ AMyObject[2] := 'Second'; //второй способ • • •

end;

Будьте внимательны, применяя зарезервированное слово default, — для обычных и векторных свойств оно употребляется в разных случаях и с различным синтаксисом.

О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в распоряжении стандартных классов 100% полей недоступны (помещены в секцию private) и заменены базирующимися на них свойствами. Рекомендуем при разработке своих классов придерживаться этого же правила.

НАСЛЕДОВАНИЕ. МЕТОДЫ

Принцип наследования позволяет объявить класс

tNewObject = class(tOldObject);

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

В Delphi все классы являются потомками класса TObject. При построении дочернего класс прямо от TObject в определении его можно не упоминать.

Следующие объявления одинаково верны:

tMyObject = class(TObject); tMyObject = class;

Первый вариант предпочтительнее, хотя он и более длинный, — для устранения возможных неоднозначностей.

Класс TObject несет очень серьезную нагрузку и будет рассмотрен отдельно.

Унаследованные от предка поля и методы доступны в дочернем классе; если имеет место совпадение имен методов, то говорят, что они перекрываются.

По тому, какие действия происходят при вызове, методы делятся на три группы:

  • 1) статические;
  • 2) виртуальные (virtual) и динамические (dynamic);
  • 3) перегружаемые (overload) методы.

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

Принципиально отличаются от статических виртуальные и динамические методы (директива virtual или dynamic). Их адрес определяется во время выполнения программы по специальной таблице. С точки зрения наследования методы этих двух видов одинаковы: они могут быть перекрыты в дочернем классе только одноименными методами, имеющими тот же тип.

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

ПОЛИМОРФИЗМ.

ВИРТУАЛЬНЫЕ И ДИНАМИЧЕСКИЕ МЕТОДЫ

Рассмотрим пример. Пусть у нас имеются некое обобщенное поле для хранения данных — абстрактный класс tField и три его потомка — для хранения строк, целых и вещественных чисел:

tField = class

function GetData: string; virtual; abstract;

end;

tStringField = class(tField) fData : string;

function GetData: string; override;

end;

tlntegerField = class(tField)

fData : Integer;

function GetData: string; override;

end;

tExtendedField = class(tField)

fData : Extended;

function GetData: string; override;

end;

function tStringField.GetData;

begin

Result := fData;

end;

function tlntegerField.GetData;

begin

Result := IntToStr(fData);

end;

function tExtendedField.GetData;

begin

Result:=FloatToStr(fData);

end;

procedure ShowData(AField:tField);

begin

Writeln(AField.GetData);

end;

В этом примере классы содержат разнотипные данные и «умеют» сообщать о значении этих данных текстовой строкой (при помощи метода GetData). Внешняя по отношению к ним процедура Show Data получает объект в виде параметра и показывает эту строку.

Правила контроля соответствия типов (typecasting) языка Delphi гласят, что объекту как указателю на экземпляр объектного типа может быть присвоен адрес любого экземпляра любого из дочерних типов. В процедуре ShowData параметр описан как tField — это значит, что в нее можно передавать объекты классов и tStringField, и tlntegerField, и tExtendedField, и любого другого потомка tField.

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

При вызове виртуальных и динамических методов адрес определяется не во время компиляции, а во время выполнения — это называется поздним связыванием (late binding). Позднее связывание реализуется с помощью таблицы виртуальных методов (Virtual Method Table, VMT) и таблицы динамических методов (Dynamic Method Table, DMT).

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

Динамические методы вызываются медленнее, но позволяют более экономно расходовать память. Каждому динамическому методу системой присваивается уникальный индекс. В таблице динамических методов класса хранятся индексы и адреса только тех динамических методов, которые описаны в данном классе. При вызове динамического метода происходит поиск в этой таблице; в случае неудачи просматриваются таблицы DMT всех классов-предков в порядке иерархии и, наконец, DMT класса TObject, где имеется стандартный обработчик вызова динамических методов. Экономия памяти налицо.

Для перекрытия и виртуальных, и динамических методов служит директива override, с помощью которой (и только с ней!) можно переопределять оба этих типа методов.

Приведем пример.

tClassI = class fField 1: Integer; fField2: Longint; procedure stMet; procedure vrMetl; virtual; procedure vrMet2; virtual; procedure dnMetl; dynamic; procedure dnMet2; dynamic; end;

tClass2 = class(tClassl) procedure stMet; procedure vrMetl; override; procedure dnMetl; override; end;

var

Obj1: tClassI;

Obj2: tClass2;

Первый метод класса tClass2 создается заново, остальные два — перекрываются. Попытка применить директиву override к статическому методу вызовет ошибку компиляции.

ПРИМЕР РЕАЛИЗАЦИИ ПОЛИМОРФИЗМА ДЛЯ ИЕРАРХИИ ГРАФИЧЕСКИХ ОБЪЕКТОВ

Рассмотрим описание и реализацию классов «Точка» и «Окружность» для «рисования» на экране точки и окружности.

type

tCoord = Word;

// тип - координата точки на экране

// класс - точка на экране дисплея

// поля - координаты точки на экране // поле - цвет рисования точки

// Описание класса tPoint tPoint = class(TObject) protected fX, fY: tCoord; fColor: Byte; public

property X: tCoord read fX write fX; // свойство - координата X

property Y: tCoord read fY write fY; // свойство - координата Y

property Color: Byte read fColor write fColor;// свойство - цвет procedure Show; //метод высвечивания точки

procedure Hide; //метод гашения точки

procedure MoveTo(NewX, NewY: tCoord); //перемещение точки end;

// класс - окружность на экране // поле - радиус окружности

// Описание класса tCircle tCircle = class(tPoint)

protected

fRadius: tCoord;

public

property Radius: tCoord read fRadius write fRadius; //св-во - радиус procedure Show; //метод высвечивания окружности

procedure Hide; //метод гашения окружности

procedure MoveTo (NewX,NewY:tCoord) //перемещение окружности

end;

// Реализация методов класса tPoint

procedure tPoint.Show;

begin

// «Рисование» точки

Writeln('Pi/icyio точку (', fx,fy,') цветом fColor);

end;

procedure tPoint.Hide;

begin

// «Стирание» точки Л/гйе1п('Стираю точку (', fx, ',fy,') цвета fColor);

end;

procedure tPoint.MoveTo(NewX, NewY: tCoord);

begin

Hide;

fX := NewX; fY := NewY;

Show;

end;

// Реализация методов класса tCircle

procedure tCircle.Show;

begin

//«Рисование» окружности

Writeln('PHcyK) окружность с центром (', fx,fy,') радиуса fRadius,' цветом fColor);

end;

procedure tCircle.Hide; begin

//«Стирание» окружности

Л/гйе1п('Стираю окружность с центром (', fx,',', fy,') радиуса ', fRadius,' цвета ', fColor);

end;

procedure tCircle.MoveTo (NewX,NewY: tCoord);

begin

Hide;

fX := NewX; fY := NewY;

Show;

end;

Обратите внимание, что методы MoveTo классов tPoint и tCircle реализуются одинаково за исключением того, что в tPoint. MoveTo используются tPoint.Hide и tPoint.Show, а в tCircle.MoveTo используются tCircle.Hide и tCircle.Show. Взаимодействие методов классов tPoint и tCircle можно проиллюстрировать схемой, приведенной на рис. П3.1.

tPoint

П3.1. Взаимодействие методов классов tPoint и tCircle

Рис. П3.1. Взаимодействие методов классов tPoint и tCircle

Поскольку методы реализуются одинаково, можно попытаться унаследовать метод МоуеТо от класса 1РотЕ При этом возникает следующая ситуация.

При компиляции метода 1РотСМоуеТо в него будут включены ссылки на коды методов 1РотС81кж и 1РотЕНк1е. Так как метод с именем МоуеТо не определен в классе 1Слгс1е, то компилятор обращается к родительскому типу и ищет в нем метод с этим именем. Если метод найден, то адрес родительского метода заменяет имя в исходном коде потомка.

Следовательно, при вызове 1СДс1е. МоуеТо в программу будут включены коды 1РотС МоуеТо (то есть класс 1:Сцс1е будет использовать метод так, как он откомпилирован). В этом случае процедура 1Сцс1е. МоуеТо будет работать неверно.

Структура вызовов методов при таком наследовании приведена на рис. П3.2.

tPoint

П3.2. Структура вызовов методов при наследовании метода

Рис. П3.2. Структура вызовов методов при наследовании метода

MoveTo от класса tPoint

Для правильного вызова методов процедуры Show и Hide должны быть объявлены виртуальными, так как только в этом случае класс tCircle может наследовать метод MoveTo у типа tPoint. Это становится возможным потому, что подключение виртуальных методов Show и Hide к процедуре MoveTo будет осуществляться во время выполнения программы, и будут подключаться методы, определенные для типа tCircle (tCircle.Show и tCircle.Hide).

При использовании виртуальных методов Show и Hide взаимодействие методов классов tPoint и tCircle можно проиллюстрировать схемой, приведенной на рис. ПЗ.З.

tPoint

virtual

virtual

override

override

Рис. ПЗ.З. Структура вызовов методов при использовании виртуальных методов Show и Hide

Подключение виртуальных методов осуществляется с помощью специальной таблицы виртуальных методов (VMT).

Описание классов «Точка» и «Окружность» с использованием виртуальных методов:

// Описание класса tPoint tPoint = class(TObject) protected fX, fY: tCoord; fColor: Byte; public

property X: tCoord read fX write fX; property Y: tCoord read fY write fY; property Color: Byte read fColor write fColor; procedure Show; virtual; procedure Hide; virtual; procedure MoveTo(NewX, NewY: tCoord); end;

// Описание класса tCircle tCircle = class (tPoint) protected fRadius: tCoord;

public

property Radius: tCoord read fRadius write fRadius; procedure Show; override; procedure Hide; override; end;

ПЕРЕГРУЗКА МЕТОДОВ

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

Рассмотрим пример:

type

tClassI = class i: Extended;

procedure SetData(AValue : Extended);

end;

tClass2 = class(tClassl) j: Integer;

procedure SetData(AValue : Integer);

end;

var

Obj1: tClassI;

Obj2: tClass2;

Попытка вызова методов

Obj2.SetData(1.0);

Obj2.SetData(1);

вызовет ошибку компиляции на первой из двух строк. Для компилятора внутри Obj2 статический метод с параметром типа Extended перекрыт, и он его не «признает».

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

type

tClassI = class i: Extended:

procedure SetData(AValue : Extended); overload; end;

tClass2 = class(tClassl) j: Integer;

procedure SetData(AValue : Integer); overload; end;

Объявив метод Set Data перегружаемым, в программе можно использовать обе его реализации одновременно. Это возможно потому, что компилятор определяет тип передаваемого параметра (целый или с плавающей точкой) и в зависимости от этого подставляет вызов соответствующего метода.

Можно перегружать и виртуальные методы. В этом случае необходимо добавить директиву reintroduce:

type

tClassI = class i : Extended;

procedure SetData(AValue : Extended); virtual; overload; end;

tClass2 = class(tClassl) j: Integer;

procedure SetData(AValue : Integer); reintroduce; overload; end;

На перегрузку методов накладывается ограничение — нельзя перегружать методы, находящиеся в области видимости published.

АБСТРАКТНЫЕ МЕТОДЫ

Абстрактными называются методы, которые определены в классе, но не содержат никаких действий, никогда не вызываются и обяза-

тельно должны быть переопределены в потомках класса. Абстрактными могут быть только виртуальные и динамические методы. В Delphi есть одноименная директива (abstract), указываемая при описании метода:

procedure NeverCallMe; virtual; abstract;

Никакого кода для этого метода писать не нужно. Вызов абстрактного метода приведет к созданию исключительной ситуации Е Abstract Error.

Пример с классом tField из параграфа «Полиморфизм» поясняет, для чего нужны абстрактные методы. В данном случае класс tField не используется сам по себе; его основное предназначение — быть родоначальником иерархии конкретных классов-«полей» и дать возможность абстрагироваться от частностей. Хотя параметр процедуры ShowData и описан как tField, но если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.

ОБЪЕКТ ИЗНУТРИ

Рассмотрим уже знакомый пример из параграфа «Полиморфизм»:

tClassI = class fFieldl: Integer; fField2: Longint; procedure stMet; procedure vrMetl; virtual; procedure vrMet2; virtual; procedure dnMetl; dynamic; procedure dnMet2; dynamic;

end;

tClass2 = class(tClassl) procedure stMet; procedure vrMetl; override; procedure dnMetl; override;

end;

var

Obj1: tClassI;

Obj2: tClass2;

Внутренняя структура объектов Objl и Obj2 приведена на рис. П3.4.

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

Перед таблицей виртуальных методов расположена специальная структура, которая называется информацией о типе времени выполнения (runtime type information, RTTI). В ней содержатся данные, полностью характеризующие класс: его имя, размер экземпляра, указатели на класс-предок, на имя класса и т. д. На рисунке она показана одним блоком, а ее содержимое расшифровано ниже.

Указатель на класс tClassl

Поле fFieldl

Поле МеШ

Указатель на класс

Поле fFieldl Поле fField2

VMT класса tClassl

DMT класса tClassl

RTTI класса tClassl

Число динамических методов: 2

@tClassl .vrMetl

Индекс tClassl .dnMetl (-1)

(2>tClassl ,vrMet2

Индекс tClassl ,dnMet2 (-2)

(2>TObject. Destroy

(й) tClassl .dnMetl

@tClassl ,dnMet2

VMT класса tClass2

DMT класса tClass2

RTTI класса tClass2

Число динамических методов: 1

@tClass2. vrMetl

Индекс tClass2.dnMetl (-1)

(2; tClassl ,vrMet2

(2>tClass2. dnMetl

(aiTObiect. Destroy

Рис. П3.4. Внутренняя структура объектов Obj 1 и Obj2

Одно из полей структуры содержит адрес таблицы динамических методов класса (DMT). Таблица имеет следующий формат — в начале слово, содержащее количество элементов таблицы; затем — слова, соответствующие индексам методов. Нумерация индексов начинается со значения — 1 и идет по убывающей. После индексов идут собственно адреса динамических методов.

Обратите внимание, что DMT объекта Obj 1 состоит из двух элементов, Obj2 — из одного элемента, соответствующего перекрытому методу dnMetl. В случае вызова метода Obj2.dnMet2 индекс не будет найден в таблице DMT Obj2, поиск продолжится в DMT объекта Obj 1. Именно так экономится память при использовании динамических методов.

Напомним, что указатель на класс указывает на адрес первого виртуального метода. Служебные данные размещаются перед таблицей VMT, то есть с отрицательным смещением. Эти смещения описаны в модуле SYSTEM.PAS:

  • -76;
  • -72;
  • -68;
  • -64;
  • -60;
  • -56;

= -52; = -48; = -44; = -40; -36;

= -32; = -28; = -24; -20;

= -16; = -12; = -8;

= -4;

vmtSelfPtr =

vmtlntfTable =

vmtAutoTable

vmtlnitTable =

vmtTypelnfo =

vmtFieldTable =

vmtMethodTable

vmtDynamicTable

vmtClassName

vmtlnstanceSize

vmtParent =

vmtSafeCallException

vmtAfterConstruction

vmtBeforDestruction

vmtDispatch =

vmtDefaultHandler

vmtNewInstance

vmtFreelnstance

vmtDestroy

Поля УггйОупагтсТаЫе, Уггй018ра1с11 и У1УйОеГаиИ:Напб1ег отвечают за вызов динамических методов. Поля угтп№у1п81апсе, угШЕгее1п81апсе и УгШЭез^оу содержат адреса процедур создания и уничтожения экземпляра класса. Поля утПшЛаЫе, УпйА^оТаЫе, уш15аГеСа11Ехсер1юп введены для обеспечения совместимости с моделью объектов СОМ. Остальные поля доступны через методы класса ТСИуесЕ в Ое1р1м информация этой таблицы играет важную роль и может использоваться программистом неявно.

ПРОВЕРКА СОВМЕСТИМОСТИ И ПРИВЕДЕНИЕ ОБЪЕКТНЫХ ТИПОВ

В языке определены два оператора: к и аз, неявно обращающиеся к информации о классе.

Оператор к предназначен для проверки совместимости по присваиванию экземпляра объекта с заданным классом. Выражение вида:

AnObject is TObjectType

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

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

with ASomeObject as tAnotherType do ...

Использование оператора as отличается от стандартного способа приведения типов с помощью конструкции

tAnotherType(ASomeObject)

наличием проверки на совместимость типов во время выполнения (как в операторе is): попытка приведения к несовместимому типу приводит к возникновению исключительной ситуации EInvalidCast.

После применения оператора as сам объект остается неизменным, но вызываются только те его методы, которые есть у присваиваемого класса. Присваиваемый фактически тип должен быть известен на стадии компиляции, поэтому на месте TObjectType (после is) и tAnotherType (после as) не может стоять переменная-указатель на класс.

УКАЗАТЕЛЬ НА КЛАСС. МЕТОДЫ КЛАССА

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

Доступ к информации класса вне методов этого класса можно получить, описав соответствующий указатель, который называется указателем на класс, или указателем на объектный тип (class reference). Он описывается при помощи зарезервированных слов class of. Например, указатель на класс TObject описан в модуле SYSTEM.PAS и называется tClass:

type

TObject = class; tClass = class of TObject;

Указатели на классы подчиняются правилам приведения объектных типов. Указатель на класс-предок может ссылаться и на любые дочерние классы; обратное невозможно.

С указателем на класс тесно связано понятие методов класса. Такие методы можно вызывать без создания экземпляра объекта — с указанием имени класса, в котором они описаны. Перед описанием метода класса нужно поставить зарезервированное слово class:

type

tMyClass = class (TObject) class function GetSize: string; end;

var

MyObject: tMyClass;

AString: string;

begin

// обращение к методу класса до создания объекта этого класса

AString := tMyClass.GetSize;

MyObject := tMyObject.Create;

AString := MyObject.GetSize;

end.

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

Некоторые важные методы класса TObject приведены в табл. П3.1.

Таблица ПЗ. I

Основные методы класса TObject

Метод

Описание

class function Classlnfo: Pointer;

Возвращает указатель на структуру с дополнительными данными об опубликованных методах и свойствах

class function ClassName: ShortString;

Возвращает имя класса

class function ClassNamels (const Name: ShortString): Boolean;

Возвращает True, если имя класса равно заданному

class function Class Parent: TClass;

Возвращает указатель на родительский класс (для TObject возвращает nil)

class function InheritsFrom

(AClass: TClass): Boolean;

Возвращает True, если данный класс порожден от класса AClass

class function Initlnstance (Instance: Pointer): TObject;

Инициализирует экземпляр класса

class function InstanceSize: Longint;

Возвращает размер экземпляра класса

class function

MethodAddress(const Name: ShortString): Pointer;

Возвращает адрес метода по его имени (только для опубликованных методов)

class function MethodName (Address: Pointer): ShortString;

Возвращает имя метода по его адресу (только для опубликованных методов)

class function Newlnstance: TObject; virtual;

Создает новый экземпляр объекта.

В частности, эта функция вызывается внутри конструктора

Метод

Описание

function ClassType: TClass;

Возвращает указатель на класс данного объекта

constructor Create;

Конструктор. Создает новый экземпляр класса и инициирует обработчик исключительных ситуаций

procedure AfterConstruction;

virtual;

Вызывается сразу после создания экземпляра класса

procedure BeforeDestruction;

virtual;

Вызывается перед уничтожением экземпляра

destructor Destroy; virtual;

Деструктор. Производит действия по уничтожению экземпляра класса

procedure Default Handler (var Message); virtual;

Обработчик сообщений по умолчанию.

В TObject не содержит ничего, кроме кода возврата

procedure Dispatch(var Message);

Вызывает методы обработки сообщений в зависимости от значения параметра Message

function FieldAddress(const

Name: String): Pointer;

Возвращает адрес поля вызвавшего объекта с заданным именем

procedure Free;

Используется вместо деструктора. Проверяет передаваемый деструктору указатель на экземпляр

procedure Free Instance; virtual;

Уничтожает экземпляр объекта. Вызывается внутри деструктора

В следующем примере переменная Object Ref является указателем на класс; он по очереди указывает на TObject и TMyObject. Посредством этой переменной-указателя вызывается функция класса ClassName:

type

TMyObject = class;

TMyObjClass = class of TObject;

var

ObjectRef: TMyObjClass;

s: String; begin

ObjectRef := TObject;

s := ObjectRef .ClassName; //s :=T0bject'

ObjectRef := TMyObject;

s:= ObjectRef .ClassName; //s : ='TMyObject'

end.

Виртуальные методы AfterConstruction и Before Destruction вызываются сразу после создания экземпляра класса и непосредственно перед его уничтожением. Их можно использовать, если по каким-либо причинам не хватает собственно конструктора и деструктора.

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

Методы MethodName и MethodAddress используются для получения имени или адреса только опубликованных методов, это связано с тем, что система Delphi хранит имена только таких методов. Так как директива published предназначена для описания свойств, размещаемых в инспекторе объектов, где указываются их имена.

Если метод MethodName вызывается для указателя на неопубликованный метод, то возвращается пустая строка.

При передаче методу MethodAddress в качестве параметра имени неопубликованного или несуществующего метода возвращается значение nil.

Метод FieldAddress используется для получения адреса в памяти опубликованного поля объекта. Если в качестве параметра передается имя неопубликованного или несуществующего поля, то возвращается значение nil.

 
Если Вы заметили ошибку в тексте выделите слово и нажмите Shift + Enter
< Пред   СОДЕРЖАНИЕ   След >
 

Популярные страницы