Профессиональная разработка приложений с помощью Delphi 5: часть 1

Источник: УКЦ Interface Ltd
Сергей Трепалин

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

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

Материалы, изложенные в настоящем разделе (если это особо не оговаривается) применимы как к Delphi, так и к C++Builder (за исключением синтаксиса).

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

Современное название объекта - класс ; термин «объект» иногда используется для обозначения рабочей копии (экземпляра) класса.

Класс можно вкратце определить как запись record , к которой добавлены методы для работы с переменными внутри этой записи. Однако в отличие от записей и методов, объявленных вне класса, класс обладает рядом дополнительных возможностей.

Каждому классу, за исключением самого первого, должен предшествовать класс-родитель. В свою очередь, любой класс можно использовать для создания других классов, и он в этом случае будет являться их родителем. В Delphi у класса бывает только один родитель, в C++ родителей может быть несколько. Поэтому в Delphi классы образуют иерархическое дерево с классом TObject в роли корня. Иерархию классов в Delphi можно проследить при вызове команды View/Browser после успешной компиляции какого-либо проекта.

В C++ такое дерево построить невозможно: отдельные его ветви будут сливаться.

Иерархическое дерево реализуется довольно просто: при определении в среде-разработке нового класса указывается класс-предок:

TMyListBox=class(TListBox)
end;

Разрешается не указывать класс-предок. В этом случае по умолчанию считается, что предком является TObject. Код вида

TMyClass=class
end;

эквивалентен записи

TMyClass=class(TObject)
end;

Такое объявление однозначно определяет место нового класса в иерархии классов. Кроме того, оно означает следующее: все переменные и все методы класса-предка копируются в новый класс. Простым объявлением TMyListBox=class(TListBox) мы получили новый класс, который обладает всеми свойствами списка: в него можно добавлять строки, он будет показан на форме, при необходимости на нем автоматически появится вертикальная полоса прокрутки и т.д. Таким образом, при продвижении по ветвям иерархического дерева происходит накопление переменных и методов. Например, класс TWinControl имеет все переменные и методы, определенные в классах TControl, TComponent, TPersistent и TObject.

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

Объявление переменных и методов класса

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

TMyClass=class(TObject)
public
 FName:string;
 FAge:integer;
end;

В класс TMyClass к переменным класса TObject добавлены две новые переменные: FName и FAge. Названия переменных в классе принято (но не обязательно) начинать с буквы F от слова field . Классовые переменные, определенные внутри класса, отличаются от глобальных (служебное слово var в секции interface или implementation модуля) и локальных (служебное слово var в процедуре или функции) переменных. При загрузке приложения память для глобальных переменных выделяется немедленно и освобождается по завершении приложения. Локальным же переменным память выделяется в стеке при вызове метода, и после завершения работы метода эти ресурсы возвращаются системе.Так, если была объявлена, например, одна глобальная (или локальная) переменная типа N:integer , то резервируется 4 байта памяти, куда можно поместить одно значение. При объявлении же классовых переменных во время загрузки приложения не выделяется память для их хранения - она выделяется только при создании экземпляра класса после вызова конструктора (см. ниже). Поскольку экземпляров класса может быть несколько, в работающем приложении может быть несколько копий классовых переменных (в том числе и нулевое количество). Соответственно в каждой из этих переменных могут храниться различающиеся данные. Этим и определяется отличие классовых переменных от глобальных и локальных - для последних имеется только одна копия. Еще одной интересной особенностью классовых переменных является то, что при создании экземпляра класса они инициализируются нулями (то есть все их биты заполняются нулями). Поэтому если такая переменная представляет собой указатель, то он равен nil , если целое число, то оно равно 0, если логическую переменную, то она равна False . Локальные и глобальные переменные не инициализируются.

Формально объявление метода класса похоже на объявление обычного, не относящегося к классу, метода:

TMyClass=class(TObject)
public
 FName:string;
 FAge:integer;
 procedure DoSomething(K:integer);
 function AddOne(N:integer):integer;
end;

В данном примере к методам TObject добавлены два новых метода - DoSomething и AddOne. Синтаксис Object Pascal разрешает объявлять новые методы только после объявления переменных - приведенный ниже пример вызовет ошибку компиляции:

TMyClass=class(TObject)
public
 FName:string;
 procedure DoSomething(K:integer);
 FAge:integer;
 function AddOne(N:integer):integer;
end;

После объявления какого-либо метода в классе необходимо в секции implementation данного модуля описать его реализацию. Перед заголовком метода следует поместить указание на класс, к которому он относится. Это необходимо делать, поскольку различные классы могут иметь методы с одинаковыми названиями:

…
interface
type
 TMyClass=class(TObject)
 procedure DoSomething(N:integer);
 end;
 TSecondClass=class(TObject)
 procedure DoSomething(N:integer);
 end;
…
implementation
procedure TMyClass.DoSomething(N:integer);
begin
…
end;
procedure TSecondClass.DoSomething(N:integer);
begin
…
end;

Методы, объявленные в классе

Методы, описываемые в классе, подразделяются на классовые методы и другие методы. Приведем примеры их объявления и реализации:

TMyClass=class(TObject)
 class procedure DoSomething(K:integer); 
    {Class method}
 function AddOne(N:integer):integer; {Keywords class 
    absent - means method}
end;
…
implementation
…
 class procedure TMyClass.DoSomething(K:integer);
begin
 …
end;
 function TMyClass.AddOne(N:integer):integer;
begin
…
end;

Обратите внимание, что и в секции реализации используется служебное слово class.

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

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

Для работы с методами имеется структура TMethod, определенная в модуле SysUtils:

type
 TMethod = record
 Code, Data: Pointer;
end;

Эта запись позволяет «разобрать» и вновь «собрать» метод класса на две переменные типа Pointer, что бывает полезным для передачи ссылки на метод во внутренний ( in-process ) сервер автоматизации.

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

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

TMyClass=class(TObject)
 function AddOne(N:integer):integer; {Keywords absent 
    - means
 static method}
 procedure Rotate(Angle:single); virtual; {Virtual method}
 procedure Move(Distance:single); dynamic; {Dynamic 
    method}
end;

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

В классическом объектно-ориентированном программировании требуется наличие виртуальных методов. Динамические методы поддерживают не все объектно-ориентированные языки, но их использование является достаточно эффективным. Для понимания различий между ними рассмотрим создание главной формы какого-либо приложения. При этом разберемся, как в памяти компьютера распределяются три метода формы: DoEnter (динамический метод), CreateWnd (виртуальный) и DoKeyDown (статический). Каждый из этих методов определен на уровне TWinControl.

Иерархия классов, которая ведет от TWinControl к TForm1, следующая:

TWinControl -> TScrollingWinControl -> TCustomForm -> TForm -> 
    TForm1

При этом в памяти компьютера будет размещена единственная копия статического метода DoKeyDown. При создании экземпляра любого из вышеперечисленных классов и вызове из него метода DoKeyDown компилятор генерирует код, который ссылается на адрес этого метода. Таким образом, один экземпляр статического метода обслуживает не только несколько экземпляров класса, в котором он определен, но и все экземпляры его потомков. При этом следует обратить внимание на то, что вызов метода происходит очень быстро: в коде указывается его адрес в ОЗУ.

