![](/user_photo/1319_hJRrZ.png)
[ Миронченко ] Императивное и объектно-ориентированное програмирование на Turbo Pascal и Delphi
.pdf301
31:if (Kl1(K2) is Kl1) then //TRUE
32:writeln('Kl1(K2) ist Kl1');
33:writeln('Wirklicher Name:', KL1(K2).ClassName);//настоящее имя
34:{ //Если раскомментировать этот фрагмент, то компилятор
выдаст ошибку
35:if ((K2 as Kl1) is Kl1) then
36:writeln('Kl1(K2) ist Kl1');
37:}
38:p:=Kl1.Create;
39:if TObject(p) is Kl1 then //TRUE
40:writeln('Name ',TObject(p).ClassName);
41:readln;
42:end.
Т.к. Obj ссылается на объект класса Kl2, то условие в строке 26 не выполнится, а условие в строке 28 будет верным.
Заметьте (строка 31), что хотя объект К2 принадлежит классу Kl2, но если его привести к типу Kl1, то условие в строке 31 выполнится. А функцию ClassName этим не обманешь – она все равно выдаст настоящее имя. Убрав комментарий в строках 3437 и запустив программу, вы убедитесь, что компилятор выдаст ошибку на этапе компоновки программы и до выполнения дело даже не дойдет.
В строке 38 был создан объект класса Kl1 и его адрес в динамической памяти был занесен в бестиповый указатель. При этом можно без проблем приводить этот указатель к типу TObject и потом проверять принадлежность объекта, адрес которого находится в переменной p на принадлежность типу Kl1. В данном случае мы приводим бестиповый указатель к ссылке на объект класса TObject, что можно сделать лишь обычным приведением типов; оператор as в данном случае использовать нельзя, т.к. компилятор на этапе компоновки скажет вам, что бестиповый указатель приводить к классу нельзя.
Вы видите, что смешанное использование операции приведения типов и операторов is, as приводит к неожиданным результатам, поэтому гораздо лучше не использовать бестиповые указатели как ссылки на объекты – для этого объявляйте ссылку на TObject и старайтесь не использовать операцию приведения типов при работе с объектами.
16.14. Метаклассы
Часто нужна ссылка не на объект, а на дескриптор класса. Вы уже знаете, что название класса служит в коде ссылкой на дескриптор этого класса. Но в таком случае вы можете ссылаться лишь на дескриптор какого-то конкретного класса. Для того, чтобы достичь общности, в Delphi вводится понятие метакласса:
•Метакласс – тип данных, переменной которого являются ссылки на дескрипторы классов.
Спомощью метаклассов можно передавать в качестве параметров методов ссылки на дескрипторы классов.
Описываются метаклассы так:
MetaKlName = class of Etwas
302
Где MetaKlName – это название метакласса, а Etwas – название любого класса. Например:
type
MetaKlasse = class of TControl
Значением объекта этого метакласса (метаобъекта) может быть ссылка на дескриптор любого класса, являющегося наследником TControl, либо самим TControl.
Пример 10: Класс, порождающий объекты других классов.
1 : {$APPTYPE CONSOLE}
2 : uses
3 : SysUtils;
4 : type
5 : MetaKlasse = class of TObject;
6 :
7 : KLasse = class
8 : public
9 : x:integer; 10: end;
11:
12:AufBauer = class
13:public
14:class function GibObjekt(MK:MetaKlasse):TObject;
15:end;
16:
17:class function AufBauer.GibObjekt(MK:MetaKlasse):TObject;
18:begin
19:Result:=MK.Create;
20:end;
21:
22:var
23:MK:MetaKlasse;
24:k:TObject;
25:begin
26:MK:=KLasse;
27:k:=Aufbauer.GibObjekt(MK);
28:Klasse(k).x:=10;
29:writeln(Klasse(k).x);
30:readln;
31:end.
Встроке 5 объявляется метакласс, метаобъектом которого может быть ссылка на дескриптор любого класса. В строке 12 объявляется класс Aufbauer (aufbauen – сооружать, создавать, cоответственно Aufbauer – конструктор, создатель). У этого класса есть только 1 метод, причем – метод класса, который принимает метаобъект (содержащий ссылку на класс) и возвращает ссылку на TObject. Все, что делает эта функция сводится к вызову конструктора класса, ссылку на который она получила в качестве параметров. Кстати, сам объект класса AufBauer может быть создан той же функцией GibObjekt. Так что с помощью метаклассов можно вообще избавиться от вызова конструкторов в коде, а вместо этого всегда поручать это дело ауфбауеру.
303
Ясно, что по определению нельзя объявить метакласс метакласса, кроме того, нельзя наследовать от метакласса (наследовать можно лишь от обычного класса).
Ссылку на дескриптор класса объекта, на который ссылается объектоссылка и ссылку на дескриптор родителя этого класса можно определить с помощью методов, которые определены в классе TObject:
type
TClass = class of TObject;
function ClassType: TClass; //возвращает тип объекта
class function ClassParent: TClass; //возвращает тип родителя объекта
Т.к. метод ClassParent объявлен как метод класса, то можно на этапе прогона программы узнавать родителя любого класса. Делать метод ClassType методом класса смысла нет, т.к. тогда получится, что вы используете ссылку на класс для получения ссылки на этот же класс.
В следующем примере вводится наследник класса TObject, который содержит дополнительный метод класса, который возвращает уровень вложенности класса (TObject считается первым уровнем).
Пример 11: Уровень вложенности класса.
1 : program ClassPar;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : uses
6 : SysUtils;
7 :
8 : type
9 : NeuesObjekt = class(TObject)
10:class function Niveau:integer; //возвращает уровень
вложенности класса
11: end; 12:
13:Klasse = class(NeuesObjekt)
14:end;
15:
16:class function NeuesObjekt.Niveau:integer;
17:var
18:KL:TClass;
19:n:integer;
20:begin
21:kl:=ClassParent;
22:n:=1;
23:while (kl<>nil) do
24:begin
25:kl:=kl.ClassParent;
26:inc(n);
27:end;
28:result:=n;
29:end;
304
30:
31:
32:begin
33:writeln(Klasse.Niveau); // 3
34:readln;
35:end.
16.15. Методы метаклассов и метаобъектов
Пусть объявлен следующий метакласс:
type
MetaKl = class of Etwas;
Тогда у самого метакласса MetaKl и любого его метаобъекта методами будут все class-методы класса Etwas. Методы класса, так же, как и обычные методы, могут переопределяться статически и динамически.
Пример 12: Статическое переопределение методов метаклассов.
1 : program MetaStat;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : type
6 : neu = class
7 : public
8 : class function Etwas:integer;
9 : end; 10:
11:NeuKind = class(Neu)
12:public
13:class function Etwas:integer;
14:end;
15:
16: MetaNeu = class of Neu; //объявляем метакласс 17:
18:class function Neu.Etwas:integer;
19:begin
20:result:=10;
21:end;
22:
23:class function NeuKind.Etwas:integer;
24:begin
25:result:=121;
26:end;
27:
28:var
29:MN:MetaNeu;
30:begin
31:MN:=Neu;
32:writeln(MN.Etwas); //10
33:MN:=NeuKind;
305
34:writeln(MN.Etwas); //10
35:readln;
36:end.
Пример 13: Динамическое переопределение методов метаклассов.
1 : program MetaVirt;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : type
6 : neu = class
7 : public
8 : class function Etwas:integer;virtual;
9 : end; 10:
11:NeuKind = class(Neu)
12:public
13:class function Etwas:integer;override;
14:end;
15:MetaNeu = class of Neu;
16:
17:class function Neu.Etwas:integer;
18:begin
19:result:=10;
20:end;
21:
22:class function NeuKind.Etwas:integer;
23:begin
24:result:=121;
25:end;
26:
27:var
28:MN:MetaNeu;
29:begin
30:MN:=Neu;
31:writeln(MN.Etwas); //10
32:MN:=NeuKind;
33:writeln(MN.Etwas); //121
34:readln;
35:end.
16.16. Свойства
Кроме полей и методов у класса есть еще свойства. Они используются для регулирования доступа к полям класса.
Давайте разберемся в том как они работают на примере. В нем мы создадим класс MatrKarte, у которого будет 2 поля – фамилия матроса и фамилия капитана корабля. Чтобы удовлетворить требованию инкапсуляции, надо сделать эти поля закрытыми. Но получать доступ к ним можно будет с помощью свойств, а не методов.
Пример 14: Свойства.
306
1 : program EigenSch;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : uses
6 : SysUtils;
7 :
8 : type
9 : MatrKarte = class //карточка матроса
10:private
11:Matr:string; //фамилия матроса
12:Kap:string; //фамилия капитана корабля
14:procedure SchrZeile(z:string);
15:function gibKapit:string;
16:public
17:constructor Bilde(const MatrName,KapitName:string);
18:property Matrose:string read Matr write SchrZeile;
19:property Kapitan:string read GibKapit;
20:end;
21:
22:constructor MatrKarte.Bilde(const MatrName,KapitName:string);
23:begin
24:matrose:=MatrName; //Но не matr:=MatrName;
25:Kap:=KapitName; //Но не Kapitan:=KapitName;
26:end;
27:
28:procedure MatrKarte.SchrZeile(z:string);
29:begin
30:if z<>'AAA' then
31:self.Matr:=z
32:else
33:self.Matr:='ZZZ';
34:end;
35:
36:function MatrKarte.gibKapit:string;
37:begin
38:result:=Kap;
39:end;
40:
41:var
42:K:MatrKarte;
43:i:integer;
44:begin
45:
46:K:=MatrKarte.Bilde('AAA','Schlachter');
47:writeln(K.Matrose);
48:K.Matrose:='AAD';
49:writeln(K.Matrose);
50:writeln(K.Kapitan);
51:
52:readln;
307
53:end.
Встрочках 18-19 объявляются 2 свойства: Matrose, Kapitan (правильно - Kapitän). Обращаться к свойствам можно с помощью операции точка, как к полям и методам. Свойства могут быть доступны либо для чтения, либо для записи, либо и для того и для другого. Способ доступа определяется методами или полями, которые находятся после ключевых слов read, write в описании свойства.
Например, в строке 18 описано свойство Matrose. После слова read стоит название поля Matr. Это означает, что при попытке прочитать данные свойства Matrose будет выдано значение поля Matr. А после слова write стоит название метода SchrZeile. Это означает, что при попытке присвоить свойству Matrose какое-то значение, будет автоматически вызван метод SchrZeile, параметром которого будет строка, которую вы хотите присвоить значению свойства Matrose.
Свойство Kapitan доступно только для чтения.
Кстати, если свойство доступно и для чтения, и для записи, то по синтаксису вы должны сначала описать действия при чтении, а потом – действия при записи.
Например, следующее описание было бы неверным: property Matrose:string write SchrZeile read Matr;
16.17. Индексированные свойства
Можно объявлять и индексированные свойства. Для этого после названия свойства надо в квадратных скобках указать параметр (или параметры), согласно которым будут индексироваться элементы массива, например:
property Mass[Ind:integer]:integer read GibZahl write StellZahl; property Mass[i,j:integer]:TObject read GibObj write StellObj;
Если свойство индексировано, то при чтении и записи свойства можно ссылаться лишь на методы, причем набор параметров должен быть таким:
Функция, отвечающая за чтение (read), должна содержать параметры, указанные в квадратных скобках (в том же порядке следования).
Процедура, с помощью которой надо устанавливать значения полей, должна содержать параметры, указанные в квадратных скобках в порядке их следования и затем еще один параметр того же типа, что и свойство.
В следующем примере мы напишем класс, который представляет собой обертку для целочисленного массива, причем этот массив может содержать лишь положительные числа. У класса будет индексированное свойство, а функции, с помощью которых можно будет получать значения элементов массива будут проверять, не вышел ли элемент за границы массива и будет ли он неотрицательным. Если пользователь запрашивает элемент массива с несуществующим индексом, то метод возвращает в качестве результата 0.
Пример 15: Использование индексированных свойств.
1 : program ArrProp;
2 :
3 : {$APPTYPE CONSOLE}
4 :
5 : uses
308 |
|
|
6 |
: |
SysUtils; |
7 |
: |
|
8 |
: const |
|
9 |
: |
n=5; |
10:type
11:Mas = array [1..n] of integer;
13:MasKlass = class
14:private
15:R:Mas;
16:procedure StellZahl(Index:integer;G:integer);
17:function GibZahl(Index:integer):integer;
18:public
19:property Mass[Ind:integer]:integer read GibZahl write StellZahl;
20:function ZurZeile:string; //преобразует в MasKLass строку
21:end;
22:
23:procedure MasKlass.StellZahl(Index:integer;G:integer);
24:begin
25:if index>n then //если индекс вышел за границы массива
26:exit;
27:if G>=0 then
28:R[Index]:=G
29:else
30:R[Index]:=0;
31:end;
32:
33:function MasKlass.GibZahl(Index:integer):integer;
34:begin
35:if index > n then
36:result:=0
37:else
38:result:=R[index];
39:end;
40:
41:function MasKlass.ZurZeile:string;
42:var
43:i:integer;
44:begin
45:Result:='';
46:for i:=1 to n do
47:Result:=Result+IntToStr(R[i])+' ';
48:end;
49:
50:var
51:MK:MasKlass;
52:i:integer;
53:begin
54:MK:=MasKlass.Create;
55:for i:=1 to n do
56:MK.Mass[i]:=i - 2;
309
57:writeln(MK.ZurZeile);
58:readln;
59:end.
Зачем вообще нужны свойства? Если мы хотим получить доступ к полю, достаточно написать 2 метода. Зачем еще вводить свойство, которое бы вызывало эти 2 метода? Один из возможных ответов – в удобстве для пользователя класса: не надо запоминать 2 метода, а достаточно лишь одно свойство. Кроме того, иногда можно сэкономить на методах, ссылаясь непосредственно на поле.
Но этих доводов мало для того, чтобы вводить свойства. Принципиальнее то, что свойства вводят как бы внутреннюю регуляцию у объектов класса: мы передаем информацию объекту, а он сам распоряжается ею (подключает собственные методы). К вопросу о полезности или бессмысленности свойств мы еще вернемся в последней главе этой книги. Там же мы рассмотрим иной способ введения саморегуляции, который будет гораздо более общим, чем использование свойств.
Так или иначе, но свойства активно используются в встроенной библиотеке классов Delphi, поэтому знать как они работают важно для программирования в среде
Delphi.
16.18. Взаимодействие классов
Часто случается ситуация, когда первый класс использует второй как параметр одной из функций, а второй класс использует аналогичным образом первый класс. Простейший пример приведен ниже:
Пример 16: Зацикливание при обращении к классам.
{$APPTYPE CONSOLE}
uses SysUtils;
type
Kl1 = class
procedure P1(Obj:Kl2); end;
Kl2 = class
procedure P2(Obj:Kl1); end;
procedure Kl1.P1(Obj:Kl2); begin
end;
procedure Kl2.P2(Obj:Kl1); begin
end;
begin end.
310
Хотя мы можем создавать лишь ссылки на объекты, но при этом компилятор все равно хочет, чтобы они не ссылались одна на другую. Чтобы такую зависимость сделать возможной, в Delphi можно делать предварительное объявление класса. Так можно исправить ошибки в предыдущем примере:
Пример 17: Обход зацикливания.
{$APPTYPE CONSOLE} uses
SysUtils; type
KL2 = class; //предварительное объявление
KL1 = class
procedure P1(Obj:KL2); end;
KL2 = class
procedure P2(Obj:KL1); end;
procedure Kl1.P1(Obj:Kl2); begin
end;
procedure Kl2.P2(Obj:Kl1); begin
end;
begin end.
До объявления класса KL1 надо сделать предварительное объявление класса KL2. Для этого достаточно написать строку:
Имя_Класса = class;
Только не перепутайте с объявлением нового класса:
Имя_Класса = class end;
16.19. Интерфейсы
Мы с вами уже знаем записи, которые представляют собой набор данных и классы – данные и функции «в одном флаконе». Не хватало лишь набора функций. Для симметрии решили добавить и такой тип данных, и назвали его интерфейсом. Кроме методов могут быть в интерфейсе и свойства, но ссылаться они должны, естественно, лишь на методы интерфейса.
Вот пример интерфейса:
MeinInterf = interface (IInterface) function ZurZeile:string;
function MeineFunk(x,y:real):real; end;