Глава 1

Основы объектно-ориентированного программирования

Многие главы этой книги вполне самостоятельны и могут быть прочитаны в любом порядке (или не прочитаны вовсе). Однако данная глава представляет собой без преувеличения краеугольный камень. Если вы — новичок в Delphi и не знакомы с объектно-ориентированным программированием (далее — ООП), то чтение последующих глав без этой будет сильно затруднено. Без преувеличения, 99% программ, созданных в Delphi, опираются на объекты и связанные с ними понятия. Поэтому без понимания внутренней логики работы внешне простых вещей далеко не уйти.

Даже если вы профессионал, советуем прочитать, изложенное ниже. Программисты из фирмы Inprise непрерывно совершенствуют язык Object Pascal и иерархию объектов. Об этом — также в этой главе.

Основные понятия

В этом разделе мы рассмотрим класс, объект, поле, метод и свойство.

Классом в Object Pascal называется особая структура, которая может иметь в своем составе поля, методы и свойства. Такой тип также будем называть объектным типом:

type

TMyObject = class(TObject) MyField: Integer;

Function MyMethod: Integer;

end;

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

var

AMyObject: TMyObject;

До введения термина "класс" в языке Pascal существовала двусмысленность определения "объект", который мог обозначать и тип, и переменную этого

типа. Теперь же существует четкая граница: класс — это описание, объект — то, что создано в соответствии с этим описанием.

Из чего состоит объект?

Поля объекта аналогичны полям записи. Это — данные, уникальные для каждого созданного в программе экземпляра класса. Описанный здесь класс TMyObject имеет одно поле — MyField.

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

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

Обратите внимание, что классы могут быть описаны либо в секции интерфейса (interface) модуля, либо на верхнем уровне вложенности секции реализации (implementation). He допускается описание классов "где попало", т. е. внутри процедур и других блоков кода.

Разрешено опережающее объявление классов, как в следующем примере:

type

TFirstObject = class;

TSecondObject = class(TObject) Fist : TFirstObject;

end;

TFirstObject == class(TObject) F2nd : TSecondObject;

end;

Создание и уничтожение объектов

Как создаются и уничтожаются объекты?

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

AMyObject := TMyObject.Create;

{ действия с созданным объектом }

AMyObj ect.Destroy;

Но, — скажет внимательный читатель, — ведь объекта еще нет, как мы можем вызывать его методы? Справедливое замечание. Однако обратите внимание, что вызывается метод TMyObject. Create, a нe AMyObject. Create.

Есть такие методы (в том числе конструктор), которые успешно работают до (или даже без) создания объекта. О таких методах, называемых методами класса, пойдет речь чуть ниже.

Те, кто раньше использовал ООП в работе на C++ и особенно в Turbo Pascal, будьте внимательны: в Object Pascal экземпляры объектов могут быть только динамическими. Это означает, что в приведенном выше фрагменте переменная AMyObject на самом деле является указателем, содержащим адрес объекта.

В Object Pascal конструкторов у класса может быть несколько. Общепринято называть конструктор create (в отличие от Borland Pascal 7.0, где конструктор обычно назывался Init, и от C++, где его имя совпадает с именем класса). Типичное название деструктора — Destroy.

type

TMyObject = class(TObject)

MyField: Integer;

Constructor Create;

Destructor Destroy;

Function MyMethod: Integer;

end;

Для уничтожения экземпляра объекта рекомендуется использовать метод Free, который первоначально проверяет указатель (не равен ли он nil) и только затем вызывает Destroy:

AMyObject.Free;

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

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

Чтобы правильно проинициализировать в создаваемом объекте поля, относящиеся к классу-предку, нужно сразу же при входе в конструктор вызвать конструктор предка при помощи зарезервированного слова

inherited:

constructor TMyObject.Create;

begin inherited Create;

end;

Взяв любой из примеров, прилагаемых к этой книге или поставляемых вместе в Delphi, вы почти не увидите там вызовов конструкторов и деструкторов. Почему? Дело в том, что любой компонент, попавший при визуальном проектировании в ваше приложение из Палитры компонентов, включается в определенную иерархию. Иерархия эта замыкается на форме (класс TForm): для всех ее составных частей конструкторы и деструкторы вызываются автоматически, незримо для программиста. Кто создает и уничтожает формы? Это делает приложение (глобальный объект с именем Application). В файле проекта (с расширением dpr) вы можете увидеть вызовы метода Application. createForm, предназначенного для этой цели.

Что же касается объектов, создаваемых вами динамически (во время выполнения приложения), то здесь нужен явный вызов конструктора и метода Free.

Инкапсуляция. Свойства

В предыдущих разделах мы ввели ряд новых понятий, которыми будем пользоваться в дальнейшем. Теперь поговорим о принципах, составляющих суть объектно-ориентированного программирования. Таких принципов три — инкапсуляция, наследование и полиморфизм.

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

Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В старых реализациях ООП (например, в Turbo Pascal) эта мысль внедрялась только посредством призывов и примеров в документации: в языке же Object Pascal есть соответствующая конструкция. В Delphi пользователь вашего объекта может быть полностью отгорожен от полей при помощи свойств.

Обычно свойство определяется тремя элементами: полем и двумя методами, которые осуществляют его чтение/запись:

TAnObject = class(TObject)

function GetAProperty: TSomeType;

procedure SetAProperty(ANewValue: TSomeType);

property AProperty: TSomeType read GetAProperty write SetAProperty;

end;

В данном примере доступ к значению свойства AProperty осуществляется через вызовы методов GetAProperty и SetAProperty. Однако в обращении к этим методам в явном виде нет необходимости: достаточно написать

AnObject.AProperty := AValue;

АVariable := AnObject.AProperty;

и компилятор оттранслирует эти операторы в вызовы методов. То есть внешне свойство выглядит в точности как обычное поле, но за всяким обращением к нему могут стоять нужные вам действия. Например, если у вас есть объект, представляющий собой квадрат на экране, и его свойству "цвет" вы присваиваете значение "белый", то произойдет немедленная перерисовка, приводящая реальный цвет на экране в соответствие значению свойства.

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

TPropObject = class(TObject) FValue: TSomeType;

procedure DoSomething;

function Correct(AValue: Integer):boolean;