При динамическом методе DoEnter в памяти компьютера создается единственная его копия, а в таблице динамических методов TWinControl указывается его адрес. В таблицах динамических методов классов TScrollingWinControl…TForm1 в качестве адреса этого метода указывается nil . При вызове этого метода из экземпляра класса TForm1 первоначально происходит поиск этого метода в таблице динамических методов TForm1. Естественно, метод найден не будет, и поиск продолжится уже в таблице динамических методов класса TForm. Так будет продолжаться до тех пор, пока не начнется поиск в таблице динамических методов TWinControl, где будет найден его адрес, по которому будет передано управление процессом. Как и статические, динамические методы требуют мало ресурсов: один экземпляр динамического метода обслуживает как экземпляры класса, где он определен, так и экземпляры всех его потомков. Но вызов метода происходит достаточно долго, поскольку для этого приходится просматривать несколько таблиц. Вызов может замедляться еще и при использовании директивы компилятора {$R+} (проверка диапазона допустимых значений).

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

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

Переписанные методы. Полиморфизм. Абстрактные методы

Методы классов могут вызываться как непосредственно программистом, так и автоматически в ответ на какие-либо события. Предположим, перед вызовом метода необходимо выполнить какое-либо действие. Если метод вызывается в явном виде из кода программы, то программист может перед вызовом метода дописать необходимый код. Однако достаточно часто бывает необходимо выполнить действия перед автоматическим вызовом классом какого-либо метода в ответ на какое-либо событие (или после вызова метода). Например, класс TEdit (однострочный редактор текста) позволяет редактировать текст. При этом, например, программист хочет, чтобы пользователь не мог покинуть данный элемент управления, пока в нем не будет введено корректное значение какого-либо текста (например, целое число). Но покинуть данный элемент управления и перейти к другому пользователь может несколькими способами - щелкнуть мышкой на другом элементе, нажать акселератор или клавишу Tab. Если бы программист никак не мог изменить имеющиеся методы, он вынужден был бы перехватывать события о нажатой клавише мыши на всех имеющихся на форме элементах управления, проверять содержимое целевого элемента и при необходимости возвращать ему фокус ввода. Такие же проверки необходимо было бы сделать для обработчика событий, связанных с нажатием на клавиши…

К счастью, в классах виртуальные и динамические (но не статические!) методы можно подменить на другие, созданные программистом. При этом, если данный метод вызывается автоматически, будет выполняться уже новый метод, написанный программистом. Такая подмена осуществляется при использовании служебного слова override . В данном случае (проверка содержимого редактора перед выходом из него) решение будет заключаться в следующем. Любой объект класса TWinControl (и TEdit) вызывает метод DoExit перед тем, как он теряет фокус ввода. Этот метод является динамическим, и его можно переписать:

TMyEdit=class(TEdit)
 protected
 procedure DoExit; override;
 end;
…
implementation
…
procedure TMyEdit.DoExit;
var
 N,I:integer;
begin
 inherited DoExit;
 Val(Text,N,I);
 if I<>0 then begin
 MessageDlg(‘Illegal value’,mtError,[mbOK],0);
 SetFocus;
 end;
end;

Теперь, создавая копию данного класса в обработчике события OnCreate главной формы

procedure TForm1.FormCreate(Sender: TObject);
begin
 with TMyEdit.Create(Self) do begin
 Parent:=Self;
 Left:=200;
 Top:=100;
 end;
end;

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

Таким образом, переписывание виртуального или динамического метода осуществляется следующим образом. В классе-потомке определяется метод с тем же самым названием и с тем же самым списком параметров, который был ранее объявлен в каком-либо из классов-предков данного класса. При этом не имеет значения, как называются формальные параметры переписываемого метода, значение имеет их порядок следования, модификаторы (var, const) и их тип. Например, на уровне TWinControl определен виртуальный метод: procedure AlignControls(AControl: TControl; var Rect: TRect); При его переписывании в каком-либо классе-потомке данный метод можно определить следующим образом:

procedure AlignControls(AC: TControl; var R: TRect); override;

Однако компилятор Delphi не пропустит перечисленные ниже определения:

procedure AControls(AControl: TControl; var Rect: TRect); override; {Name 
    is not consistent}
procedure AlignControls(AControl: TControl; Rect: TRect); override; {var 
    missed}
procedure AlignControls(var Rect: TRect); override; {Parameter list differs}

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

Итак, рассмотрим еще раз, что происходит в классе TEdit и его потомке TMyEdit. Класс TEdit устроен таким образом, что перед тем, как он теряет фокус ввода, приложение обращается к таблице динамических методов TEdit и извлекает оттуда адрес 27-го метода (метод DoExit среди динамических определен 27-м по счету). После этого управление процессом передается по найденному адресу. Класс-потомок TMyEdit имеет собственные таблицы виртуальных и динамических методов. Таблица динамических методов отличается тем, что в нем 27-й адрес уже указывает на реализованный нами метод DoExit. Соответственно приложением извлекается уже новый адрес, и управление процессом передается вновь реализованному методу. При его старте происходит обращение к таблице динамических методов класса TEdit и вызывается 27-й метод - это делает строка inherited DoExit . Затем проверяется содержимое свойства Text в экземпляре класса TMyEdit, и если это не целое число, то об этом сообщается пользователю и фокус ввода вновь переносится на редактор текста. Схематически это можно изобразить на рисунке следующим образом.

При забывании директивы override появляется новый, статический метод с тем же самым названием и списком параметров, что и ранее определенный виртуальный (или динамический) метод. При этом новый метод делает «невидимым» старый, то есть при явном вызове метода из кода приложения будет вызываться новый, определенный программистом метод. Если метод вызывается классом в ответ на какое-либо событие, по-прежнему будет вызываться старый метод (адрес в таблицах не изменится). Такое поведение объекта часто приводит к печальным последствиям, которые иногда очень трудно обнаруживаются.

С переписываемыми методами тесно связано и другое понятие для классов - полиморфизм , суть которого заключается в том, что родительский класс имеет какой-либо виртуальный или динамический метод, а в различных его потомках этот метод переписывается по-разному. После этого выполняя формально одни и те же методы, можно добиться принципиально разного результата . Хороший пример полиморфных классов - класс TStream (базовый) и три его потомка - TFileStream, TMemoryStream и TResourceStream. Эти классы используются для хранения и передачи данных в двоичном формате. В базовом классе TStream определено несколько абстрактных методов, например:

function Write(const Buffer; Count: Longint): Longint; virtual;

а в классах-потомках этот метод переписан таким образом, что записывает данные из переменной Buffer в файл (TFileStream), или в ОЗУ (TMemoryStream), или в ресурсы (TResourceStream). Программист при реализации приложения может определить метод для сохранения своих данных в двоичном формате:

procedure SaveToStream(Stream:TStream);
begin
 Stream.Write(FYear,sizeof(FYear));
 Stream.Write(FMonth,sizeof(FMonth));
 {... A very long code to save all data as binary may 
    be inserted here …}
