Тема 4: Инкапсуляция. Наследование. Полиморфизм.

 

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

2. Полиморфизм. Виртуальные и динамические методы

3. Методы и описание

4. Перегрузка методов

5. Абстрактные методы

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

 

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

 

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

 

tNewClass=class(tOldClass);

 

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

 

В Delphi все классы являются потомками класса tObject. Поэтому, если вы строите дочерний класс прямо от tObject, то в определении его можно не упоминать. Следующие два описания одинаково верны:

 

tMyClass=class(tObject);

tMyClass=class;

 

Более подробно класс tObject будет рассмотрен ниже.

 

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

 

Рассмотрим поведение методов при наследовании. По тому, какие действия происходят при вызове, методы делятся на три группы. В первую группу отнесем статические методы, во вторую - виртуальные (virtual) и динамические (dynamic) и, наконец, в третью - появившиеся только в Delphi 4 перегружаемые (overload) методы.

 

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

 

type

tFirstClass=class

fData:Extended;

procedure SetData(aValue:Extended);

end;

 

tSecondClass=class(tFirstClass)

fData:Integer;

procedure SetData(aValue:Integer);

end;

 

procedure tFirstClass.SetData(aValue:Extended);

Begin

fData:=1.0;

End;

 

procedure tFirstClass.SetData(aValue:Extended);

Begin

fData:=1;

inherited SetData(0.99);

End;

 

В этом примере разные методы с именем SetData присваивают значение разным полям с именем fData. Перекрытое (одноименное) поле предка недоступно в потомке. Поэтому два одноименных поля с именем fData приведены только для примера.

 

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

 

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

 

2.Полиморфизм. Виртуальные и динамические методы

 

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

 

type

tFiled = class

function GetData:string; virtual; abctract;

end;

 

tStringFiled = class(tFiled)

fData:string;

function GetData: string; override;

end;

 

tIntegerFiled = class(tFiled)

fData:Integer;

function GetData: string; override;

end;

 

tExtendedFiled = class(tFiled)

fData:Extended;

function GetData: string; override;

end;

 

function tStringFiled.GetData: string;

Begin

Result:=fData;

End;

 

function tIntegerFiled.GetData: string;

Begin

Result:=IntToStr(fData);

End;

 

function tExtendedFiled.GetData: string;

Begin

Result:=FloatToStr(fData,ffFixed, 7, 2);

End;

 

function ShowData(aFiled:tFiled): string;

Begin

Form1.Label1.Caption:=aFiled.GetData;

End;

 

В этом примере классы содержат разнотипные поля данных fData, а также имеют унаследованный от tFiled виртуальный метод GetData, возвращающий данные в виде строки. Внешняя по отношению к ним процедура ShowData получает объект в виде параметра и показывает эту строку.

 

Согласно правилам контроля соответствия типов (typecasting) ObjectPascal, объекту, как указателю на экземпляр класса, может быть присвоен адрес экземпляра любого из дочерних типов. Это означает, что в предыдущем примере в процедуру ShowData можно передавать объекты классов tStringFiled, tIntegerFiled, tExtendedFiled и любого другого потомка tFiled.

 

Но какой (точнее, чей) метод GetData будет при этом вызван? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом.

 

Возвращаясь к рассмотренному выше примеру, отметим, что у компилятора нет возможности определить класс объекта, фактически переданного в процедуру ShowData на этапе компиляции. Механизм, позволяющий определить этот класс прямо во время выполнения называется поздним связыванием. Естественно, такой механизм должен быть связан с передаваемым объектом. Для этого служит таблица виртуальных методов (Virtual Method Table, VMT) и таблица динамических методов (Dynamic Method Table, DMT).

 

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

 

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

 

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

 

type

tParentClass=class

fFirstFiled:Integer;

fSecondFiled:longInt;

procedure StaticMethod;

procedure VirtualMethod1; virtual;

procedure VirtualMethod2; virtual;

procedure DynamicMethod1; dynamic;

procedure DynamicMethod2; dynamic;

end;

 

tChildClass=class(tParentClass)

procedure StaticMethod;

procedure VirtualMethod1; override;

procedure DynamicMethod1; override;

end;

 

Первый метод класса tChildClass создается заново, два остальных перекрываются. Создадим объекты этих классов:

 

var Obj1: tParentClass;

Obj2: tChildClass;

 

Внутренняя структура этих объектов показана ниже. 

 