procedure SetValue(NewValue:

Integers-property AValue:

Integer read FValue write SetValue;

end;

procedure TPropObject.SetValue(NewValue: Integer);

begin

if (NewValueOFValue) and Correct (NewValue) then FValue:= NewValue;

DoSomething;

end;

В этом примере чтение значения свойства AValue означает просто чтение поля rvalue. Зато при присвоении значения внутри setvalue вызывается сразу два метода.

Если свойство должно только читаться или только записываться, в его описании может присутствовать только соответствующий метод:

type

TAnObject = class(TObject)

property AProperty: TSomeType read GetValue;

end;

В этом примере вне объекта значение свойства можно лишь прочитать; попытка присвоить свойству AProperty значение вызовет ошибку компиляции.

Для присвоения свойству значения по умолчанию используется ключевое слово default:

property Visible: boolean read FVisible write SetVisible default True;

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

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

Property APoints[Index : Integer]:TPoint read GetPoint write SetPoint;

На самом деле, среди полей класса может и не быть поля-массива. При помощи свойств вся обработка обращений к внутренним структурам класса может быть замаскирована.

Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. После ключевых слов read и write в этом случае должны стоять имена методов — использование здесь полей типа массив недопустимо. Метод, читающий значение векторного свойства, должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр того же типа и с тем же именем, что и индекс свойства:

function GetPoint(Index:Integer):TPoint;

Аналогично, метод, помещающий значения в такое свойство, должен первым параметром иметь индекс, а вторым — переменную нужного типа (которая может быть передана как по ссылке, так и по значению):

procedure SetPoint(Index:Integer; NewPoint:TPoint);

У векторных свойств есть еще одна важная особенность. Некоторые важные классы в Delphi (списки TList, наборы строк TStrings) "построены" вокруг основного векторного свойства. Один из методов такого класса дает доступ к некоторому массиву, а все остальные методы являются как бы вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано с ключевым словом default:

tyре TMyObject = class;

property Strings[Index: Integer]: string read Get write Put; default;

end;

Если у объекта есть default-свойство, то можно его не упоминать, а ставить индекс в квадратных скобках сразу после имени объекта:

var

AMyObject: TMyObject;

begin

AMyObject.Strings[1] := 'First';

{первый способ}

AMyObject[2] := 'Second'; {второй способ}

end.

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

О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в распоряжении программиста стандартных классов 100% полей недоступны и заменены базирующимися на них свойствами. Рекомендуем при разработке собственных классов придерживаться этого же правила.

Рассмотрим базовые понятия ООП на примере игральных карт. Любая карточная игра начинается с колоды карт, игроков, и игрового стола. Естественно, каждой из этих сущностей нужно поставить в соответствие собственный класс.

Начнем с класса "карта". Каждая карта должна имеет масть (suit) и вес (weight). Также ее отличительной чертой служит то, открыта ли она на данный момент или нет (назовем это свойство Known):

TCard = class(TShape) private

FSuit : TSuit ;

FWeight : TWeight;

FKnown : boolean-protected

function GetOrder:Integer;

function GetCardName: string;

procedure SetKnown(bk: boolean);

public

constructor Create(AOwner:TComponent;

S:TSuit;

W:TWeight);

property Suit: TSuit read FSuit write FSuit;

property Weight: TWeight read FWeight write FWeight;

property Order : Integer read GetOrder;

property CardName : string read GetCardName;

property Known: boolean read FKnown write SetKnown;

end;

Помимо уже указанных свойств, нам понадобится порядковый номер карты в колоде (свойство order). Оно является производным от suit и weight"; карты упорядочены следующим образом — сначала все пики, затем трефы, бубны и, наконец, червы.

Const CardsInSuit = 8;

//в большинстве игр 8 карт в масти

function TCard.GetOrder:Integer;

begin Result:=(Integer(Suit)-1)*CardsInSuit+Integer(Weight);

end;

Также нам понадобится свойство CardName — имя карты, отображаемое при ее рисовании, например, “8”:

const SuitName : array [TSuit] of string =

(' ',chr(170),chr(167),chr(168),chr(169));//эти символы соответствуют мастям в шрифте 'Symbol'

WeightName : array [TWeight] of string = ('7',"8','9','10','J','Q','К','А');// имена карт по старшинству

function TCard.GetCardName:string;

begin Result := WeightName[Weight]+SuitName[Suit];

end;

Большинство описанных свойств напрямую обращаются к соответствующим полям. Только установка свойства Known выполнена посредством процедуры SetKnown — при открытии или закрытии карты ее нужно, как минимум, перерисовать. Этот метод, как и многие другие, будет добавлен в класс TCard впоследствии.

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

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

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

TNewObject = class(TOldObject);

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

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

TMyObject = class(TObject);

TMyObject = class;

Первый вариант, хотя он и более длинный, предпочтительнее — для устранения возможных неоднозначностей. Класс TObject несет очень серьезную нагрузку и будет рассмотрен отдельно.

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

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

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

type

TIstObj = class i : Extended;

procedure SetData(AValue: Extended);

end;

T2nd0bj = class(TIstObj) i : Integer; procedure SetData(AValue: Integer);

end;

 …

procedure T1stObj.SetData;

begin

i := 1.0;

end;

procedure T2nd0bj.SetData;

begin i := 1;

inherited SetData(0.99);

end;

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

Вы много выиграете, если будете присваивать всем идентификаторам в программе осмысленные названия. В Delphi перед именами полей принято ставить символ F (от Field): FLength, FWidth, FMyFileld и т. п.

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

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

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

Как идеология наследования используется в нашем примере с игральными картами? Наряду с классом TCard, который соответствует карте, должны быть держатели таких карт. Базовым классом в этом случае является

TCardHolder. Этот держатель карт "умеет" взять, сбросить и отдать карту другому держателю. Кроме того, он отвечает за отрисовку имеющихся у него карт. От абстрактного TCardHolder порождены другие держатели — колода ), игральный стол (тр1ауТаЬ1е), снос, а также игроки (Tpiayer). уже имеет собственное имя, а также такое свойство, как число От игрока Tpiayer порожден преферансист (TPrefplayer), уже знающий ходы по правилам; от него — TNetPrefPlayer, играющий по сети. Однако все они пользуются свойствами и методами класса TCardHolder.

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

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

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

type TField = class

function GetData:string;virtual; abstracts-end;

TStringField = class(TField)

FData : string;

function GetData: strings-override;

end;

TIntegerField = class(TField) FData : Integers-function GetData: strings-override;

end;

TExtendedField = class(TField)

FData : Extended;

function GetData: strings-override;

end;

function TStringField.GetData;

begin Result := FData;

ends; function TIntegerField.GetData;

begin

Result := IntToStr (fuar.a) ;

end;

function TExtendedField.GetData;

begin

Result:- FloatToStrF(FData,ffFixed, 7,2);

end;

procedure ShowData(AField : TField);

begin

Forml.Label 1.Caption:= AField.GetData;

end;

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

Правила контроля соответствия типов (typecasting) языка Object Pascal гласят, что объекту как указателю на экземпляр объектного типа может быть присвоен адрес любого экземпляра любого из его дочерних типов. В процедуре ShowData параметр описан как TField - это значит, что в нее можно передавать объекты классов И TStringField, И TIntegerField, И TExtendedField, и любого другого потомка Класса TField.

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

Наглядный пример использования полиморфизма дает среда Delphi. В ней имеется класс TComoonent. на уровне которого сосредоточены определенные "правила" взаимодействия компонентов со средой разработки и с другими компонентами. Следуя этим правилам, можно порождать от TComponent с-зон компоненты, настраивая Delphi на решение специальных задач. Теперь, надеемся, стало более или менее ясно, какие преимущества ООП лили ему стать основным способом разработки серьезного программного обеспечения. Те, кто начинал программировать еще для Windows 3.0, наверняка помнят, сколько усилий требовалось для написания совершенно тривиального кода. Сейчас для того же самого в Delphi достаточно буквально пары щелчков мышью! На самом деле именно сложность программирования для Windows стала катализатором внедрения ООП.

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

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

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

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

type

TFirstClass = class

FMyFieldl: Integer;

FMyField2: Longint;

procedure StatMethod;

procedure VirtMethod1; virtual;

procedure VirtMethod2; virtual;

procedure DynaMethod1; dynamic;

procedure DynaMethod2; dynamic;

end;

TSecondClass = class(TMyObject)

procedure StatMethod;

procedure VirtMethodI; override;

procedure DynaMethodI; override;

end;

var

Objl: TFirstClass;

Obj2: TSecondClass;

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

Будьте внимательны: попытка перекрытия с директивой не override, a virtual или dynamic приведет на самом деле к созданию нового одноименного метода.

Более углубленное рассмотрение таблиц вы найдете в разделе "Как устроен объект изнутри " данной главы.

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

В последних версиях Delphi появилась новая, совершенно особенная разновидность методов — перегружаемые.

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

Рассмотрим немного измененный пример, иллюстрировавший статические методы:

type T1stObj = class

i : Extended;

procedure SetData(AValue: Extended);

end;

T2nd0bj = class(T1stObj) j : Integer;

procedure SetData(AValue: Integer);

end;

var T1: TIstObj;

T2 : T2nd0bj;

В этом случае попытка вызова из объекта т 2 методов

T2.SetData(1.0);

T2.SetData(1) ;

вызовет ошибку компиляции на первой из двух строк. Для компилятора внутри T2 статический метод с параметром типа extended перекрыт, и он его "не признает". Где же выход из сложившегося положения? Переименовать один из методов? Можно, но если методов не два, а, скажем, сто, моментально возникнет путаница. Сделать методы виртуальными? Нельзя, так как тип и количество параметров в одноименных виртуальных методах должны в точности совпадать. Теперь для этого существуют перегружаемые методы, объявляемые при помощи директивы overload:

type

TIstObj = class

i : Extended;

procedure SetData(AValue: Extended);overload;

end;

T2nd0bj = class(TIstObj)

j : Integers; procedure SetData(AValue: Integer); overloads

end;

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

Можно перегрузить и виртуальный метод. Надо только в этом случае добавить директиву reintroduce:

type

T1stObj = class

i : Extended;

procedure SetDatafAValue: Extended); overload; virtual-end;