end;

Далее, вызывая этот метод и используя разные классы-потомки, можно сохранить данные либо в памяти компьютера, либо в файле:

procedure TForm1.Button1Click(Sender: TObject);
var
 MStream:TMemoryStream;
 FStream:TFileStream;
begin
 MStream:=TMemoryStream.Create;
 SaveToStream(MStream); {Store data in memory}
 DoSomething(MStream); {Manipulation with memory data}
 MStream.Free;
 FStream:=TFileStream.Create('C:\Test.dat',fmCreate);
 SaveToStream(FStream); {Store data in file}
 FStream.Free;
end;

В классах, являющихся базовыми для полиморфизма, часто используют абстрактные методы. Они объявляются со служебным словом abstract :

TMyClass=class(TObject)
protected
 procedure DisplayGraphic(Canvas:TCanvas); virtual; 
    abstract;
end;

При объявлении метода абстрактным реализация этих методов не требуется (более того - не допускается компилятором). В секции implementation для метода DisplayGraphic абсолютно ничего писать не надо. Для абстрактного метода в виртуальной (или динамической) таблице методов резервируется запись, куда помещается адрес nil . В классах-потомках на место этого адреса подставляются реальные адреса.

При попытке вызвать абстрактный метод возникает исключение - метод-то отсутствует! В частности, для вышеприведенного примера метод Write класса TStream является абстрактным и при попытке вызвать метод SaveToStream

procedure TForm1.Button1Click(Sender: TObject);
var
 Stream:TStream;
begin
 Stream:=TStream.Create;
 SaveToStream(Stream); {Store data in ???}
 Stream.Free;
 end;

возникает ошибка:

Классы, содержащие абстрактные методы, называют абстрактными; они являются базовыми для создания классов-потомков. В любом случае не следует создавать экземпляры абстрактных классов в приложении! Компилятор Delphi тем не менее позволяет осуществлять вызов конструкторов абстрактных классов с соответствующим предупреждением.

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

Так же как и для обычных методов, в методах класса допустима директива overload - перегружаемый метод:

TOver1Class=class(TObject)
public
 procedure DoSmth(N:integer); overload;
 class procedure DoSecond(N:integer); overload; dynamic;
end;

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

TOver2Class=class(TOver1Class)
public
procedure DoSmth(S:string); overload;
class procedure DoSecond(S:string); reintroduce; overload; dynamic;
end;

Теперь можно вызывать методы DoSmth и DoSecond из экземпляра TOver2Class с целочисленным и строковым параметром:

procedure TForm1.Button3Click(Sender: TObject);
var
 CO:TOver2Class;
begin
 CO:=TOver2Class.Create;
 CO.DoSmth(1);
 CO.DoSmth('Test');
 CO.Free;
end;

В файлах помощи Delphi рекомендуют использовать директиву reintroduce , когда вводится перегружаемый виртуальный метод, но и без этой директивы приложение прекрасно работает. Директива reintroduce просто подавляет сообщение компилятора (warning) о возникновении скрытого метода.

Создание экземпляров классов. Конструкторы и деструкторы

Переменная типа класса объявляется в приложении так же, как обычная переменная:

var
 Stream:TMemoryStream;

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

Stream:=TMemoryStream.Create;

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

procedure TForm1.Button1Click(Sender: TObject);
var
 SL1,SL2:TStringList;
begin
 SL1:=TStringList.Create;
 SL2:=TStringList.Create;
 SL1.Add('String added to SL1 object');
 SL2.Add('String added to SL2 object');
….
end;

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

procedure TForm1.Button1Click(Sender: TObject);
var
 SL:TStringList;
begin
 SL:=nil;
 try
 SL.Create; {Error. Should be 
    typed SL:=TStringList.Create;}
…
 finally
 if Assigned(SL) then SL.Free;
 end;
end;

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

Любой класс может иметь несколько конструкторов. Примером может служить класс Exception, имеющий восемь конструкторов. По соглашению имя конструктора содержит слово Create (CreateFmt, CreateFromFile…). Конструкторы могут быть как статическими, так и виртуальными или динамическими. Последние могут быть переписаны - в классах-потомках при необходимости определяется новый конструктор со служебным словом override. Переписывать конструкторы необходимо только для компонентов Delphi и для форм - во всех остальных классах их можно просто добавлять к существующим методам. Необходимость переписывания конструкторов компонентов и форм обусловлена тем, что их вызывает среда разработки. Забытая директива override в компоненте приводит к тому, что при создании формы не выполняется новый конструктор. В большинстве других классов (не потомков TComponent) конструктор вызывается в явном виде из приложения, и поэтому будет вызываться последний написанный конструктор.

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

TMyBox=class(TListBox)
private
 FData:TList;
 FNAccel:integer;
public
 constructor Create(AOwner:TComponent); override;
end;
implementation
constructor TMyBox.Create;
begin
 inherited Create(AOwner);
 FData:=TList.Create; {work copy of class creation}
 FNAccel:=5; {zero - by default, changing to five}
 Items.Add('1');
end;

Следует обратить внимание на то, что в конструкторе первым вызывается inherited -метод - конструктор класса-предка и только потом пишется код для инициализации переменных. Это обязательное условие в объектно-ориентированном программировании, которое может нарушаться только в отдельных случаях (примером такого случая является класс TThread). При таком способе записи каждый конструктор предка будет вызывать конструктор своего предка - и так до уровня конструктора класса TObject, который фактически будет первым оператором при вызове конструктора любого класса. Далее происходит выполнение кода в конструкторе класса-потомка и т.д. Для класса TMyBox при обращении к конструктору сначала происходит резервирование памяти для хранения переменных, определенных в данном классе и его предках. Затем вызывается конструктор TObject. Далее происходит обращение к конструктору TComponent, который устанавливает связь экземпляра TMyBox с его владельцем, передаваемым в параметре AOwner. Выполняется код конструктора TCustomListBox, который создает экземпляр класса TStrings и инициализирует ряд переменных. И наконец выполняются операторы, определенные в конструкторе TMyBox. Если оператор inherited поставить последним в конструкторе TMyBox, произойдет исключение при выполнении оператора Items.Add('1') - объект для хранения строк создается в конструкторе класса TCustomListBox, который еще не был вызван.

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

TMyBox=class(TListBox)
private
 FData:TList;
 FNAccel:integer;
public
 constructor Create(AOwner:TComponent); override;
 destructor Destroy; override;
end;
implementation
destructor TMyBox.Destroy;
begin
 FData.Free;
 inherited Destroy;
end;

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

procedure TForm1.Button1Click(Sender: TObject);
var
 MB:TMyBox;
begin
 MB:=TMyBox.Create(Self); {Class reference 
    TMyBox}
 {....}
 MB.Destroy; 
    {Object instance reference MB}
end;

Для этого примера должен быть создан новый деструктор, так как внутри экземпляра класса TMyBox создается экземпляр класса TList. Соответственно разрушаться они должны совместно.

