Как вам уже известно из первой лекции, методы представляют собой по сути дела процедуры и функции, объявленные в контексте объявления класса.
Очевидно, вы помните, что в соответствии с принципом инкапсуляции класс имеет две части – интерфейс и реализацию. В Delphi раздел интерфейса обозначается с использованием ключевого слова interface, а реализация – implementation.
При этом если метод не помечен как абстрактный с использованием ключевого слова abstract, то он должен быть реализован в разделе implementation.
Абстрактные методы не могут и не должны быть реализованы. Их реализация возлагается на потомков.
При объявлении методов следует придерживаться следующих правил:
действия методов должны быть взаимонезависимы. Это означает, что вызов метода не должен подразумевать обязательного вызова другого метода класса;
ни один из методов не должен приводить компонент в состояние, при котором другие методы не действуют; По сути дела это обратная сторона взаимонезависимости методов.
метод должен иметь осмысленное ("говорящее") имя. Другими словами, по названию метода должно быть возможно догадаться, что именно делает этот метод.
Например, очевидно, что метод, называющийся SaveToFile сохранит некоторую информацию в файл, а метод – LoadFromFile – загрузит ее.
Это же правило относится и к именам передаваемых методу параметров.
Например, параметр, называющийся FileName явно предназначен для передачи имени файла.
Следует придумывать для методов по возможности короткие "осмысленные" имена: они легче запоминаются и набираются на клавиатуре.
Не следует увлекаться акронимами (аббревиатурами) для названий методов. Назвав метод вместо LFF вместо LoadFromFile, вы рискуете в скором времени забыть, что он означает.
Но нет правил без исключений: например, в компоненте TDataSet есть свойство EOF, расшифровывающееся как "End Of File" – "конец файла". Но это все же исключение, имеющее свою историю: с ранних версий языка Pascal существует одноименная функция EOF(), позволяющая определить находится ли указатель в конце файла. Очевидно, разработчики VCL сочли логичным так же назвать и свойство.
Рекомендации по наименованию методов во многом применимы и для других атрибутов класса – полей, свойств и событий.
По степени видимости методы могут быть размещены с любой секции от private до public.
Если поместить метод в разделе published, то, не смотря на то, что компилятор не выдаст сообщения об ошибке, "опубликованного" метода получить не удастся. В результате получится просто публичный (public) метод.
С наименованием методов все ясно. Разберемся теперь собственно с их реализацией.
Пример 3.1. Итак, метод объявлен в интерфейсной части класса
unit Unit6; interface type TFirstClass=class private procedure PrivatMethod; end; implementation end.
Теперь необходимо его реализовать. Для этого в разделе implementation необходимо поместить требуемый код.
Пример 3.2. Код оформляется с указанием имени класса и, через точку – название метода с перечислением всех передаваемых параметров:
procedure TFirstClass.PrivatMethod; begin // Код метода end;
Такую заготовку для кода реализации можно, конечно, ввести с клавиатуры или, скопировав с помощью буфера обмена его объявление из интерфейсной части, добавить недостающие элементы. Однако для реализации атрибутов класса в редакторе кода Delphi предусмотрено специальное сочетание клавиш. Пользоваться им очень просто: достаточно поставить курсор на строку с интерфейсным объявлением и нажать сочетание клавиш <Ctrl>+<Shift>+<C>. В результате в разделе implementation автоматически будет создана заготовка для кода метода, как показано в листинге.
Пример 3.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.
Пример 3.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".
Пример 3.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) о том, что метод "прячет" уже существующий в базовом классе виртуальный метод:
Пример 3.7.
[Pascal Warning] Project6.dpr(19): W1010 Method 'Broken' hides virtual method of base type 'TFirst'
Что касается статического метода, то с ним совсем все просто – он имеет свой адрес и никоим образом не зафиксирован в таблице виртуальных методов. Естественно, что при неявном приведении типа будет вызвана только реализация в базовом классе.
Тот же эффект возникает и при явном приведении типа, который реализован в третьей части эксперимента (заголовок "Test as").
Из поставленного эксперимента можно сделать несколько важных выводов:
реализация сложного (истинного) полиморфизма подразумевает переопределение виртуальных и динамических методов с вызовом реализации в классе-предке;
если класс наследник полностью изменяет логику работы виртуального метода и не нуждается в его реализации классом-предком, то можно не вызывать ее с помощью ключевого слова inherited. Это не разрушит полиморфизма метода;
статические методы в классе предназначены для реализации только той логики, которая ни при каких обстоятельствах не будет переопределяться в классах-наследниках.