T2nd0bj= class(TIstObj)

j : Integers; procedure SetData(AValue: Integer); reintroduce; overload;

end;

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

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

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

procedure NeverCalIMe; virtual; abstract;

При этом никакого кода для этого метода писать не нужно. Вызов метода NeverCalIMe. Приведет к созданию исключительной ситуации EabstractError

(исключительным ситуациям посвящена глава 4).

Пример с классом TField из раздела "Полиморфизм. Виртуальные и динамические методы" данной главы поясняет, для чего нужно использование абстрактных методов. В данном случае класс TField не используется сам по себе; его основное предназначение — быть родоначальником иерархии конкретных классов-"полей" и дать возможность абстрагироваться от частностей. Хотя параметр процедуры showData и описан как TField, но, если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.

События и делегирование

Программистам, давно работающим в Windows, наверное, не нужно пояснять смысл термина "событие". Сама эта среда и написанные для нее программы управляются событиями, возникающими в результате воздействий пользователя, аппаратуры компьютера или других программ. Весточка о наступлении события — это сообщение Windows, полученное так называемой функцией окна. Таких сообщений сотни, и, по большому счету, написать программу для Windows — значит, определить и описать реакцию на некоторые из них.

Работать с таким количеством сообщении, даже имея под рукой справочник. нелегко. Поэтому одним из больших преимуществ Delphi является то, что программист избавлен от необходимости работать с сообщениями Windows (хотя такая возможность у него есть; об этом — в следующем разделе. Типовых событий в Delphi — не более двух десятков, и все они имеют простую интерпретацию, не требующую глубоких знаний среды.

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

property OnMyEvent: TMyEvent read FOnMyEvent write FOnMyEvent;

Здесь FOnMyEvent — поле объекта, содержащее адрес некоторого метода. Присвоить такому свойству значение — значит указать объекту адрес метода. который будет вызываться в момент наступления события. Такие методы назовем обработчиками событий. Например, запись:

Application.OnActivate := MyActivatingMethod;

означает, что при активизации объекта Application (так называется объект, соответствующий работающему приложению) будет вызван метод-обработчик MyActivatingMethod.

Внутри библиотеки времени выполнения Delphi вызовы обработчиков событий находятся в методах, обрабатывающих сообщения Windows. Выполнив необходимые действия, этот метод проверяет, известен ли адрес обработчика, и, если это так, вызывает его:

if Assigned(FOnMyEvent) then FOnMyEvent(Self);

События имеют разное количество и тип параметров в зависимости от происхождения и предназначения. Общим для всех является параметр sender — он указывает на объект-источник события. Самый простой тип — TNotifyEvent - не имеет других параметров:

TNotifyEvent = procedure (Sender: TObject) of object;

Тип метода, предназначенный для извещения о нажатии клавиши, предусматривает передачу программисту кода этой клавиши, о передвижении мыши — ее текущих координат и т. п. Все события в Delphi принято предварять префиксом on: oncreate, OnMouseMove, Onpaint и так далее. Щелкнув в Инспекторе объектов на странице Events в поле любого события, вы получите в программе заготовку метода нужного типа. При этом его имя будет состоять из имени текущего компонента и имени события (без префикса On), а относиться он будет к текущей форме. Пусть, например, на форме Formi есть текст Label1. Тогда для обработки щелчка мышью (событие OnClick) будет создана заготовка метода TFormi.

Labeliciick:

procedure Tform1.Label1Click(Sender: TObject);

begin

end;

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

Object1.OnMouseMove := Object2.OnMouseMove;

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

Можно при необходимости выбирать один из нескольких возможных вариантов обработчиков событий. В предлагаемом примере мышь задействуется для трех совершенно разных операций: поэтому в зависимости от выбора пользователя для обработки событий от нее задействуется одна из трех групп методов. Большинство компонентов отслеживает три основных события, связанных с мышью — нажатие кнопки (onMouseDown), ее отпускание (onMouseUp) и перемещение мыши (onMouseMove).

В Delphi 5 имеются также события onMouseWheel, OnMouseWheeiup, OnMouseWheelDown. Они предназначены для обработки событий от колесика, имеющегося у таких мышей, как Microsoft IntelliMouse или Genius NetMouse Pro.

Рассмотрим обработку этих событий применительно к изображению (компонент Timage) для его ручной прокрутки (группа методов, имена которых начинаются со слова scroll), измерения длин (Measure) и выделения прямоугольников (select). Таким образом, методов всего девять. Переключение между ними происходит при нажатии соответствующих кнопок (еще три метода). Текущее состояние мыши хранится в переменной FMouseState.

type TMouseState = (msNormal, msDragging, msMeasuring, msSelecting) ;

var

FMouseState : TMouseState;

OldPos, NewPos : TPoint;

MaxShift : TPoint;

procedure TForml.ScrollMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integers;

begin

with Imagel do

begin MaxShift.X := Parent.Width - Width;

MaxShift.Y := Parent.Height - Height;

end;

if (MaxShift.X > 0) and (MaxShift.Y > 0) then Exit;

if MaxShift.X > 0 then MaxShift.X := 0;

if MaxShift.Y > 0 then MaxShift.Y := 0;

FMouseState := msDragging;

OldPos := Point(X, Y);

Screen.Cursor := crHandPoint;

end;

procedure TForml.ScrollMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);