При переписывании деструктора прежде всего разрушаются экземпляры классов, созданных внутри данного класса, и только после этого вызывается деструктор класса-предка inherited Destroy (отметим, что в конструкторе используется обратный порядок). При таком способе вызова в последнюю очередь будет вызван метод Destroy класса TObject, который вернет системе память, зарезервированную для хранения переменных класса. В примере с классом TMyBox первоначально будет разрушен экземпляр класса TList, ссылка на который содержится в переменной FData. После этого будет вызван деструктор класса TlistBox, в котором разрушается экземпляр класса TStrings. И наконец, будет вызван деструктор класса TObject, где будет освобождена память, зарезервированная для классовых переменных TMyBox.

Вместо прямого вызова деструктора рекомендуется вызывать метод Free , позволяющий проверить, была ли выделена память для разрушаемого экземпляра класса, и если да, то вызывать его деструктор. Использование этого метода важно еще и потому, что деструктор должен быть описан таким образом, чтобы он мог корректно разрушить частично созданный экземпляр класса. Частично созданный экземпляр класса получается в том случае, если в его конструкторе произошло исключение. При этом немедленно вызывается деструктор данного класса, и после его отработки nil-указатель возвращается на создаваемый экземпляр класса. Если, например, в конструкторе резервировалась память под какую-либо переменную (FPBuf):

constructor TMyBox.Create(AOwner:TComponent);
begin
 inherited Create(AOwner);
 FData:=TList.Create;
 GetMem(FPBuf,65500);
end;
destructor TMyBox.Destroy;
begin
 FData.Free;
 FreeMem(FPBuf);
 inherited Destroy;
end;

то исключение может произойти в конструкторе в момент вызова inherited Create или в момент вызова TList . Create - из-за нехватки системных ресурсов. Сразу же будет вызван деструктор, и в момент выполнения оператора FreeMem произойдет генерация еще одного исключения. При этом метод inherited Destroy не будет вызван, а частично созданный экземпляр TMyBox не будет разрушен. Корректная реализация деструктора выглядит так:

if FPBuf<>nil then FreeMem(FPBuf);

При этом в обязательном порядке необходимо проверить, была ли выделена освобождаемая память ранее. Такие проверки необходимо делать со всеми ресурсами, подлежащими освобождению в деструкторе. В противном случае освобождать ресурс лучше в защищенном блоке try…except…end без вызова метода raise в секции except…end . Распространение исключения из деструктора недопустимо (пользователя не должно волновать, что программист не смог корректно высвободить ресурсы!).

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

TTest=class(TObject)
private
 FData:TList;
public
 constructor Create(AData:TList);
end;
implementation
constructor TTest.Create(AData:TList);
begin
 inherited Create;
 FData:=AData;
end;

Если сам объект AData будет разрушен в той процедуре, где он создан, то переписывать деструктор класса TTest для разрушения объекта FData не требуется. Повторный вызов деструктора приводит к исключению. При этом применение метода Free не спасает, он лишь проверяет, что ссылка на экземпляр класса не указывает на nil.

В отличие от конструктора, для которого может быть определено несколько методов, деструктор бывает только один. Невозможно представить себе ситуацию, когда в классе может понадобиться дополнительный деструктор. Тем не менее компилятор Delphi позволяет это сделать - а зря… Классы с двумя деструкторами - довольно частое явление на распространяемых компонентах для Delphi третьих фирм. Причиной тому программист, забывший директиву override. Это часто приводит к тому, что ресурсы, освобождением которых занимается деструктор, не освобождаются. Во-первых, метод Free обращается к первому виртуальному методу класса - Destroy. При этом будет честно вызван деструктор класса-предка, но ресурсы, освобождение которых программист старательно описывал в деструкторе с забытой директивой override, освобождены не будут. Во-вторых, при разрушении формы содержащиеся на ней компоненты также разрушаются через вызов первого метода в виртуальной таблице, что ведет к аналогичному результату.

В заключение следует рассмотреть на первый взгляд странный вопрос: а всегда ли следует вызывать деструктор (непосредственно или через метод Free) из кода приложения? Правомерность постановки такого вопроса обусловлена тем, что программист нигде не пишет кодов вызова деструкторов компонентов, помещенных на форму на этапе разработки. Ответ заключается в структуре и реализации деструктора класса TComponent. Любой компонент в конструкторе запоминает ссылку на своего хозяина (AOwner) и заносит себя в список компонентов, которыми владеет хозяин. При вызове деструктора компонента он в первую очередь вызывает деструкторы своих «вассалов», и только после этого вызывается собственный деструктор. Таким образом, нет необходимости вызывать деструктор класса TComponent или его потомка - он будет автоматически разрушен при вызове деструктора его хозяина:

TMyBox=class(TListBox)
 private
 FData:TComponent;
 public
 constructor Create(AOwner:TComponent); 
    override;
 end;
constructor TMyBox.Create(AOwner:TComponent);
begin
 inherited Create(AOwner);
 FData:=TComponent.Create(Self);
end;

В данном случае деструктор для разрушения объекта FData не нуждается в переписывании, поскольку он будет разрушен автоматически при разрушении объекта TMyBox. Деструктор для TComponent (или его потомка) следует вызывать только в случае, если его владелец - nil.

Для всех классов-потомков TComponent не следует в явном виде вызывать деструктор.

Для всех остальных классов необходимо в коде приложения вызывать деструктор.

В Dephi5 появились два новых виртуальных метода - AfterConstruction и BeforeDestruction, - которые вызываются сразу же после конструктора или перед вызовом деструктора соответственно. Можно поспорить насчет необходимости введения метода BeforeDestruction: любой класс имеет виртуальный деструктор, который можно переписать. Появление метода AfterConstruction следует приветствовать, поскольку виртуальный конструктор появляется только на уровне TComponent в иерархии классов VCL. Появление виртуального конструктора существенно облегчило написание приложений для распределенных вычислений. Например, TComObject - базовый класс для реализации интерфейсов в COM-серверах - является потомком TObject и не содержит виртуального конструктора. Экземпляры этого класса создаются в ответ на запрос клиентов, а не командами из кода приложений, что затрудняет выполнение инициализации переменных при создании экземпляра класса. Введение виртуального метода AfterConstruction сделало инициализацию данных в этих классах рутинной процедурой.

Область видимости переменных и методов

В любом классе определяется три секции, в которых можно определить переменные и методы: private , protected и public . В секции published , которая важна только для компонентов, можно определять только методы и свойства - переменные определять нельзя (правда, это и не требуется). Разбиение на секции необходимо для того, чтобы скрыть переменные и методы, - то есть чтобы запретить изменения значений переменных или вызов методов в классах-потомках или в экземплярах классов. Согласно правилам любой класс должен быть максимально скрытым и экспонировать только те методы, которые необходимы для изменения его состояния, но не вспомогательные. Что касается переменных, то их экспонирование вообще запрещено - вместо них используют свойства .

Рассмотрим следующий модельный класс:

TNameSave=class(TComponent)
private
 FData1:string;
 procedure CheckData1;
protected
 FData2:string;
 procedure CheckData2;
public
 FData3:string;
 procedure CheckData3;
