Наряду с полями и свойствами, обеспечивающими сохранение статической информации об объекте, методы являются важнейшими атрибутами класса, обеспечивающими его функциональность.
В лекции рассмотрены практические приемы объявления и реализации методов класса и делегирования их реализации приложению с помощью свойств.
Как вам уже известно из первой лекции, методы представляют собой по сути дела процедуры и функции, объявленные в контексте объявления класса.
Очевидно, вы помните, что в соответствии с принципом инкапсуляции класс имеет две части – интерфейс и реализацию. В Delphi раздел интерфейса обозначается с использованием ключевого слова interface, а реализация – implementation.
При этом если метод не помечен как абстрактный с использованием ключевого слова abstract, то он должен быть реализован в разделе implementation.
Абстрактные методы не могут и не должны быть реализованы. Их реализация возлагается на потомков.
При объявлении методов следует придерживаться следующих правил:
действия методов должны быть взаимонезависимы. Это означает, что вызов метода не должен подразумевать обязательного вызова другого метода класса;
ни один из методов не должен приводить компонент в состояние, при котором другие методы не действуют; По сути дела это обратная сторона взаимонезависимости методов.
метод должен иметь осмысленное ("говорящее") имя. Другими словами, по названию метода должно быть возможно догадаться, что именно делает этот метод.
Например, очевидно, что метод, называющийся SaveToFile сохранит некоторую информацию в файл, а метод – LoadFromFile – загрузит ее.
Это же правило относится и к именам передаваемых методу параметров.
Например, параметр, называющийся FileName явно предназначен для передачи имени файла.
Следует придумывать для методов по возможности короткие "осмысленные" имена: они легче запоминаются и набираются на клавиатуре.
Не следует увлекаться акронимами (аббревиатурами) для названий методов. Назвав метод вместо LFF вместо LoadFromFile, вы рискуете в скором времени забыть, что он означает.
Но нет правил без исключений: например, в компоненте TDataSet есть свойство EOF, расшифровывающееся как "End Of File" – "конец файла". Но это все же исключение, имеющее свою историю: с ранних версий языка Pascal существует одноименная функция EOF(), позволяющая определить находится ли указатель в конце файла. Очевидно, разработчики VCL сочли логичным так же назвать и свойство.
Рекомендации по наименованию методов во многом применимы и для других атрибутов класса – полей, свойств и событий.
По степени видимости методы могут быть размещены с любой секции от private до public.
Если поместить метод в разделе published, то, не смотря на то, что компилятор не выдаст сообщения об ошибке, "опубликованного" метода получить не удастся. В результате получится просто публичный (public) метод.
С наименованием методов все ясно. Разберемся теперь собственно с их реализацией.
Пример 1. Итак, метод объявлен в интерфейсной части класса
unit Unit6; interface type TFirstClass=class private procedure PrivatMethod; end; implementation end.
Теперь необходимо его реализовать. Для этого в разделе implementation необходимо поместить требуемый код.
Пример 2. Код оформляется с указанием имени класса и, через точку – название метода с перечислением всех передаваемых параметров:
procedure TFirstClass.PrivatMethod; begin // Код метода end;
Такую заготовку для кода реализации можно, конечно, ввести с клавиатуры или, скопировав с помощью буфера обмена его объявление из интерфейсной части, добавить недостающие элементы. Однако для реализации атрибутов класса в редакторе кода Delphi предусмотрено специальное сочетание клавиш. Пользоваться им очень просто: достаточно поставить курсор на строку с интерфейсным объявлением и нажать сочетание клавиш <Ctrl>+<Shift>+<C>. В результате в разделе implementation автоматически будет создана заготовка для кода метода, как показано в листинге.
Пример 3. Автоматически созданная заготовка кода метода
unit Unit6; interface type TFirstClass=class private procedure PrivatMethod; // Для реализации нажать <Ctrl>+<Shift>+<C> end; implementation { TFirstClass } procedure TFirstClass.PrivatMethod; begin end; end.
Теперь в заготовку метода нужно ввести код, реализующий требуемую функциональность.
Код метода имеет доступ ко всем (в том числе и private) атрибутам класса. Если же необходимо явно указать, что атрибут относится к данному классу, то можно использовать неявную ссылку Self.
Обратите внимание, что при создании первого метода в разделе implementation непосредственно перед ним будет вставлен комментарий с именем реализуемого класса. Если в последующем с помощью вышеописанного приема создавать другие атрибуты класса, то связанные с их реализацией методы будут помещаться в модуле поблизости (ниже) от упомянутого комментария. Это сделает модуль компактным и удобным для дальнейших модификаций.
Реализация методов имеет свои особенности по сравнению с простыми функциями и процедурами. Поскольку в соответствии с принципом полиморфизма названия методов класса-наследника могут совпадать с именами методов класса-предка, то должна быть возможность в методах наследника вызвать методы предка. Такая возможность реализуется с помощью ключевого слова inherited с указанием имени метода предка. Если метод предка вызывается из одноименного метода наследника, то имя метода можно не указывать, а ограничиться одним лишь словом inherited.
Пример 5. Пример реализации статических и виртуальных методов
program Project6; {$APPTYPE CONSOLE} uses SysUtils; type TFirst=class procedure StaticProc; procedure VirtualProc; virtual; procedure DynamicProc; dynamic; procedure Broken; virtual; end; TSecond=class(TFirst) procedure StaticProc; procedure VirtualProc; override; procedure DynamicProc; override; procedure Broken; virtual; end; { TFirst } procedure TFirst.Broken; begin Writeln('TFirst.Broken'); end; procedure TFirst.DynamicProc; begin Writeln('TFirst.DynamicProc'); end; procedure TFirst.StaticProc; begin Writeln('TFirst.StaticProc'); end; procedure TFirst.VirtualProc; begin Writeln('TFirst.VirtualProc'); end; { TSecond } procedure TSecond.Broken; begin inherited; Writeln('TSecond.Broken'); end; procedure TSecond.DynamicProc; begin inherited; Writeln('TSecond.DynamicProc'); end; procedure TSecond.StaticProc; begin inherited; Writeln('TSecond.StaticProc'); end; procedure TSecond.VirtualProc; begin inherited; Writeln('TSecond.VirtualProc'); end; procedure TestClass(P: TFirst); begin with P do begin StaticProc; VirtualProc; DynamicProc; Broken; end; end; var TestObj: TSecond; begin TestObj:= TSecond.Create; Writeln('============ Test methods ============'); with TestObj do begin StaticProc; VirtualProc; DynamicProc; Broken; end; Writeln('============ TestClass ============'); TestClass(TestObj); Writeln('============ Test as ============'); with TFirst(TestObj) do begin StaticProc; VirtualProc; DynamicProc; Broken; end; Readln; end.
Как видно из вышеприведенного листинга, класс TFirst реализует один статический, два виртуальных и один динамический методы. Логика их работы проста (каждый из методов выводит в стандартный вывод консольной программы текстовое сообщение, соответствующее названию метода) и служит лишь для индикации срабатывания метода.
Второй класс TSecond является наследником TFirst и переопределяет все его методы. Причем виртуальный методы VirtualProc и динамический метод DynamicProc переопределяются (override), статический метод StaticProc и виртуальный метод Broken практически объявляется повторно.
Реализация всех методов класса TSecond обеспечивает вызов одноименных методов предка (TFirst) и использованием ключевого слова inherited.
Программа создает экземпляр класса TSecond и вызывает все его методы. В приведенном ниже листинге показан результат работы методов, обозначенный заголовком "Test methods".
Пример 6. Результат работы программы
============ Test methods ============ TFirst.StaticProc TSecond.StaticProc TFirst.VirtualProc TSecond.VirtualProc TFirst.DynamicProc TSecond.DynamicProc TFirst.Broken TSecond.Broken ============ TestClass ============ TFirst.StaticProc TFirst.VirtualProc TSecond.VirtualProc TFirst.DynamicProc TSecond.DynamicProc TFirst.Broken ============ Test as ============ TFirst.StaticProc TFirst.VirtualProc TSecond.VirtualProc TFirst.DynamicProc TSecond.DynamicProc TFirst.Broken
Из листинга видно, что действительно при вызове методов потомка сначала вызываются методы предка (TFirst)
Вроде бы все работает как надо. Но что будет, если в программе понадобиться (а в программировании такая необходимость возникает весьма часто) централизованно обрабатывать экземпляры различных классов?
Для имитации такой обработки в программе реализована процедура TestClass, которая принимает параметр типа TFirst и вызывает все его методы. Поскольку TSecond является наследником TFirst, то экземпляр такого типа тоже может передаваться в качестве аргумента процедуре.
В результате работы процедуры (заголовок "TestClass") оказывается, что работа правильно переопределенных виртуальных методов не претерпела никаких изменений, а заново определенные методы StaticProc и Broken работают так, как они реализованы в классе TFirst.
Это произошло потому, что повторное объявление виртуального метода фактически разрушает сложный полиморфизм и при неявно приведении типа, которое имеет место при передаче параметра процедуре, срабатывает только реализация в базовом классе.
Кстати, такое грубое нарушение не остается незамеченным компилятором, он честно выдаст предупреждение (Warning) о том, что метод "прячет" уже существующий в базовом классе виртуальный метод:
Пример 7.
[Pascal Warning] Project6.dpr(19): W1010 Method 'Broken' hides virtual method of base type 'TFirst'
Что касается статического метода, то с ним совсем все просто – он имеет свой адрес и никоим образом не зафиксирован в таблице виртуальных методов. Естественно, что при неявном приведении типа будет вызвана только реализация в базовом классе.
Тот же эффект возникает и при явном приведении типа, который реализован в третьей части эксперимента (заголовок "Test as").
Из поставленного эксперимента можно сделать несколько важных выводов:
реализация сложного (истинного) полиморфизма подразумевает переопределение виртуальных и динамических методов с вызовом реализации в классе-предке;
если класс наследник полностью изменяет логику работы виртуального метода и не нуждается в его реализации классом-предком, то можно не вызывать ее с помощью ключевого слова inherited. Это не разрушит полиморфизма метода;
статические методы в классе предназначены для реализации только той логики, которая ни при каких обстоятельствах не будет переопределяться в классах-наследниках.
Такие атрибуты объектов, как поля и методы составляют минимально необходимую основу объектно-ориентированной парадигмы и реализованы буквально в каждом невизуальном языке программирования, поддерживающим объекты. В таких языках для реализации новой функциональности объекта переопределяются (перекрываются) необходимые методы, которые возможно изменяют значения полей.
В визуальных системах разработки дело обстоит несколько иначе. Для реализации новой функциональности далеко не всегда разрабатывается новый класс. Действительно, было бы, по крайней мере, нелогично разрабатывать новый класс кнопки, которая должна всего лишь закрыть форму или вызвать диалог выбора файла. Вместо этого класс позволяют обеспечить выполнение написанного пользователем компонента кода в случае возникновения какого-либо события. Такой код называется «обработчиком события». Фактически класс как бы «поручает» (или как еще говорят «делегирует») реализацию метода приложению.
Рассмотрим как это устроено.
Для того чтобы вызвать обработчик события необходимо запомнить его адрес, перечень и темы передаваемых параметров. Вся эта информация запоминается в приватном поле класса объектного типа.
Сам адрес обработчика события устанавливается через свойство. Так что событие класса фактически является свойством объектного типа.
Если событие это – свойство, то может возникнуть вопрос "Каким же образом Delphi определяет, какие свойства объекта показывать на странице “Properties” («Свойства»), а какие – на странице “Events” («События»)?" Для определения страницы инспектор объектов использует информацию о типе свойств. На странице “Events” появляются только свойства типа «указатель на метод».
Пример 8. Фрагмент кода библиотеки VCL, иллюстрирующий объявление событий.
type TControl = class(TComponent) private … FOnDblClick: TNotifyEvent; … protected property OnDblClick: TNotifyEvent read FOnDblClick write FOnDblClick; … end;
Из листинга видно, что объявление события, как и любого свойства, состоит из двух частей. Во-первых, событие требует наличия внутреннего поля для хранения указателя на метод. Во-вторых, создается соответствующее свойство, которое предоставляет возможность во время проектирования присоединить обработчик события.
Объявление типа «указатель на метод» напоминает объявление процедурного типа с добавлением спецификатора of object.
Пример 9. Так, например, простейший тип ссылки на метод TNotifyEvent объявлен следующим образом:
TNotifyEvent = procedure(Sender: TObject) of object;
Указатель на метод может быть и функцией.
В библиотеке VCL принято первым параметром всех делегируемых методов передавать ссылку на сам объект, вызывающий событие. По соглашению этот первый параметр называется Sender и имеет тип TObject. Он позволяет определить, какой именно объект вызвал обработчик события.
Для того, чтобы обработчик события был в какой-то момент вызван, в коде класса в нужном месте необходимо реализовать проверку существования кода обработчика события и его вызов. Операции по определению наличия делегированного метода, как правило, возлагается на так называемый "метод диспетчеризации".
Пример 10. Типичный код метода диспетчеризации выглядит следующим образом:
procedure TControl.DblClick; begin if Assigned(FOnDblClick) then FOnDblClick(Self); end;
Логика работы приведенного метода следующая: сначала с помощью функции Assigned проверяется неравенство ссылки на метод значению Nil. Только в том случае, если адрес обработчика события отличен от nil, его код выполняется. Обратите внимание, что первым параметром обработчику передается ссылка на сам объект с помощью неявной ссылки Self.
Методы диспетчеризации (а их может быть более одного на одно событие), как правило, объявляются как защищенные (protected) методы того класса, которому они принадлежат. Делается это для того, чтобы логика их работы могла быть переопределена в классах-наследниках.
В библиотеке VCL объявлено большое количество типов "указателей на методы", которых хватает для диспетчеризации часто возникающих событий, связанных с клавиатурой, мышью и так далее. Однако в ряде случаев возникает необходимость определить свой пользовательский тип "указателя на метод".
Предположим, что разрабатываемый компонент должен производить некоторые вычисления и в случае успеха возвращать результат в обработчик события.
В этом случае необходимо объявить новый тип "указателя на метод" (см. листинг ниже) и создать в разрабатываемом компоненте свойство, хранящее ссылку на метод. В нужный момент уже известным способом вызвать обработчик события (делегированный метод).
Пример 11. Объявление и использование пользовательского типа "ссылка на метод"
unit Unit8; interface type TOnResult=procedure(Res: extended) of object; TSolver=class private FOnResult: TOnResult; public procedure Calc; published property OnResult:TOnResult read FOnResult write FOnResult; end; implementation { TSolver } procedure TSolver.Calc; var r: extended; begin r:=random(); // Некоторые вычисления if Assigned(FOnResult) then FOnResult(r); end; end.
Иногда при объявлении типа "ссылка на метод" требуется в параметрах указать класс, который будет объявлен позже, например сам компонент. В этом случае прибегают к приему "упреждающего объявления класса".
Пример 12. Пример использования упреждающего объявления класса
unit Unit8; interface type TSolver=class; // Упреждающее объявление TOnResult=procedure(Comonent: TSolver;Res: extended) of object; TSolver=class private FOnResult: TOnResult; public procedure Calc; published property OnResult:TOnResult read FOnResult write FOnResult; end; implementation { TSolver } procedure TSolver.Calc; var r: extended; begin r:=random(); // Некоторые вычисления if Assigned(FOnResult) then FOnResult(Self, r); end; end.
Как видно из листинга, упреждающее объявление класса представляет собой декларацию типа со знаком равенства и ключевым словом class, закрытого точкой с запятой.
На первый взгляд в листинге как будто присутствует два объявления класса. На самом деле компилятор, встретив упреждающее объявление класса, "считает", что такой класс есть, а "подробности ему будут сообщены позже".
С помощью такого приема становится возможным сделать казалось бы невозможно рекурсивные объявления типов.
Методы являются важнейшими атрибутами класса, реализующими его функциональность;
Статические методы следует объявлять в классе для тех действий, которые не будут переопределяться в потомках;
Виртуальные и динамические методы следует переопределять в потомках с использованием ключевого слова override в интерфейсной части и вызовом метода предка с использованием ключевого слова inherited;
События представляют собой свойства типа "ссылка на метод" и хранят адрес процедуры или функции приложения, реализующей делегируемый метод.
Что такое метод?
Какие особенности имеет абстрактный метод?
Какими правилами следует руководствоваться при выборе названия методу?
Может ли метод быть опубликованным (published)?
Как правильно переопределить виртуальный (динамический) метод?
Как вызвать метод предка?
Что такое событие?
Как реализовать метод диспетчеризации события?
Сколько для одного события может быть методов диспетчеризации?
Как объявить пользовательский тип "ссылка на метод"?
Как упреждающе объявить класс?