Первое поле каждого экземпляра каждого объекта содержит указатель на его класс. Класс, как структура состоит из двух частей. Начиная с адреса, на который ссылается указатель на класс, располагается таблица виртуальных методов. Она содержит адреса всех виртуальных методов класса, включая и унаследованные от предков. Перед таблицей виртуальных методов расположена специальная структура, содержащая дополнительную информацию. В ней содержатся данные, полностью характеризующие класс: имя, размер экземпляров, указатели на класс-предок и т.д. Одно из полей структуры содержит адрес таблицы динамических методов класса (DMT). Таблица имеет следующий формат: в начале - слово, содержащее количество элементов таблицы. Затем - слова, соответствующие индексам методов. Нумерация индексов начинается с –1 и идет по убывающей. После индексов идут собственно адреса динамических методов. Следует обратить внимание на то, что DMT объекта Obj1 состоит из двух элементов, Obj2 - из одного, соответствующего перекрытому методу DynamicMethod1. В случае вызова Obj2.DynamicMethod2 индекс не будет найден в DMT Obj2, и произойдет обращение к DMT Obj1. Именно так экономится память при использовании динамических методов.

 

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

 

vmtSelfPtr = -76

vmtIntfTable = -72

vmtAutoTable = -68

vmtInitTable = -64

vmtTypeInfo = -60

vmtFiledTable = -56

vmtMethodTable = -52

vmtDynamicTable = -48

vmtClassName = -44

vmtInstanceSize = -40

vmtParent = -36

vmtSafeCallException = -32

vmtAfterConstruction = -28

vmtBeforeDestruction = -24

vmtDispatch = -20

vmtDefaultHandler = -16

vmtNewInstance = -12

vmtFreeInstance = -8

vmtDestroy = -4

 

Поля vmtDynamicTable, vmtDispatch и vmtDefaultHandler отвечают за вызов динамических методов. Поля vmtNewInstance, vmtFreeInstance и vmtDestroy содержат адреса методов создания и уничтожения экземпляров класса. Поля vmtIntfTable, vmtAutoTable, vmtSafeCallException введены для обеспечения совместимости с моделью объекта COM. Остальные поля доступны через методы объекта tObject. В Object Pascal эта информация играет важную роль и может использоваться программистом неявно. В языке определены два оператора - is и as, неявно обращающиеся к ней. Оператор is предназначен для проверки совместимости по присвоению экземпляра объекта с заданным классом. Выражение вида:

 

AnObject is tObjectType

 

Принимает значение True только если объект AnObject совместим по присвоению с классом tObjectType, то есть является объектом этого класса или его потомком.

 

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

 

with AObjectOfSomeType as tAnotherType do . . .

 

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

 

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

 

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

 

Разумеется, методы класса не могут использовать значения, содержащиеся в полях класса: ведь экземпляра класса не существует!!! Методы класса служат для извлечения внутренней информации класса. Ниже перечислены методы класса tObject:

 

3.Метод и Описание

 

сlass function ClassName:ShortString

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

 

сlass function ClassNameIs(const Name:ShortString):Boolean

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

 

сlass function ClassParent:tClass

Возвращает указатель на родительский класс

 

сlass function ClassInfo:pointer

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

 

сlass function InstanceSize:Longint

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

 

сlass function InheritsFrom (aClass: tClass):Boolean

Возвращает True, если данный класс наследует от заданного

 

сlass function MethodAddress(const Name:ShortString):Pointer

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

 

сlass function MethodName (Addres: pointer):ShortString

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

 

В Delphi 4 в класс tObject добавлены еще два виртуальных метода - AfterConstruction и BeforeDestruction. Как следует из названия, они вызываются сразу после создания экземпляра объекта и непосредственно перед уничтожением.

 

4.Перегрузка методов

 

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

 

Type

tFirstClass=class

E:extended;

procedure SetData(aValue: Extended); overload;

end;

 

tSecondClass=class(tFirstClass)

I:integer;

procedure SetData(aValue: Integer); overload;

end;

 

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

 

Для перегрузки виртуального метода используется зарезервированное слово reintroduce:

 

procedure SetData(aValue:string); reintrouce;overload;

 

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

 

5.Абстрактные методы

 

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

 

Procedure NeverCallMe; virtual; abstract;

 

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

 

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

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

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

Итак, инкапсуляция означает, что методы (коды) и данные одновременно представлены в одной и той же структуре.

Например,

Type

            Coordinates = class

                                       x, y : byte;

                                       procedure Init (Xinit, Yinit: byte);

                                       function GetX : byte;

                                       function GetY : byte;

                                   end;