Современные технологии программирования
Конспект лекций
назад | содержание | вперед

Лекция 7. Наследование и методы

Содержание

Введение *

Изучаемые вопросы: *

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

Классификация методов *

Совместимость по присваиванию для объектов *

Полиморфизм *

Виртуальные и динамические методы *

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

Контрольные вопросы *

Источники дополнительных сведений *



Введение

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

Наследование – возможность повторного использования кода. Наследуются поля, методы, свойства; поля, методы, свойства можно перекрывать в классах потомках. Перекрытые методы доступны в дочернем классе с помощью зарезервированного слова inherited. Подстановка адресов кодов статических методов (раннее связывание) отличается от динамических и виртуальных методов (позднее связывание). Для объявления методов общих для всей иерархии классов используют абстрактные методы.

Изучаемые вопросы:

Наследование полей и методов.

Перекрытие методов в классах потомках.

Совместимость по присваиванию для переменных объектовых типов.

Классификация методов.

Полиморфизм.




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

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

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

//----------------------------

TMyClass = class(TObject)

End;

//----------------------------

TMyClass = class

End;

//----------------------------

Класс Tobject несёт серьёзную нагрузку, и мы рассмотрим его позже.

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

Рассмотрим поведение методов при наследовании. По тому, какие действия происходят при вызове, методы делятся на три группы: статические (static), виртуальные (virtual) и динамические (dynamic).

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

//----------------------------

Type

//----------------------------

TClass1 = class

i: real;

procedure SetData(AValue: real);

end;

//----------------------------

TClass2 = class

i: integer;

procedure SetData(AValue: integer);

end;

//----------------------------

procedure TClass1.SetData;

begin

i:= AValue;

end;

//----------------------------

procedure TClass2.SetData;

begin

i:= AValue;

inherited SetData(0.99);

end;

//----------------------------

var

O1: TClass1; O2: TClass2;

begin

O2:= TClass1.Create;

O2.i:= 10;//В поле типа integer будет занесено значение 10

В этом примере разные методы с именем SetData присваивают значения разным полям с именем i. Перекрытое поле предка недоступно в потомке; поэтому, конечно, два одноимённых поля с именем i приведены только для примера.

В отличие от поля, внутри других методов перекрытый метод родителя доступен в методах дочернего класса при указании зарезервированного слова inherited. Методы объектов по умолчанию являются статическими – их адреса определяются ещё на стадии компиляции проекта (раннее связывание). Они вызываются быстрее всего.

Пример1. Перекрытие и наследование статических методов

В примере ниже реализована следующая иерархия классов:

Рис. Иерархия классов

//------------------------------------------------------------

unit UInherit;

interface

type

//------------------------------------------------------------

T = class(TObject)

private

Fld: integer;

public

function info: String;

function GetFld:Integer;

constructor Create(new:Integer);

end;

//------------------------------------------------------------

T1 = class(T)

private

Fld1: Real;

public

function info: integer;//перекрываем

function GetFld1:Real;

constructor Create(new:Integer;new1:Real);//перекрываем

end;

//------------------------------------------------------------

implementation

//------------------------------------------------------------

constructor T.Create(new:Integer);

begin

Fld:= new

end;

//------------------------------------------------------------

function T.GetFld:Integer;

begin

result:= Fld

end;

//------------------------------------------------------------

function T.Info;

begin

result:= ClassName;//метод ClassName унаследован от TObject

end;

//------------------------------------------------------------

constructor T1.Create(new:Integer; new1:Real);

begin

inherited Create(new);//вызов родительского метода

Fld1:= new1;

end;

//------------------------------------------------------------

function T1.GetFld1:Real;

begin

result:= Fld1

end;

//------------------------------------------------------------

function T1.info;

begin

result:= InstanceSize;//метод InstanceSize унаследован от TObject

end;

end.

//------------------------------------------------------------

program PInherit;

uses

Forms,

UInherit in 'UInherit.pas';

{$R *.RES}

var O1: T1;

O: T;

begin

O:= T.Create(15) ;

writeln(O.GetFld:6);//15

writeln(O.info:6);//T

O1:= T1.Create(15,2.5);

writeln(O1.GetFld:6);//15