begin

if FMouseState = msDragging then with Sender as Timage do

begin

NewPos := Point(X - OldPos.X, Y - OldPos.Y);

if Left + NewPos.X > 0 then NewPos.X := - Left;

if Left + NewPos.X < MaxShift.X then NewPos.X := MaxShift.X - Left;

if Top + NewPos.Y > 0 then NewPos.Y := — Top;

if Top + NewPos.Y < MaxShift.Y then NewPos.Y := MaxShift.Y - Top;

Parent.ScrollBy(NewPos.X, NewPos.Y) ;

end;

end;

procedure TForml.ScrollMouseUp(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin FMouseState := msNormal;

Screen.Cursor := crDefault;

with Sender as Timage do Labell.Caption := Format('(%d , %d)',[-Left, -Top]) ;

end;

procedure TForml.MeasureMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integer);

begin

FMouseState := msMeasuring;

OldPos := Point(X, Y);

NewPos := OldPos;

Imagel.Canvas.Pen.Mode := pnNotXor;

Imagel.Canvas.Pen.Color := cIRed;

Imagel.Canvas .MoveTo (X, Y);

Screen.Cursor := crCross;

end;

procedure TFormI.MeasureMouseMove(Sender: TObject;

Shift: TShiftState; X, Y: Integer);

begin

if FMouseState = msMeasuring then begin Imagel. Canvas. Poly-Line ([OldPos, NewPos]);

NewPos := Point(X, Y);

Iinagel.Canvas.PolyLine([OldPos, NewPos]);

Labell.Caption := Format('%7.2f,

[Sqrt(Sqr(Longint(X - OldPos-X))

+Sqr(Longint(Y - OldPos. Y) ) ) ]);

end;

end;

procedure TFormI.MeasureMouseUp(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integers); begin

Imagel.Canvas.PolyLine([OldPos, NewPos]) ;

Screen.Cursor := crDefault;

FMouseState := msNormal;

end;

procedure TFormI.SelectMouseDown(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y: Integers); begin FMouseState := msSelecting;

OldPos := Point(X, Y);

NewPos := OldPos;

with Imagel. Canvas do

begin

Pen.Mode := pmNotXor;

Pen. Color := crGreen;

MoveTo(X,Y) ;

end;

Screen. Cursor := crCross;

procedure TPormI.SelectMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);

begin

if FMouseState = msSelecting then begin

Imagel.Canvas.Rectangle(OldPos.X, OldPos.Y, NewPos.X, NewPos.Y);

NewPos := Point(X, Y) ;

Imagel.Canvas.Rectangle(OldPos.X, OldPos.Y, NewPos.X, NewPos.Y);

Labell.Caption := Format('%d x %d',[X - OldPos.X, Y - OldPos.Y]);

end;

end;