end;

Описанное ниже поведение различных секций кода класса TNameSave относится к тому случаю, когда данный класс определен в отдельном модуле. Предположим, был создан экземпляр класса TNameSave. Используя ссылку на рабочую копию класса, можно изменить или прочитать значение переменной FData3 и вызвать метод CheckData3. При попытке же обратиться к переменным FData1, FData2 или вызвать методы CheckData1, CheckData2 компиляция завершается с диагностическим сообщением об отсутствии данной переменной или метода:

procedure TForm1.Button1Click(Sender: TObject);
var
 FC:TNameSave;
begin
 FC:=TNameSave.Create(Self);
 FC.FData3:='Test'; {Legal}
 FC.CheckData3; {Legal}
 FC.FData2:='12'; {Compile error}
 FC.CheckData1; {Compile error}
 FC.Free;
end;

Таким образом, переменные и методы, определенные в секции Private и Protected, скрыты для использования из экземпляра класса. Чтобы понять различие между ними, создадим новый класс - потомок класса TNameSave:

TSecondName=class(TNameSave)
private
 procedure DoSomething;
end;

В этом классе определим новый метод DoSomething, а в реализации этого метода попытаемся обратиться к переменным и методам класса TNameSave:

procedure TSecondName.DoSomething;
begin
 FData3:='AA'; {Legal}
 FData2:='BB'; {Legal}
 FData1:='CC'; {Compile error}
 CheckData3; {Legal}
 CheckData2; {Legal}
 CheckData1; {Compile error}
end;

В этом случае уже можно обратиться к методам и переменным, определенным в секции protected (так же, как и в public), но по-прежнему невозможно обратиться к методам, определенным в секции private, которые являются полностью закрытой. Поэтому в этой секции не имеет смысла определять виртуальные или динамические методы. В этом случае для выполнения приложения потребуется больше ресурсов (или код будет выполняться дольше), а переписать методы директивой override невозможно, поскольку они не видны в классах-потомках.

В C++ имеется такое понятие, как дружественный класс ( friend class ). Можно один класс объявить дружественным другому и после этого использовать переменные в секции private или вызывать методы из этой секции. В Delphi также существует понятие дружественных классов, однако синтаксис объявления классов дружественными отсутствует. Классы считаются дружественными, если они объявлены в одном и том же модуле ( unit ). Таким образом, если для примера выше класс TSecondName объявить в том же самом модуле, что и класс TNameSave, то можно вызывать все его методы и обращаться к переменным, даже определенным в секции private. Более того, если в этом же модуле будет создан экземпляр класса TNameSave, то, используя ссылку на него, можно также обращаться ко всем переменным и методам:

procedure TForm1.Button1Click(Sender: TObject);
var
 FC:TNameSave;
begin
 FC:=TNameSave.Create(Self);
 FC.FData3:='Test'; {Legal}
 FC.CheckData3; {Legal}
 FC.FData2:='12'; {Now Legal!}
 FC.CheckData1; {Now Legal!}
 FC.Free;
end;

Наличие дружественных классов, а также возможность доступа в классах-потомках к методам и переменным секции protected можно использовать в целях получения доступа к секции protected из экземпляра класса в произвольном модуле. В качестве примера рассмотрим часто встречающуюся задачу: получены (например, посредством автоматизации) какие-то данные, на которые в адресном пространстве приложения имеется указатель и размер которых известен. Необходимо считать эти данные в приложение через поток. Тривиальное решение таково: создается экземпляр класса TMemoryStream, вызывается его метод Write, данные помещаются в поток. Далее указатель текущей позиции устанавливается на начало потока, и происходит считывание данных из него. У такого решения имеются два недостатка: тратятся системные ресурсы на создание второй копии данных и время на их копирование. С другой стороны, TMemoryStream имеет метод SetPointer, который позволяет просто объявить данный указатель началом потока без создания его копии в памяти. Но этот метод вызвать нельзя - он содержится в секции protected! Поэтому мы поступаем следующим образом:

type
 TMyStream=class(TMemoryStream)
 end;
 procedure ReadPointer(PData:pointer; Size:integer);
var
 Stream:TMyStream;
begin
 Stream:=TMyStream.Create;
 Stream.SetPointer(PData,Size);
 {... reading from stream ...}
 Stream.SetPointer(nil,0);
 Stream.Free;
end;

Иными словами, мы просто объявляем новый класс - и все! А поскольку класс для данного модуля считается дружественным, в экземпляре класса можно вызвать protected-метод SetPointer. Единственный нюанс: перед вызовом деструктора необходимо установить указатель равным nil, иначе деструктор TMemoryStream попытается освободить память и, если память была выделена не в данном модуле, произойдет исключение.

Свойства

Выше уже упоминалось о том, что переменные положено скрывать (инкапсулировать) в классе. Поэтому их объявляют в секции private и изредка в секции protected. Возникает вопрос: каким образом можно прочитать значения полей экземпляров классов или изменить их значение? Для этого используют свойства ( property ).

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

TPropClass=class(TObject)
private
 FData:integer;
public
 property Data:integer read FData write FData;
end;

В данном классе объявлено свойство Data типа integer. При чтении свойства возвращается содержимое переменной FData, а при записи этой переменной присваивается новое значение. Переменная FData должна быть того же типа, что и свойство. Чтение и запись свойств в Delphi осуществляется посредством оператора присваивания (:=) с использованием ссылки на экземпляр класса:

procedure TForm1.Button1Click(Sender: TObject);
var
 PC:TPropClass;
 N:integer;
begin
 PC:=TPropClass.Create;
 PC.Data:=5; {Property writing}
 N:=PC.Data; {Property reading}
 PC.Free;
end;

Свойство присваивается как обычная переменная. Однако в отличие от обычной переменной свойство не может быть послано в качестве фактического параметра метода, если его формальный параметр имеет модификатор var .

Имя свойства может быть любым, но по соглашению оно совпадает с именем переменной без буквы F в начале слова. Типы свойств могут быть любые - как стандартные (integer, string и др.), в том числе и классовые типы, так и определенные ранее программистом (TMyListBox…). В классе TPropClass свойство Data имеет доступ как для чтения (служебное слово read после указания типа), так и для записи (служебное слово write ). Также свойство может быть только для чтения ( property Data:integer read FData ) или только для записи ( property Data:integer write FData ). При попытке присвоить какое-либо значение свойству только для чтения или при попытке считать значение свойства только для записи возникает ошибка при компиляции проекта. Не бывает свойства, которое не будет доступно ни для чтения, ни для записи: конструкцию типа property Dummy, string не пропустит компилятор. Однако немного похожая конструкция property Dummy (без указания типа) вполне легальна (ее использование будет обсуждено ниже).

В приведенном выше примере при чтении или записи свойства происходит обращение к переменной FData. Однако при чтении и/или записи свойства может быть выполнен какой-нибудь метод. В этом случае свойство объявляется по-другому:

TPropClass=class(TObject)
private
 FData:integer;
 function GetData:integer;
 procedure SetData(Value:integer);
public
 property Data:integer read GetData write SetData;
