Современные технологии программирования |
Конспект лекций |
Лекция 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
Контрольные вопросы
Источники дополнительных сведений