procedure TFormI.SelectMouseUp(Sender: TObject; Button: TMouseButton;

Shift: TShiftState; X, Y; Integers-begin

Imagel.Canvas.Rectangle(OldPos.X,

OldPos.Y, NewPos.X, NewPos.Y);

Screen.Cursor := crDefault;

FMouseState := msNormal;

end;

procedure TFormI.SelectButtonClick

(Sender: TObject);

begin with Imagel do begin

OnMouseDown := SelectMouseDown;

OnMouseMove := SelectMouseMove;

OnMouseUp := SelectMouseUp;

Cursor := crCross;

end;

end;

procedure TFoml.

MeasureButtonClick( Sender: TObject);

begin with Imagel do

begin

OnMouseDown := MeasureMouseDown;

OnMouseMove := MeasureMouseMove;

OnMouseUp := MeasureMouseUp;

Cursor := crCross;

end;

end;

procedure TFormI.ScrollButtonClick(Sender: TObject);

begin with Image 1 do begin OnMouseDown := ScrollMouseDown;

OnMouseMove := ScrollMouseMove;

OnMouseUp := ScrollMouseUp;

Cursor := crDefault;

end;

end;

Но какой механизм позволяет подменять обработчики, ведь это не просто процедуры, а методы? Здесь как нельзя кстати приходится существующее в Object Pascal понятие указателя на метод. Отличие метода от обычной процедуры состоит в том, что помимо явно описанных параметров методу всегда неявно передается еще и указатель на вызвавший его экземпляр (переменная Self). Вы можете описать процедурный тип, который будет совместим по присваиванию с методом (то есть предусматривать получение self). Для этого в описание процедуры нужно добавить зарезервированные слова of object. Указатель на метод — это указатель на такую процедуру.

type TMyEvent = procedure(Sender:

TObject; var AValue: Integer) of object;

TIstObject = class;

FOnMyEvent: TMyEvent;

property OnMyEvent: TMyEvent read

FOnMyEvent write FOnMyEvent;

end;

T2nd0bject = class;

procedure SetValuel(Sender: TObject; var AValue: Integers); procedure SetValue2(Sender: TObject; var AValue: Integer);

end;

var

Obj1: TIstObject;

Obj2: T2nd0bject;

begin

Obj1 := TIstObject.Create ;

Obj2 := T2nd0bject.Create;

Obj1.OnMyEvent := Obj2.SetValuel;

Obj2.OnMyEvent := Obj2.SetValue2;

...

end.

Этот пример показывает, что при делегировании можно присваивать методы других классов. Здесь обработчиком события onMyEvent объекта objl по очереди выступают методы setvaluel и SetValue2 объекта obj2.

Однако здесь имеется один подводный камень. Даже подмена в предыдущем примере трех событии от мыши тремя группами обработчиков породила девять методов, занявших не одну страницу текста. Эти методы относятся к главной форме (объекту TFormi), которая и без того бывает загромождена большим количеством обработчиков других событий. Возникает естественное желание обособить эти девять методов, выделить их. К сожалению, их нельзя сделать просто процедурами — обработчики событий обязательно должны быть чьими-то методами. Но их можно "отдать" какому-либо другому объекту. Более того, можно описать и создать специальный объект (назовем его, к примеру, TMouseEventCarrier). Его единственное предназначение — быть носителем методов, которые затем делегируются другим объектам. Разумеется, такой объект надо не забыть создать до использования его методов, а в конце — уничтожить. Можно и не делать этого, объявив методы методами класса, о которых — в одном из последующих разделов.

Мы сейчас решили задачу использования нескольких разных обработчиков того или иного события для одного объекта. Но не менее часто требуется решить обратную задачу — а как использовать для разных событий разных объектов один и тот же обработчик?

Если никакой "персонификации" объекта, вызвавшего метод, не нужно, все делается тривиально и никакой проблемы не возникает. Самый простой при- , мер: в современных программах основные функции дублируются дважды — в меню и в панели инструментов. Естественно, сначала нужно создать и наполнить метод содержимым (скажем, для пункта меню), а затем в Инспекторе объектов указать его же для кнопки панели инструментов.

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

If Sender is TMenuItem then ShowMessage ('Выбран пункт меню');

Если же все объекты, разделяющие между собой один обработчик события, относятся к одному классу, то приходится прибегать к дополнительным ухищрениям. Типовой прием — использовать свойство Tag, которое имеется у всех компонентов, и, вполне вероятно, именно для этого и задумывалось:

const colors : array[0..7] of TColor = (clWhite,cIRed,clBlue,clYellow,clAqua,

cIGreen,cIMaroon,clBlack) ;

procedure TForml.CheckBoxClick(Sender: TObject);

begin

with (Sender as TCheckBox) do if Checked then Color := Colors[Tag] else Color := clBtnFace;

end;

Пусть на форме имеется несколько переключателей. Для того чтобы при нажатии каждый из них окрашивался в свой цвет, нужно в Инспекторе объектов для каждого присвоить свойству Tag значения от 0 до 7 и связать событие OnClick с методом checkBoxClick. Этот единственный способ справится с задачей для всех переключателей.

Обработка сообщений Windows

Можно сказать, что события и сообщения Windows находятся в знаменитой пропорции "80:20". В данном случае это означает, что доступные в компонентах Delphi события соответствуют 20% наиболее важных сообщений, которые покрывают 80% типовых потребностей программиста. Но есть еще и остальные 80% сообщений...

Методы, предназначенные специально для обработки сообщений Windows, составляют подмножество динамических методов и объявляются директивой message, за которой следует индекс — идентификатор сообщения. Динамические методы были введены в язык Object Pascal именно тогда, когда встала проблема создания объектов, соответствующих элементам интерфейса Windows. Когда каждый из большой иерархии компонентов содержит обработчики десятков разнообразных сообщений (посмотрите ради интереса на исходный код классов TControl и TWinControl в модуле classes . pas), разумное сочетание скорости и компактности обеспечивают только динамические методы.

Идентификатор, стоящий после директивы message, может быть числом в диапазоне от 1 до 49151 или константой вида wm_xxx, скажем, wm_mousemove.

Методы должны быть обязательно описаны как процедуры, имеющие один var-параметр. Этот параметр соответствует структуре, содержащей сообще-Windows. Однако он может быть описан произвольно, например:

type

TMyControl = class(TWinControl)

procedure WMSize(var Message: TWMSize); message WM_SIZE;

end;

TOtherMyControl = class(TMyControl)

procedure Resize(var Info); message WM SIZE;

end;

Нет необходимости изобретать собственные структуры, по-своему интерпретирующие содержание того или иного сообщения. Для большинства сообщений Windows структуры уже описаны в модуле messages. раs. Например, тип TMessage и приведенный в примере тип TWMSize выглядят так:

TMessage = record

Msg: Cardinals-case

Integer of 0: (

WParam: Longint;

LParam: Longint;

Result: Longint) ;

1: (

WParainLo Word;

WParamHi Word;

LParamLo Word;

LParamHi Word;

ResultLo Word;

ResultHi Word);

end;

TWMSize = record Msg: Cardinal;

SizeType: Longint;

( SIZE_MAXIMIZED, SIZE_MINIMIZED, SIZE_RESTORED,

SIZE_MAXHIDE, SIZE_MAXSHOW } Width: Word;

Height: Word;

Result: Longint;

end;

Для перекрытия методов-обработчиков сообщений директива override не используется, в этом случае нужно сохранить директиву message с индексом метода. Именно индекс является главным "якорем", привязывающим метод к определенному сообщению. В данном примере метод Resize перекрывает WMSize, хотя у них разные имена и даже разный формальный тип параметра. Важно только то, что они оба привязаны к сообщению wm_size.

В обработчиках сообщений можно вызвать метод-предок, просто указав ключевое слово inherited, без указания его имени и преобразования типа параметров: предок будет найден по индексу. Следует напомнить, что система обработки сообщений встроена в Object Pascal на уровне модели объектов, и самый общий обработчик — метод DefaultHandler — описан в классе TObject. Чтобы выполнить все необходимые действия при обработке сообщения, за редким исключением настоятельно рекомендуется вызвать предыдущий обработчик:

procedure MyMessageHandler(var Message);

message wm_command;

begin

MyProcessing;

inherited;

end;

Delphi предоставляет доступ и к остальным составным частям системы обработки сообщений. В "традиционных" программах, использующих только Windows API, главной из них является функция окна WndProc, куда собственно и поступают все входящие сообщения (такие примеры поставлялись еще с Borland Pascal for Windows). Сейчас же последовательность обработки напоминает известную историю "бабка за дедку, дедка за репку...". Итак:

  1. Через функцию setWindowLong системе сообщается адрес функции окна. По умолчанию эта процедура MainWndProc. При желании можно сообщить и другой адрес, однако это требует определенной квалификации; во всяком случае, не следует этого делать раньше, чем вы подробно ознакомитесь с тем, как это сделано в Delphi.
  2. Сообщение поступает в метод MainWndProc. Он не виртуальный, и "вклиниться" здесь тяжело.
  3. Метод MainWndProc вызывает метод, адрес которого содержится в свойстве windowproc. По умолчанию здесь содержится адрес виртуального метода WndProc. Тут открываются две возможности: а) переопределить значение свойства windowproc, присвоив ему адрес собственной процедуры обработки и б) перекрыть виртуальный метод WndProc.
  4. Для разных компонентов внутри WndProc может быть предусмотрена обработка тех или иных сообщений. В противном случае вызывается виртуальный метод TObject. Dispatch; именно он реализует поиск необходимого динамического метода-обработчика по индексу сообщения. Здесь тоже открываются две возможности: перекрыть Dispatch и написать свой метод-обработчик сообщения. Первая возможность совсем гипотетическая; вторая же, наоборот, самая реальная.
  5. Если метод Dispatch не нашел обработчика для данного индекса сообщения, вызывается виртуальный метод Default Handler. Если и ему "нечего сказать" по поводу данного сообщения, происходит обращение к свойству DefWndProc и вызывается метод, на который оно указывает. Этот последний метод "предоставляет последнее слово" стандартному обработчику Windows.