end;

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

procedure TPropClass.SetData(Value:integer);
begin
 if (Value>0) and (Value<100) then FData:=Value 
    else begin
 beep;
 ShowMessage(‘Illegal value’);
 end;
end;

Название функции для считывания значения свойства GetData:integer может быть произвольным, но по соглашению оно должно совпадать с именем свойства с приставкой Get перед ним. Возвращаемый результат обязан совпадать с типом свойства (в данном случае - integer). В процедуре SetData (Value:integer) название также не играет роли, но по соглашению должно совпадать с названием свойства с добавлением приставки Set перед ним. Название параметра ( Value ) может быть любым, но обычно используют Value . А вот тип параметра обязан совпадать с типом свойства. Методы для чтения/записи свойства могут быть статическими и виртуальными, но не динамическими. И наконец, свойство может быть смешанным. Это означает, что при чтении свойства вызывается метод, а при записи обращаются к переменной и наоборот:

property Data:integer read GetData write FData;
property Data:integer read FData write SetData;

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

TArrayClass=class(TObject)
 private
 procedure SetFirstName(Index:integer; 
    Value:string);
 function GetFirstName(Index:integer):string;
 public
 property FirstName[NIndex:integer]:string 
    read GetFirstName write SetFirstName;
 end;

Для свойства-вектора после названия свойства указываются в квадратных скобках идентификатор и его тип. По нему будет осуществляться индексация массива. Название идентификатора абсолютно ни с чем не связано и никакой роли не играет. Обычно его называют Index , но в нашем примере он называется Nindex и нигде в приложении больше не используется. О типе идентификатора мы поговорим чуть позже. К методам для чтения и записи свойства добавляется также идентификатор элемента массива. Его тип обязан совпадать с типом, указанным в квадратных скобках при определении свойства. Имя не имеет значения, но в отличие от имени идентификатора, указанного в квадратных скобках (которое нигде не используется), имя этого идентификатора используется при реализации метода.

Нетрудно объявить и двухмерный массив:

TArrayClass=class(TObject)
 private
 procedure SetFirstName(Index1,Index2:integer; 
    Value:string);
 function GetFirstName(Index1,Index2:integer):string;
 public
 property FirstName[I1,I2:integer]:string 
    read GetFirstName write SetFirstName;
 end;

Сравнив объявления вектора и двухмерного массива, нетрудно догадаться, как объявить N-мерный массив.

К свойствам-массивам обращаются как к обычному массиву после создания экземпляра класса:

procedure TForm1.Button1Click(Sender: TObject);
var
 AC:TArrayClass;
 S:string;
begin
 AC:=TArrayClass.Create;
 AC.FirstName[1,2]:='Test';
 S:=AC.FirstName[5,1];
 AC.Free;
end;

Теперь о типе индексов массивов. В Object Pascal в качестве индексов используются переменные ординального ( ordinal ) типа (то есть переменные, содержащие данные, которые можно упорядочить и пронумеровать). Разрешается объявлять массивы:

var
 N:array [1..5] of integer;
 K:array ['A'..'Z'] of string;
 M:array [mrOK..mrNo] of boolean; {Modal result enumeration}

Но запрещается объявлять следующие массивы:

var
 N:array [1.4..3.14] of integer; {Floating-point index}
 K:array ['AZ'..'ZA'] of string; {String index}

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

TDistanceClass=class(TObject)
 private
 function GetDistance(Index:double):string;
 public
 property Distance[Index:double]:string 
    read GetDistance;
end;
function TDistanceClass.GetDistance(Index:double):string;
begin
 if Index<0 then Result:='Illegal' else
 if Index<1 then Result:='Short' else
 if Index<3.24 then Result:='Medium' else Result:='Long';
end;

и при создании экземпляра класса использовать индексы - действительные числа:

procedure TForm1.Button2Click(Sender: TObject);
var
 DC:TDistanceClass;
begin
 DC:=TDistanceClass.Create;
 Caption:=DC.Distance[-1.345]+' '+DC.Distance[Pi];
 DC.Free;
end;

Формально это выглядит как обращение к массиву, индексированному действительными числами!

Со свойствами-массивами тесно связано одно из двух значений служебного слова default . Если свойство-массив определить со служебным словом default (для примера выше):

property Distance[Index:double]:string read GetDistance; default;

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

Caption:=DC[-1.345]+' '+DC[Pi];

Обратите внимание, что идентификатор distance отсутствует после идентификатора экземпляра класса (DC). Если имеется несколько свойств-массивов, то служебное слово default можно использовать только с одним из них, в противном случае во время компиляции произойдет ошибка.

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

property Age:integer read FAge write FAge default 30;

Если значение свойства, установленное в инспекторе объектов, отличается от 30 и отсутствует директива stored (см ниже) или ее значение равно True , то это значение будет сохранено в потоке ресурсов. Если для свойства не указывается директива default , то берется соответствующее значение для класса-предка. Если директива вообще отсутствует, то любое значение свойства будет запоминаться в потоке ресурсов. Начинающие программисты часто пытаются при помощи этой директивы установить значения свойств по умолчанию, то есть немедленно после выполнения конструктора класса. Из вышесказанного ясно, что желаемый результат в этом случае достигнут не будет. Для изменения свойств по умолчанию необходимо в явном виде присваивать им значения в конструкторе. Как видно, данное значение директивы default настолько отличается от ее использования с массивами, что было бы разумно сделать два различающихся служебных слова, чтобы не путать их действия.

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

И наконец, последняя директива, используемая со свойствами, - stored . Она используется следующим образом:

property Age:integer read FAge write FAge stored False;

После директивы stored указывается либо True, либо False, либо идентификатор логической переменной в данном классе. Если значение этой переменной равно True, то будет ли запоминаться свойство в ресурсах или нет регулируется директивой default. Если же значение этой переменной равно False, то значение свойства не будет запоминаться в ресурсах, каким бы мы его не назначили в инспекторе объектов.

Свойства можно определять в любой из секций (private, protected, public и published). Компилятор Delphi разрешает определять свойства в секции private, но это не имеет смысла. Свойство в секции private невозможно ни использовать, ни переопределить. Далее, если свойство объявлено в секции published, то для компонента оно показывается в инспекторе объектов (если оно имеет доступ read и write). Секция public - стандартное место объявления свойств для классов, не являющихся компонентами. Для компонентов в секции public объявляются так называемые run-time properties - свойства, к которым можно обращаться только во время выполнения приложения. И наконец, в секции protected свойства объявляются для переопределения в классах-потомках.

Переопределение ( редекларация ) свойств означает повторное объявление свойства в классе-потомке. Редекларация используется в целях:

  1. Изменения способа чтения или записи свойства. Если в классе-предке при чтении свойство ссылалось на переменную, то в классе-потомке можно определить метод.
  2. Изменения значение default (для параметра) или в случае необходимости отмены ранее определенной директивы default либо изменения директивы stored. Все эти изменения влияют на способ запоминания свойств в ресурсах.
  3. Экспонирования свойства компонента в инспекторе объектов. Если свойство ранее не демонстрировалось в инспекторе объектов, то его можно экспонировать в классе-потомке простым переопределением в секции published.

