Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

[ Миронченко ] Императивное и объектно-ориентированное програмирование на Turbo Pascal и Delphi

.pdf
Скачиваний:
68
Добавлен:
25.04.2014
Размер:
3.16 Mб
Скачать

301

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;