В зависимости от обстоятельств, нужные вам действия можно предусмотреть на любом из уровней. В 90% случаев достаточно написать динамический метод-обработчик:

TForrol = class(TForm)

procedure FormDblClick(Sender: TObject);

private

procedure WMLButtonDblClick(var Msg:TWMLButtonDblClk);

message WM_LBUTTONDBLCLK ;

end;

procedure TForml.WMLButtonDblClickfvar Msg:TWMLButtonDblClk);

begin

ShowMessage(Format('Click at %d,%d',[Msg.XPos,Msg.YPos]));

end;

procedure TFormI.FormDblClick(Sender: TObj ect) ;

begin

ShowMessage('Dbl Click') ;

end;

В данном случае реакция на двойной щелчок левой кнопкой мыши будет перехвачена нашим методом; в ответ на него будет выведено сообщение с указанием координат случившегося события. Обратите внимание, что в обработчике отсутствует оператор inherited, который вызвал бы унаследованную обработку. Это означает, что имеющийся в нашей форме обработчик события Delphi OnDblcik получает управление только по двойному щелчку правой кнопкой мыши, но — не левой. Стоит добавить оператор inherited — и все встанет на свои места.

Теперь от этого примера плавно перейдем к более сложному — нашим картам. Игральная карта порождена от класса TShape, порожденному от TGraphicControi. У этих классов есть особенность — поступившие в них сообщения от мыши они отдают системным обработчикам перетаскивания (drag-and-drop), и те интерпретируют их по-своему. Поэтому если мы слепо повторим предыдущий опыт и перенесем в TCard метод WMLButtonDblclick, то он никогда не получит управления. Это как раз тот случай, когда нужно переопределять метод WndProc:

TCard = class(TShape) procedure WndProc(var Message: TMessage); override;

end;

procedure TCard.WndProc(var Message: TMessage);

begin

if Message.Msg = WM_LBUTTONDBLCLK then

Dispatch(Message) else

inherited;

end;

Этот короткий метод перехватывает только сообщение WM_LButtonDbiclick и отправляет его на обработку самому себе; все остальные сообщения идут по обычному пути.

В принципе, если хорошо знать VCL (Visual Component Library), а также, кто и как реагирует на сообщения Windows, то можно не вызывать методы компонентов, а посылать и принимать от них сообщения. Это, конечно, малоценный вариант с практической точки зрения — ведь все сообщения ставятся в системные очереди Windows и извлекаются оттуда. Это замедляет работу. Чтобы напрямую обратиться к обработчику сообщения, во всех элементах управления предусмотрен метод perform. Он напрямую передает сообщение в функцию окна данного компонента.

В качестве иллюстрации приведем полезный фрагмент кода. Часто бывает необходимо узнать текущую позицию курсора внутри компонента тмето. Никаких иных способов, кроме посылки сообщений, для этого нет:

procedure TForml.ButtonlClick(Sender: TObject);

var CharPos : TPoint;

begin with CharPos do

begin

Y:=Meniol.Perform(EM_LINEFROMCHAR, Memol.SelStart, 0);

X:=Memol.Perform(EM_LINEINDEX, Y,0);

inc(Y);

X:=Memol.SelStart-X+1;