writeln(O1.GetFld1:6:2);//2.50

writeln(O1.info:6);//16

O1.Create(10,20);

writeln(O1.GetFld:6);//10

writeln(O1.GetFld1:6:2);//20.00

O.Free;

O1.Free;

//размер переменной типа integer

writeln(sizeof(integer):6);//4

//размер переменной типа real

writeln(sizeof(real):6);//8

readln;

end.

//------------------------------------------------------------

В этом примере класс Т1 наследует от класса T поле Fld и метот GetFld. Метод info и конструктор Create класса T мы переопределяем в классе T1. Поле Fld1 и метод GetFld1 мы определяем заново в классе T1.

Конструктор класса T1 мы применяем к уже созданному объекту:

O1.Create(10,20);

В таком случае конструктор выполняет действия, которые описаны в его разделе операторов программистом. В нашем примере – это занесение новых значений в поля объекта. Системные действия по созданию объекта не выполняются. Распределение памяти под объекты классов T и T1приведена на рисунке ниже.

Рисунок. Объекты на физическом уровне

Пример. Наследование и переопределение.

В примере ниже реализована следующая иерархия классов:

unit Unit1;

interface

type

//----------------------------

A = class

procedure G;

procedure H;

end;

//----------------------------

B = class(A)

procedure G;

procedure I;

end;

//----------------------------

C = class(B)

procedure S;

procedure H;

end;

//----------------------------

implementation

//----------------------------

procedure A.G;

begin

writeln('A.G');

end;

//----------------------------

procedure A.H;

begin

writeln('A.H');

end;

//-------------------------------

procedure B.G;

begin

writeln('B.G');

end;

//----------------------------

procedure B.I;

begin

writeln('B.I');

end;

//----------------------------

procedure C.S;

begin

writeln('C.S');

end;

//----------------------------

procedure C.H;

begin

writeln('C.H');

end;

end.

//----------------------------

program Project2;

{$APPTYPE CONSOLE}

uses

SysUtils,

Unit1 in 'Unit1.pas';

var

o1: A; o2: B; o3: C;

begin

o1:= A.Create;

o2:= B.Create;

o3:= C.Create;

o2.H;//A.H

o2.G;//B.G

o3.H;//C.H

o3.G;//B.G

readln;

end.

Класс B унаследовал метод H от класса A. Метод G в классе B переопределён. Метод I в классе B определён заново.

Для объектов класса C можно использовать методы: G (унаследован от класса B), I (унаследован от класса B), S (определён в классе C), H (определён в классе C).

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

//----------------------------

Type

//----------------------------

TClass1 = class

End;

//----------------------------

TClass2 = class(TClass1)

End;

//----------------------------

TClass3 = class(TClass2)

//TClass3 наследует одновременно свойства класса TClass1 и //TClass2

End;

//----------------------------

или

//----------------------------

TClass3 = class

FObj1: TClass1;

FObj2: TClass2;

//TClass3 наследует одновременно свойства класса TClass1 и //TClass2

End;

//----------------------------

Принципиально отличаются от статических методов виртуальные и динамические методы. Они могут быть объявлены так путём добавления соответствующей директивы virtual или dynamic. Адреса таких методов определяется по специальной таблице во время выполнения программы (позднее связывание).

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





Классификация методов

В Object Pascal определены следующие виды методов:

Совместимость по присваиванию для объектов

Рассмотрим следующую иерархию классов:

Здесь T,T1,T2,T3,T11,T31 – идентификаторы объектовых типов. Тогда типы T1,T2,T3 для типа T будут дочерними типами, а тип T для них будет родительским типом. Типы T1,T2,T3,T11,T31 для типа T будут потомками, а тип T для них будет предком.

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

Var

O: T; O1: T1; O2: T2; O3: T3; O11: T11; O31: T31;

O:= O1;//допустимо

O:= O2; //допустимо

O:= O3; //допустимо

O1:= O11; //допустимо

O3:= O31; //допустимо

O3:= O11; //не допустимо

O11:= O31; //не допустимо

O31:= O3; //не допустимо

 

Полиморфизм

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

//----------------------------

unit Unit1;

interface

uses SysUtils;

type

//----------------------------

TField = class

function GetData: String;virtual;abstract;

