2. Методы

Как вам уже известно из первой лекции, методы представляют собой по сути дела процедуры и функции, объявленные в контексте объявления класса.

Очевидно, вы помните, что в соответствии с принципом инкапсуляции класс имеет две части – интерфейс и реализацию. В Delphi раздел интерфейса обозначается с использованием ключевого слова interface, а реализация – implementation.

Примечание

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

Внимание

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

2.1. Объявление методов в интерфейсной части класса

При объявлении методов следует придерживаться следующих правил:

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

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

  • метод должен иметь осмысленное ("говорящее") имя. Другими словами, по названию метода должно быть возможно догадаться, что именно делает этот метод.

Например, очевидно, что метод, называющийся SaveToFile сохранит некоторую информацию в файл, а метод – LoadFromFile – загрузит ее.

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

Например, параметр, называющийся FileName явно предназначен для передачи имени файла.

Совет

Следует придумывать для методов по возможности короткие "осмысленные" имена: они легче запоминаются и набираются на клавиатуре.

Внимание

Не следует увлекаться акронимами (аббревиатурами) для названий методов. Назвав метод вместо LFF вместо LoadFromFile, вы рискуете в скором времени забыть, что он означает.

Примечание

Но нет правил без исключений: например, в компоненте TDataSet есть свойство EOF, расшифровывающееся как "End Of File" – "конец файла". Но это все же исключение, имеющее свою историю: с ранних версий языка Pascal существует одноименная функция EOF(), позволяющая определить находится ли указатель в конце файла. Очевидно, разработчики VCL сочли логичным так же назвать и свойство.

Совет

Рекомендации по наименованию методов во многом применимы и для других атрибутов класса – полей, свойств и событий.

По степени видимости методы могут быть размещены с любой секции от private до public.

Примечание

Если поместить метод в разделе published, то, не смотря на то, что компилятор не выдаст сообщения об ошибке, "опубликованного" метода получить не удастся. В результате получится просто публичный (public) метод.

С наименованием методов все ясно. Разберемся теперь собственно с их реализацией.

2.2. Реализация методов

Пример 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.4.

Рассмотрим пример, иллюстрирующий приемы работы с методами.


Пример 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. Это не разрушит полиморфизма метода;

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