ShowMessage(Format('%d:%d',[x,y]));

end;

end;

Сообщения em_linefromchar и em_lineindex — специализированные, обрабатываемые только текстовыми элементами управления. Вообще, компонент TMemo — это только оболочка, методы которой представляют собой посылку и получение сообщений от соответствующего элемента управления, имеющегося в составе стандартных библиотек Windows. О таких компонентах рассказано в главе 11.

Области видимости

При описании нового класса важен разумный компромисс. С одной стороны, требуется скрыть от других методы и поля, представляющие собой внутреннее устройство класса (для этого и придуманы свойства). Маловажные детали на уровне пользователя объекта будут бесполезны и только помешают целостности восприятия.

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

В модели объектов языка ObjectPascal существует механизм доступа к составным частям объекта, определяющий области, где ими можно пользоваться (области видимости). Поля и методы могут относиться к пяти группам (секциям), отличающимся областями видимости. Методы и свойства могут быть общими (public), личными (private), защищенными (protected) и опубликованными (published); пятая группа, automated, используется для создания объектов автоматизации и будет рассмотрена в главах, посвященных интерфейсам сом.

Области видимости, определяемые первыми тремя директивами, таковы:

Рассмотрим пример, иллюстрирующий три варианта областей видимости:

unit first;

interface

type

TFirstObj = class

private

procedure Method1;

protected

procedure Method2;

public

procedure Method3;

end;

procedure TestProcI;

implementation

uses dialogs;

var AFirstObj: TFirstObj ;

procedure TestProcI;

begin AFirstObj := TFirstObj.Create;

AFirstObj.Methodi; {допустимо} AFirstObj.Method2; {допустимо} AFirstObj.Method3; {допустимо} AFirstObj.Free;

end;

procedure TFirstObj.Methodi;

begin

ShowMessage('1');

end;

procedure TFirstObj.Method2;

begin

ShowMessage('2');

 

unit Second;

interface uses First;

type