end;

//----------------------------

TStringF = class(TField)

Data: String;

function GetData: String;override;

end;

//----------------------------

TIntF = class(TField)

Data: Integer;

function GetData: String;override;

end;

//----------------------------

TRealF = class(TField)

Data: Real;

function GetData: String;override;

end;

//----------------------------

procedure ShowField(f: TField);

//----------------------------

implementation

//----------------------------

function TStringF.GetData: String;

begin

result:= Data

end;

//----------------------------

function TIntF.GetData: String;

begin

result:= IntToStr(Data)

end;

//----------------------------

function TRealF.GetData: String;

begin

result:= FloatToStr(Data)

end;

//----------------------------

procedure ShowField(f: TField);

begin

writeln(f.GetData);

//полиморфный объект f

end;

end.

//----------------------------

program Project2;

{$APPTYPE CONSOLE}

uses

SysUtils,

Unit1 in 'Unit1.pas';

var

s: TStringF;

i: TIntF;

r: TRealF;

begin

s:= TStringF.Create;

s.Data:= 'nnn';

i:= TIntF.Create;

i.Data:= 5;

r:= TRealF.Create;

r.Data:= 2.3;

ShowField(s);

ShowField(i);

ShowField(r);

readln;

end.

//----------------------------

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

Правило совместимости типов языка говорят о том, что переменной объектного типа может быть присвоен указатель на экземпляр любого типа потомка. В процедуре ShowData параметр описан как TField – это значит, что в неё можно передать указатели на объекты классов TStringF, TIntF,TRealF и любого другого потомка TField.

Но какой (точнее, чей) метод GetData будет вызван? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом, и он, представляет наиболее важную особенность ООП. Допустим, вы имеете дело с некоторой совокупностью явлений или процессов. Чтобы смоделировать их средствами ООП, необходимо выделить их самые общие признаки – свойства и поведение. Те из них, которые не изменяют своего содержания, должны быть реализованы в виде статических методов. Те же, которые варьируют при переходе от общего к частному, лучше облечь в форму виртуальных методов. Основные, “родовые” признаки (методы) нужно описать в классе-предке и затем перекрывать их в классах-потомках. В нашем примере программисту, пишущему процедуру вроде ShowData, важно лишь одно – то, что любой объект, переданный в неё, является потомком TField и он умеет сообщить о значении своих данных (выполнив метод GateData). Если, к примеру, такую процедуру откомпилировать и поместить в динамическую библиотеку, то эту библиотеку можно будет раз и навсегда использовать без изменений, хотя будут появляться и новые, неизвестные в момент её создания классы-потомки TField.

Наглядный пример использования полиморфизма даёт сама Delphi. В ней имеется класс TComponent, на уровне которого сосредоточены определённые “правила” того, как взаимодействовать со средой разработки и с другими компонентами. Следуя этим правилам, можно порождать от TComponent свои компоненты, настраивая Delphi на решение ваших собственных задач.

 

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

Общим для виртуальных и динамических методов является то, что при их вызове адрес определяется не во время компиляции, а во время выполнения путём поиска в специальных таблицах. Такой поиск называется ещё “поздним связыванием”. Разница между методами заключается в поиске адреса.

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

Procedure Show(O: T);

Begin

O.VM;

End;

Так, если тип T из нашей иерархии типов, и в этой иерархии определён виртуальный метод VM, то на этапе компиляции невозможно найти этот метод, поскольку при вызове процедуры Example вместо формального параметра O в качестве фактического параметра может быть передан объект потомок для типа T. Поэтому поиск адреса метода будет отложен до выполнения программы.

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

//----------------------------

unit UVirt;

interface

uses SysUtils;

type

//----------------------------

T = class

function VM: String;virtual;

end;

//----------------------------

T1 = class(T)

function VM: String;override;

end;

//----------------------------

T2 = class(T1)

function VM: String;override;

end;

//----------------------------

implementation

//----------------------------

function T.VM: String;

begin

result:= ClassName

end;

//----------------------------

function T1.VM: String;

begin

result:= IntTostr(InstanceSize)

end;

//----------------------------

function T2.VM: String;

begin

result:= ClassParent.ClassName;

end;

end.