Сказанное выше можно проиллюстрировать следующим примером:

TInitialClass=class(TComponent)
 private
 FData1,FData2,FData3,FData4,FData5:integer;
 protected
 property Data1:integer read 
    FData1 write FData1;
 property Data2:integer read 
    FData2 write FData2 default 20;
 property Data3:integer read 
    FData3 write FData3 default 20;
 property Data4:integer read 
    FData4 write FData4 stored True;
 property Data5:integer read 
    FData5 write FData5;
 end;
 TRedeclaredClass=class(TInitialClass)
 private
 procedure SetData1(Value:integer);
 protected
 property Data1 write SetData1; 
    {Using method instead of variable}
 property Data2 default 30; 
    {Changing default value}
 property Data3 nodefault; {Reject 
    early defined Default}
 property Data4 stored False; 
    {Not store in stream now}
 published
 property Data5; {The property 
    will be exposured in the Object Inspector}
 end;

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

Некоторые классы Delphi

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

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

Потомок TObject - класс TPersistent. В этом классе реализованы механизмы запоминания значений всех классовых переменных в поток и считывания их из потока через объект TFiler. Кроме того, определен метод Assign, который позволяет скопировать значения классовых переменных из одного объекта в другой. Эти методы являются виртуальными и «пустыми» - в секции реализации определены только заголовки и небольшое количество кода, информирующего об ошибках, но они будут переписаны в потомках этого класса.

И наконец, класс TComponent является потомком TPersistent. В этом классе реализована иерархия «владелец-вассалы». Класс TComponent имеет поле Owner, в котором хранится ссылка на его владельца. Он также имеет список Components, куда помещаются ссылки на его вассалов. Эта иерархия используется в деструкторе компонента, при вызове деструктора владельца автоматически вызываются все деструкторы вассалов. Такая иерархия характерна именно для библиотеки Delphi Visual Component Library, и ее не следует путать с иерархией «родитель-дети», которая реализована в Windows API и вводится в потомках TComponent. В классе определен виртуальный конструктор. Компоненты Delphi могут быть только потомками класса TComponent. Можно создать потомка любого другого класса, но установить его на палитру компонентов не удастся.

Следующий важный потомок TComponent - TControl. На этом этапе появляются свойства Left, Top, Width, Height. Очевидно, что потомков TControl можно увидеть на экране во время работы приложения. Становится понятной разница между визуальными и невизуальными компонентами: визуальные компоненты имеют в качестве предка TControl, а невизуальные не содержат его в списке предков. В этом же классе появляется свойство Parent - начало иерархии «отец-дети», которую мы обсудим ниже. Анализируя методы класса TControl, следует отметить появление ряда методов, название которых начинается с букв WM… и CM… Это обработчики сообщений. Многие начинающие программисты заблуждаются, думая что TControl способен получать сообщения. Принимать сообщения может только класс TWinControl, и он уведомляет элементы управления о приеме сообщения посредством вызова данных методов.

Следующим потомком является класс TWinControl. Этот класс инкапсулирует окно Windows API. На этом этапе появляется иерархия «родитель-дети». Помимо ссылки на родителя (Parent), которая была определена еще на уровне TControl, появляется список Controls, содержащий ссылки на «детей» главного окна. Иерархия «родители-дети» определяет, каким образом окна будут расположены на экране: дети всегда лежат поверх родителей. Рассматриваемый выше класс TControl всегда выступает формально как чей-нибудь ребенок, хотя для рисования содержимого он использует полотно (Canvas) родителя. Координаты левого верхнего угла ребенка всегда отсчитываются относительно клиентской области родителя. Если TWinControl и TControl содержат пересекающуюся область, то TWinControl всегда будет нарисован сверху TControl. Не все потомки класса TWinControl могут выступать в качестве родителей: этим свойством обладают классы (и их потомки) TForm, TPanel, TGroupBox, TScrollBox.

Кроме того, класс TWinControl способен принимать сообщения. Подробно про сообщения и их обработку мы расскажем ниже. Пока же достаточно знать, что для каждого экземпляра класса TWinControl создается довольно объемная процедура, которая обрабатывает сообщения по умолчанию (термин «по умолчанию» используется здесь потому, что программист может изменить обработчики событий). Поэтому объекты TWinControl требуют значительного количества системных ресурсов: по моим расчетам, примерно на порядок больше, чем аналогичный объект - потомок TControl (например, TButton и TSpeedButton). У TWinControl появляется свойство Handle, которое имеет тип HWND(Windows API). Он ссылается на некоторую область в памяти, которую занимает процедура обработки событий по умолчанию.

Для TWinControl существует такое понятие, как фокус ввода. Это понятие связано с тем, какому окну в данный момент посылаются сообщения о нажатии клавиш на клавиатуре. Клавиатура у компьютера одна, а окон в приложении обычно бывает много. К тому же одновременно может быть загружено несколько приложений. Про то окно, которое в данный момент принимает сигналы с клавиатуры, говорят, что оно имеет фокус ввода. В каждый данный момент только одно окно может иметь фокус ввода. Оно отмечается визуально: пунктирная черта на кнопках и списках, мигающий курсор на редакторах. Окно, имеющее фокус ввода, определяется на уровне операционной системы. Если вызвать деструктор такого окна, не проинформировав операционную систему (которая перенесет фокус ввода), то, как только пользователь нажмет клавишу или система захочет изменить фокус ввода, произойдет исключение - возможно, нескоро. Для TWinControl при вызове деструктора перемещение фокуса ввода произойдет автоматически, если только окно находится на форме. Для окон, имеющих стиль WS_POPUP (Hint и его аналоги), необходимо самим переставлять фокус ввода или информировать систему об этом посредством посылки разрушаемому окну сообщения WM_DESTROY. Обработчик события по умолчанию информирует систему при получении этого сообщения.

Сообщения в Windows и их обработка

Принцип работы операционной системы Windows полностью базируется на получении и анализе сообщений. Вся работа любого приложения, показываемого на экране и имеющего меню, заключена в команде Application.Run . Это бесконечный цикл, в задачу которого входит: достать очередное сообщение из очереди, определить, какому окну оно адресовано, и отправить это сообщение для дальнейшей обработки. Как только этот цикл разрывается, приложение прекращает свою работу и закрывается. Именно способностью обрабатывать сообщения среда Windows принципиально отличается от DOS. Если в среде DOS все, как правило, жестко детерминировано - пользователь, например, должен сначала ввести фамилию, затем имя и после отчество, то в среде Windows он, как правило, такой ввод может осуществлять в произвольном порядке. Именно после победного шествия Windows в конце 80-х умерло структурное программирование, которое всех учило, что программа должна иметь одну точку входа и выхода, может ветвиться, но не возвращаться назад. С сообщениями так не получается: программа может в любой момент выполнить произвольные участки кода.