TSecondObj = class(TFirstObj procedure Method4;

end;

procedure TestProc2;

implementation

var AFirstObj : TFirstObj ;

ASecondObj: TSecondObj ;

procedure TSecondObj.Method4;

begin

Methodi; {недопустимо -произойдет ошибка компиляции)

Method2; {допустимо} Method3; (допустимо} end;

procedure TestProc2;

begin AFirstObj := TFirstObj.Create;

AFirstObj.Methodi; {недопустимо} AFirstObj.Method2; {недопустимо} AFirstObj.Method3; {допустимо} AFirstObj.Free;

ASecondObj := TSecondObj.Create;

ASecondObj.Methodi; {недопустимо} ASecondObj.Method2; {допустимо}

Method1 ;

end;

procedure TFirstObj.Method3;

begin

ShowMessage('3');

Method2;

end;

end.

ASecondObj.Method3; {допустимо} ASecondObj.Free;

end;

end.

 

Если к этому примеру добавить модуль Third и попробовать вызвать методы классов TFirstObj и TSecondObj оттуда, то к числу недоступных будет отнесен и Method2 — он доступен только в том модуле, в котором описан.

Наконец, область видимости, определяемая четвертой директивой — published, имеет особое значение для интерфейса визуального проектирования Delphi. В этой секции должны быть собраны те свойства объекта, которые будут видны не только во время исполнения приложения, но и из среды разработки. Публиковать можно свойства большинства типов, за исключением старого типа real (теперь он называется real48), свойств типа "массив" и некоторых других. Все свойства компонентов, доступные через Инспектор объектов, являются их опубликованными свойствами. Во время выполнения такие свойства общедоступны, и как public.

Три области видимости — private, protected, public — как бы упорядо-чены по возрастанию видимости методов. В классах-потомках можно повысить видимость методов и свойств, но не понизить ее. При описании дочернего класса можно переносить методы и свойства из одной сферы видимости в другую, не переписывая их заново и даже не описывая — достаточно упомянуть о нем в другом месте:

type

TFirstObj = class private

FNumber: Integer;

protected

property Number: Integer read: FNumber;

end;

TSecondObj = class(TFirstObj) published property Number;

end;

Если какое-либо свойство объекта из состава VCL принадлежит к области public, вернуть его в private невозможно. Напротив, обратная процедура широко практикуется в Delphi. У многих компонентов (например, TEdit) есть предок (в данном случае TCustomEdit), который отличается только отсутствием опубликованных свойств. Так что, если вы хотите создать новый редактирующий компонент, порождайте его на базе TCustomEdit и публикуйте только те свойства, которые считаете нужными. Разумеется, если вы поместили свойство в область private, "достать" его оттуда в потомках возможности уже нет.

Как устроен объект изнутри

Что же представляет собой объект? Ясно, что каждый экземпляр класса содержит отдельную копию всех его полей. Ясно, что где-то в его недрах есть указатели на таблицу виртуальных методов и таблицу динамических методов. А что еще там имеется? И как происходит вызов методов? Вернемся к примеру из раздела данной главы "Полиморфизм":

type

TFirstClass = class FMyFieldl: Integer;

FMyField2: Longint;

procedure StatMethod;

procedure VirtMethodI; virtual;

procedure VirtMethod2; virtual;

procedure DynaMethodI; dynamic;

procedure DynaMethod2; dynamic;

end;

TSecondClass = class(TMyObject) procedure StatMethod;

procedure VirtMethodI; override;

procedure DynaMethodI; override;

end;

Objl: TFirstClass;

Obj2: TSecondClass;

На рис. 1.1 показано, как будет выглядеть внутренняя структура рассмотренных в нем объектов.

Первое поле каждого экземпляра каждого объекта содержит указатель на его класс. Класс как структура состоит из двух частей. Начиная с адреса, на который ссылается указатель на класс, располагается таблица виртуальных методов. Напомним, что она содержит адреса всех виртуальных методов класса. включая унаследованные от предков. Длина таблиц VMT объектов obj1 и obj2 одинакова — по два элемента (8 байт). Перед таблицей виртуальных методов расположена специальная структура, содержащая дополнительную служебную информацию. В ней содержатся данные, полностью характеризующие класс: его имя, размер экземпляра, указатели на класс-предок, имя класса и т. д. На рисунке 1.1 она показана одним блоком; а ее содержимое расшифровано ниже.

Рис. 1.1. Внутренняя структура объектов Obj1 и obj2

Одно из полей структуры содержит адрес таблицы динамических методов класса (DMT). Таблица имеет следующий формат — в начале слово, содержащее количество элементов таблицы; затем — слова, соответствующие индексам методов. Нумерация индексов начинается с —1 и идет по убывающей. После индексов идут собственно адреса динамических методов. Обратите внимание, что DMT объекта objl состоит из двух элементов, obj2 — из одного, соответствующего перекрытому методу DynaMethodi. В случае вызова obj2. DynaMethod2 индекс не будет найден в таблице DMT obj2, и произойдет обращение к DMT objl. Именно так экономится память при использовании динамических методов.

Напомним, что указатель на класс указывает на первый виртуальный метод. Служебные данные размещаются перед таблицей VMT, то есть с отрицательным смещением. Эти смещения описаны в модуле system. pas:

vmtSelfPtr = -76;

vmtIntfTable = -72;

vmtAutoTable = -68;

vmtInitTable = -64;

vmtTypeInfo = -60;

vmtFieldTable = -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;

Нам уже ясен cмысл полей vmDynamicTable, vmtDispatch И vmtDefaultHandler — они отвечают за вызов динамических методов. Поля vnitNewInstance, vmtFreeInstance И vmtDestroy содержат адреса Процедур создания и уничтожения экземпляра объекта. Поля vmtIntfTable, vmtAutoTable, vmtSafeCailException введены для обеспечения совместимости с моделью объектов СОМ. Остальные же доступны через методы класса TObject: ведь в ObjectPascal информация этой таблицы играет важную роль и может использоваться программистом неявно.

В языке определены два оператора — is и as, неявно обращающиеся к ней. Оператор is предназначен для проверки совместимости по присваиванию экземпляра объекта с заданным классом. Выражение вида:

AnObject is TObjectType

принимает значение True, только если объект AnObject совместим по присваиванию с классом TObjectType, то есть является объектом этого класса или одного из классов, порожденных от него. Кстати, определенная проверка происходит еще при компиляции: если формально объект и класс несовместимы, то компилятор выдаст ошибку в этом операторе.

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

with ASomeObject as TAnotherType do ...

От стандартного способа приведения типов с помощью конструкции TAnotherType (ASomeObject) использование оператора as отличается наличием проверки на совместимость типов во время выполнения (как в операторе is): попытка приведения к несовместимому типу приводит к возникновению исключительной ситуации EinvalidCast (см. гл. 4). После применения оператора as сам объект остается неизменным, но вызываются те его методы, которые соответствуют присваиваемому классу.

Очень полезным может быть оператор as в методах-обработчиках событий. Для обеспечения совместимости в 99% случаев источник события Sender имеет тип TObject, хотя в тех же 99% случаев им является форма или другие компоненты. Поэтому, чтобы иметь возможность пользоваться их свойствами, применяют оператор as:

(Sender as TControl).Caption := "Thanks!";

Вся информация, описывающая класс, создается и размещается в памяти на этапе компиляции. Возникает резонный вопрос: а нельзя ли получить доступ к ней, не создавая экземпляр объекта? Да, можно. Доступ к информации класса вне методов этого класса можно получить, описав соответствующий указатель, который называется указателем на класс, или указателем на объектный тип (class reference). Он описывается при помощи зарезервированных слов class of. Например, указатель на класс TObject описан в модуле system. раз и называется TCiass:

type

TObject = class;

TCiass = class of TObject;

Аналогичные указатели уже описаны и для других важных классов. Вы можете использовать в своей программе TComponentClass, TControlClass И Т. П.

Указатели на классы тоже подчиняются правилам приведения объектных типов. Указатель на класс-предок может ссылаться и на любые дочерние классы; обратное невозможно:

type TFirst = class

...

end;

TSecond = class(TFirst)

...

end;

TFirstClass = class of TFirst;

TSecondClass = class of TSecond;

var

AFirst : TFirstClass;

ASecond : TSecondClass;

begin

AFirst := TSecond; {допустимо}

ASecond := TFirst; {недопустимо} end.

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

type

TMyObject = class(TObject)

class function GetSize: string;

end;

var

MyObject: TMyObject;

AString: string;

begin

AString := TMyObject.GetSize;

MyObject := TMyObject.Create;

AString := MyObject.GetSize;

end.

Разумеется, методы класса не могут использовать значения, содержащиеся в полях класса: ведь экземпляра-то не существует! Возникает вопрос: для чего нужны такие методы?

Важнейшие методы класса определены в самом TObject: они как раз и позволяют, не углубляясь во внутреннюю структуру класса, извлечь оттуда практически всю необходимую информацию (табл. 1.1).

Таблица 1.1. Методы класса TObject

Метод

Описание

class function ClassName: ShortString;

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

class function ClassNameIs(const Name: string): Boolean;

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

class function CiassParent: TClass;

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

class function Classlnfo: Pointer;

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

class function InstanceSize: Longint;

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

class function Inherits From(AClass: TClass): Boolean;

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

class function MethodAddress(const Name: ShortString): Pointer;

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

class function MethodName(Address: Pointer): ShortString;

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

 

В следующем примере переменная AMyObjRef является указателем на класс; он по очереди указывает на TObject и TMyObject (то есть на их внутренние структуры). Посредством этой переменной-указателя вызывается функция класса className, которая извлекает имя класса из его поля vmtClassName. Обратите внимание, что эту функцию не нужно переопределять для каждого порожденного класса:

type

TMyObject = class;

TMyObjClass = class of TObject;

var AMyObjRef : TMyObjClass;

s: strings; begin

AMyObjRef := TObject;

s := AMyObjRef.ClassName;

{ s := 'TObject'}

AMyObjRef := TMyObject;

s:=AMyObjRef.ClassName;

{s := "TMyObject"} end.

Начиная с версии Delphi 4, в класс TObj ect добавлены еще два виртуальных метода — Afterconstruction и BeforeDestruction. Как следует из названия, они вызываются сразу после создания экземпляра объекта и непосредственно перед его уничтожением. Их можно использовать, если по каким-либо причинам вам не хватает собственно конструктора и деструктора.

Резюме

Не зная принципов создания и функционирования объектов, нечего и думать о серьезной работе в Delphi. Материал данной главы проецируется практически на все последующие. Однако чаще всего придется вспоминать ее: