3. Методы и события


3.1. Введение
3.2. Методы
3.2.1. Объявление методов в интерфейсной части класса
3.2.2. Реализация методов
3.3. События
3.3.1. Интерфейсная часть объявления события
3.3.2. Реализация вызова обработчика события
3.3.3. События пользовательского типа
3.4. Краткие итоги лекции
3.5. Вопросы для самопроверки

3.1. Введение

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

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

3.2. Методы

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

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

Замечание

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

Внимание

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

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

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

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

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

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

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

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

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

Подсказка

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

Внимание

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

Замечание

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

Подсказка

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

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

Замечание

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

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

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

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

Пример 4.

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


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

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

3.3. События

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

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

Рассмотрим как это устроено.

Для того чтобы вызвать обработчик события необходимо запомнить его адрес, перечень и темы передаваемых параметров. Вся эта информация запоминается в приватном поле класса объектного типа.

Сам адрес обработчика события устанавливается через свойство. Так что событие класса фактически является свойством объектного типа.

Замечание

Если событие это – свойство, то может возникнуть вопрос "Каким же образом Delphi определяет, какие свойства объекта показывать на странице “Properties” («Свойства»), а какие – на странице “Events” («События»)?" Для определения страницы инспектор объектов использует информацию о типе свойств. На странице “Events” появляются только свойства типа «указатель на метод».

Пример 8. Фрагмент кода библиотеки VCL, иллюстрирующий объявление событий.

type
  TControl = class(TComponent)
  private
…
    FOnDblClick: TNotifyEvent; 
…
  protected
    property OnDblClick: TNotifyEvent read FOnDblClick write FOnDblClick; 
…
end;	 	

3.3.1. Интерфейсная часть объявления события

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

Объявление типа «указатель на метод» напоминает объявление процедурного типа с добавлением спецификатора of object.

Пример 9. Так, например, простейший тип ссылки на метод TNotifyEvent объявлен следующим образом:

  TNotifyEvent = procedure(Sender: TObject) of object;

Замечание

Указатель на метод может быть и функцией.

В библиотеке VCL принято первым параметром всех делегируемых методов передавать ссылку на сам объект, вызывающий событие. По соглашению этот первый параметр называется Sender и имеет тип TObject. Он позволяет определить, какой именно объект вызвал обработчик события.

3.3.2. Реализация вызова обработчика события

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

Пример 10. Типичный код метода диспетчеризации выглядит следующим образом:

procedure TControl.DblClick;
begin
  if Assigned(FOnDblClick) then FOnDblClick(Self);
end; 

Логика работы приведенного метода следующая: сначала с помощью функции Assigned проверяется неравенство ссылки на метод значению Nil. Только в том случае, если адрес обработчика события отличен от nil, его код выполняется. Обратите внимание, что первым параметром обработчику передается ссылка на сам объект с помощью неявной ссылки Self.

Замечание

Методы диспетчеризации (а их может быть более одного на одно событие), как правило, объявляются как защищенные (protected) методы того класса, которому они принадлежат. Делается это для того, чтобы логика их работы могла быть переопределена в классах-наследниках.

3.3.3. События пользовательского типа

В библиотеке 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, закрытого точкой с запятой.

Замечание

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

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

3.4. Краткие итоги лекции

  1. Методы являются важнейшими атрибутами класса, реализующими его функциональность;

  2. Статические методы следует объявлять в классе для тех действий, которые не будут переопределяться в потомках;

  3. Виртуальные и динамические методы следует переопределять в потомках с использованием ключевого слова override в интерфейсной части и вызовом метода предка с использованием ключевого слова inherited;

  4. События представляют собой свойства типа "ссылка на метод" и хранят адрес процедуры или функции приложения, реализующей делегируемый метод.

3.5. Вопросы для самопроверки

  1. Что такое метод?

  2. Какие особенности имеет абстрактный метод?

  3. Какими правилами следует руководствоваться при выборе названия методу?

  4. Может ли метод быть опубликованным (published)?

  5. Как правильно переопределить виртуальный (динамический) метод?

  6. Как вызвать метод предка?

  7. Что такое событие?

  8. Как реализовать метод диспетчеризации события?

  9. Сколько для одного события может быть методов диспетчеризации?

  10. Как объявить пользовательский тип "ссылка на метод"?

  11. Как упреждающе объявить класс?