Сообщения в Windows посылаются как реакция на какое-либо событие. Пользователь нажал кнопку мыши - поступило сообщение, COM-порт получил какие-либо данные - поступило другое сообщение, необходимо перерисовать какую-либо область экрана - поступило еще одно сообщение. В Windows имеется около 130 зарегистрированных сообщений, которые генерируются при возникновении какого-либо события в системе. Кроме того, большинство стандартных элементов управления Microsoft также имеют свои сообщения - как командные, так и нотификационные. Например, список (TListBox) имеет командные сообщения, начинающиеся с приставки LB: LB_ADDSTRING - добавляет строку в список, LB_SETSEL - меняет селекцию выбранной строки. Нотификационные сообщения начинаются с префикса LBN: LBN_DBLCLK - двойной щелчок мыши и др. Всего для списка предусмотрено 46 специальных сообщений. Аналогичные сообщения (в разном количестве) имеются и для других элементов управления. Кроме того, программист может определять свои собственные сообщения в достаточно большом количестве. Они начинаются с константы WM_USER.

После отправки сообщения оно попадает в очередь сообщений (message queue) приложения, которому оно адресовано. В очереди сообщение ожидает, когда приложение извлечет его и обработает, и после этого уничтожается. Следует всегда помнить, что если в главном потоке приложения выполняется какой-либо код, то сообщения из очереди не извлекаются и не обрабатываются до завершения работы кода. Каждый, наверное, обращал внимание, что после выполнения диалога открытия файла и начала загрузки его в некоторых приложениях не сразу рисуется область, где был диалог, а только после окончания загрузки файла. Если программист не принимает специальных мер, то сообщение WM_PAINT посылается, становится в конец очереди, но выполнение кода загрузки файла мешает приложению извлечь это сообщение и обработать его.

Сообщения посылаются автоматически, но программист имеет возможность послать их из кода приложения. Имеются два метода Windows API - SendMessage и PostMessage , - которые посылают сообщения конкретному окну приложения.

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

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

Чтобы лучше понять разницу между этими методами, можно рассмотреть методы Invalidate и Refresh класса TControl. Оба этих метода объявляют, что прямоугольная область, занимаемая элементом управления, нуждается в перерисовке. Но метод Invalidate посылает родителю сообщение WM_PAINT через метод PostMessage, а Refresh - методом SendMessage. Поэтому при вызове метода Invalidate перерисовка совершается только после окончания работы кода, а метод Refresh заставляет элемент управления перерисоваться немедленно.

Теперь рассмотрим структуру записи, используемую для передачи сообщения. Сообщение передается в записи TMessage, структура ее следующая:

type
 TMessage = record
 Msg: Cardinal;
 case Integer of
 0: 
    (
 
    WParam: Longint;
 
    LParam: Longint;
 
    Result: Longint);
 1: 
    (
 
    WParamLo: Word;
 
    WParamHi: Word;
 
    LParamLo: Word;
 
    LParamHi: Word;
 
    ResultLo: Word;
 
    ResultHi: Word);
 end;

Поле Msg содержит тип сообщения (WM_PAINT, WM_DESTROY, LB_ADDSTRING…). Содержимое параметров WParam и LParam зависит от типа сообщения (поле Msg). Перед тем как делать обработчик или посылать какое-либо сообщение, следует внимательно изучить содержимое этих полей. Это крайне важно. Наконец, сообщение может вернуть результат в поле Result. Он также зависит от значения поля Msg. Как правило, это 0, но для некоторых сообщений это может быть, например, указатель на область памяти, где хранится графическое изображение для окна, и др.

В Delphi определен ряд записей - TWMMouseMove, TWMKeyUp… Эти записи также используются для передачи сообщений. Размер их полностью совпадает с размером TMessage. Они имеют поля Msg и Result, а вместо переменных WParam и LParam определены другие переменные с суммарным размером 8 байт. Форма записи используется исключительно для удобства в реализации обработчика сообщений. Вместо обработчика сообщений на нажатие левой кнопки мыши, которое использует запись TWMLButtonDown:

Procedure WMLButtonDown(var Message:TWMLButtonDown);

можно записать обработчик с использованием TMessage:

Procedure WMLButtonDown(var Message:TMessage);

Теперь мы вплотную приблизились к обработчикам событий. События обрабатывает специальный метод (WndProc), который создается для каждого экземпляра класса TWinControl и его потомков. Если программиста не устраивает обработчик события по умолчанию, он может его переопределить:

TMyButton=class(TButton)
private
 procedure WMLButtonUp(var Message:TMessage); message 
    WM_LBUTTONUP;
end;
implementation
procedure TMyButton.WMLButtonUp(var Message:TMessage);
begin
 inherited;
 Beep;
end;

В данном примере переопределен обработчик события, который вызывается, когда пользователь отпускает нажатую левую кнопку мыши. Обработчики событий следует реализовывать в секции private. Имя метода (WMLButtonUp) может быть произвольным, но по соглашению он называется так же, как и константа, идентифицирующая событие, но без нижнего подчеркивания ( _ ). Метод должен зависеть от переменного параметра типа TMessage или сопоставимого с ним типа (TWMLButtonDown, TWMGetMinMaxInfo… ). Имя параметра может быть любым. То, что это обработчик является сообщением, определяет служебное слово message , а тип перехватываемого сообщения определяет константа после слова message.

Следует обратить внимание на реализацию обработчика сообщений. Используется служебное слово inherited без названия метода и списка параметров. Delphi транслирует это как необходимость вызвать обработчик события по умолчанию. В обработчиках событий обязательно надо вызывать метод по умолчанию или, по крайней мере, четко себе представлять, к чему это может привести. В данном примере при нажатии левой кнопки мыши над объектом TMyButton происходит захват сообщений мыши. Это значит, что если передвинуть мышь в сторону и отпустить кнопку, то сообщение все равно будет послано экземпляру TMyButton. Захват мыши прекращается в обработчике событий по умолчанию WM_LBUTTONUP. Если убрать оператор inherited из кода, то мышь не будет освобождена, - где бы мы ни щелкали кнопкой, все сообщения будут направляться объекту TMyButton. Так что приложение даже нельзя будет закрыть при помощи мыши.

Ситуацию, когда не надо вызывать обработчик события по умолчанию, хорошо иллюстрирует обработчик WM_CLOSE главной формы, когда ее нежелательно закрывать. Это событие возникает, когда пользователь нажимает кнопку Close в правой части заголовка формы. Вообще, форма имеет обработчик события OnClose, где можно изменить переменную Action и отказаться от закрытия формы. Но значение переменной Action абсолютно никакой роли для главной формы приложения не играет. Поэтому следует переписать обработчик события WM_CLOSE без оператора inherited. Если обработчик события не вызывается по умолчанию, обязательно следует присвоить подходящее значение полю Result переменной TMessage! Значение его зависит от типа сообщения, но в большинстве случаев оно равно нулю (сообщение обработано). При вызове обработчика событий по умолчанию поле Result трогать не надо, обработчик сам присвоит ему подходящее значение.

Заключение

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

Однако современные средства разработки приложений, к которым относится и Dephi 5, предлагают более мощные инструменты для манипулирования с классами, а именно работу с компонентами. Созданию компонентов будет посвящена следующая статья данного цикла.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=3641