//-----------------------------

program PVirt;

{$APPTYPE CONSOLE}

uses

SysUtils,

UVirt in 'UVirt.pas';

//-----------------------------

procedure Show(o: T);

begin

write(o.VM,',')

end;

//-----------------------------

var

O: T; O1: T; O2: T;

I: T; I1: T1; I2: T2;

begin

O:= T.Create;

O1:= T1.Create;

O2:= T2.Create;

Show(O);//Будет выведено: T

Show(O1);// Будет выведено: 4

Show(O2);// Будет выведено: T1

readln;

I:= T.Create;

I1:= T1.Create;

I2:= T2.Create;

writeln(I.VM,',',I1.VM,',',I2.VM);// Будет выведено:T,4,T1

readln;

end.

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

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

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

Пример 2. Перекрытие динамических и виртуальных методов.

Type

//----------------------------

TClass1 = class

FF1: integer;

FF2: real;

Procedure StatMethod;

Procedure VirtMethod1 ;virtual;

Procedure VirtMethod2; virtual;

Procedure DynaMethod1; dynamic;

Procedure DynaMethod2; dynamic;

End;

//----------------------------

TClass2 = class(TClass1)

Procedure StatMethod;

Procedure VirtMethod1; override;

Procedure DynaMethod1; override;

//----------------------------

End;

Var

Obj1: TClass1;

Obj2: TClass2;

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

Пример 3. Перекрытие виртуального метода.

//----------------------------

unit Unit1;

interface

uses SysUtils;

type

//----------------------------

TClass = class

function VM: String;virtual;

end;

//----------------------------

TClass1 = class(TClass)

function VM: String;virtual;//создаётся новый одноимённый метод

end;

//----------------------------

implementation

//----------------------------

function TClass.VM: String;

begin

result:= ClassName

end;

//----------------------------

function TClass1.VM: String;

begin

result:= IntTostr(InstanceSize)

end;

end.

//----------------------------

program Project2;

{$APPTYPE CONSOLE}

uses

SysUtils,

Unit1 in 'Unit1.pas';

var O: TClass; O1: TClass1;

begin

O:= TClass.Create;

O1:= TClass1.Create;

writeln(O.VM,' ', O1.VM);

readln;

O:= TClass1.Create;

writeln(O.VM);//TClass

readln;

end.

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

//----------------------------

unit UVirt;

interface

uses SysUtils;

type

//----------------------------

T = class

function VM: String;virtual;

end;

//----------------------------

T1 = class(T)

function VM: String;override;

end;

//----------------------------

T2 = class(T1)

function VM: String;override;

end;

//----------------------------

implementation

//----------------------------

function T.VM: String;

begin

result:= ClassName

end;

//----------------------------

function T1.VM: String;

begin

result:= IntTostr(InstanceSize)

end;

//----------------------------

function T2.VM: String;

begin

result:= ClassParent.ClassName;

end;

end.

//-----------------------------

program PVirt;

{$APPTYPE CONSOLE}

uses

SysUtils,

UVirt in 'UVirt.pas';

//-----------------------------

procedure Show(o: T);

begin

write(o.VM,',')

end;

//-----------------------------

var

O: T; O1: T; O2: T;

I: T; I1: T1; I2: T2;

begin

O:= T.Create;

O1:= T1.Create;

O2:= T2.Create;

Show(O);//Будет выведено: T

Show(O1);// Будет выведено: 4

Show(O2);// Будет выведено: T1

readln;

I:= T.Create;

I1:= T1.Create;

I2:= T2.Create;

writeln(I.VM,',',I1.VM,',',I2.VM);// Будет выведено:T,4,T1

readln;

end.

 

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

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

Procedure NeverCallMe; virtual; abstract;

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

Пример. Массив объектов. Наследование. Статические методы.

//-------------------------------------------

unit UInherit;

interface

//-------------------------------------------

type

//-------------------------------------------

Base = class//базовый класс

procedure Put;

end;

//-------------------------------------------

Derv1 = class(Base)//первый производный клас

procedure Put;

end;

//-------------------------------------------

Derv2 = class(Base)//второй производный клас

procedure Put;

end;

//-------------------------------------------

implementation

//-------------------------------------------

procedure Base.Put;

begin

writeln('Base');

end;

//-------------------------------------------

procedure Derv1.Put;

begin

writeln('Derv1');

end;

//-------------------------------------------

procedure Derv2.Put;

begin

writeln('Derv2');

end;

//-------------------------------------------

end.

//-------------------------------------------

program PInherit;

{$APPTYPE CONSOLE}

uses

SysUtils,

UVirt in 'UVirt.pas',

UInherit in 'UInherit.pas';

var

i: integer;

vec: array[0..2] of Base;

begin

vec[0]:= Derv1.Create;

vec[1]:= Derv2.Create;

vec[2]:= Derv1.Create;

for i:=0 to 2 do vec[i].Put;//раннее связывание; вызывается метод

//Put класса Base

readln;

end.

//-------------------------------------------

Будет выведено на экран:

Base

Base

Base

Пример. Массив объектов. Наследование. Виртуальные методы.

unit UVirt;

interface

//-------------------------------------------

type

//-------------------------------------------

Base = class//базовый класс

procedure Put;virtual;

end;

//-------------------------------------------

Derv1 = class(Base)//первый производный клас

procedure Put;override;

end;

//-------------------------------------------

Derv2 = class(Base)//второй производный клас

procedure Put;override;

end;

//-------------------------------------------

implementation

//-------------------------------------------

procedure Base.Put;

begin

writeln('Base');

end;

//-------------------------------------------

procedure Derv1.Put;

begin

writeln('Derv1');

end;

//-------------------------------------------

procedure Derv2.Put;

begin

writeln('Derv2');

end;

//-------------------------------------------

end.

//-------------------------------------------

program PVirt;

{$APPTYPE CONSOLE}

uses

SysUtils,

UVirt in 'UVirt.pas';

var

i: integer;

vec: array[0..2] of Base;

begin

vec[0]:= Base.Create;

vec[1]:= Derv1.Create;

vec[2]:= Derv2.Create;

for i:=0 to 2 do vec[i].Put;

readln;

{ TODO -oUser -cConsole Main : Insert code here }

end.

Будет выведено на экран:

Base

Derv1

Derv2

Пример. Массив объектов. Наследование. Абстрактные методы.

//-------------------------------------------

unit UVirt;

interface

//-------------------------------------------

type

//-------------------------------------------

Base = class//базовый класс

procedure Put;virtual;abstract;

end;

//-------------------------------------------

Derv1 = class(Base)//первый производный клас

procedure Put;override;

end;

//-------------------------------------------

Derv2 = class(Base)//второй производный клас

procedure Put;override;

end;

//-------------------------------------------

implementation

//-------------------------------------------

procedure Derv1.Put;

begin

writeln('Derv1');

end;

//-------------------------------------------

procedure Derv2.Put;

begin

writeln('Derv2');

end;

//-------------------------------------------

end.

//-------------------------------------------

program PVirt;

{$APPTYPE CONSOLE}

uses

SysUtils,

UVirt in 'UVirt.pas',

UInherit in 'UInherit.pas';

var

i: integer;

vec: array[0..2] of Base;

begin

vec[0]:= Derv1.Create;

vec[1]:= Derv2.Create;

vec[2]:= Derv1.Create;

for i:=0 to 2 do vec[i].Put;//позднее связывание; вызывается метод

//класса в зависимости от типа указателя в

//vec[i]

readln;

end.

//-------------------------------------------

Будет выведено на экран:

Derv1

Derv2

Derv1

 

 

Контрольные вопросы

  1. Что такое наследование?
  2. Что может наследовать один класс от другого?
  3. Как обращаться к унаследованным полям, методам и свойствам?
  4. Что такое перекрытие?
  5. Что можно перекрывать в дочернем классе?
  6. Можно ли получить доступ к перекрытым полям родителя?
  7. Можно ли получить доступ к перекрытым методам родителя?
  8. Как перекрываются статические методы?
  9. Как перекрываются виртуальные и динамические методы?
  10. Как определено правило совместимости по присваиванию для объектовых типов?
  11. Для чего используются абстрактные методы?
  12. Как объявляются абстрактные методы?

 

Источники дополнительных сведений

 

 

наверх


назад | содержание | вперед