Профессиональная разработка приложений с помощью Delphi5
Часть 1. Основы объектно-ориентированного программирования
Объявление переменных и методов класса
Статические, динамические и виртуальные методы
Переписанные методы. Полиморфизм. Абстрактные методы
Создание экземпляров классов. Конструкторы и деструкторы
Область видимости переменных и методов
Сообщения в Windows и их обработка
Данная публикация является первой в цикле статей, посвященном профессиональной разработке приложений с помощью Delphi 5. Статьи цикла будут содержать базовые сведения, необходимые для создания и отладки приложений, отвечающих современным требованиям. Они представляют интерес для любого программиста — независимо от области применения создаваемых приложений.
Объектно-ориентированное программирование (ООП) – это основа для создания приложений в 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 сделало инициализацию данных в этих классах рутинной процедурой.