Self, Sender и events - ЧаВо

Источник: delphikingdom
Левко

Автор: Левко, Королество Delphi

Идея написать эту статью возникла у меня после появления на Круглом столе очередного вопроса вида "У меня есть компонент, у него есть событие On... Что ему нужно присвоить чтоб оно работало?" После недолгого наблюдения за тем, как доблесные рыцари упорно убеждали яросно сопротивляющегося автора изучить основы языка, решил я немного им помочь ;o) Сразу предупреждаю, что рассчитана статья на новичков. Итак, приступим.

Что такое события (events)? Даже начинающие программисты сталкиваются с этим явлением природы, чуть ли начав изучать Дельфи (а как иначе-то, если Дельфи само их и провоцирует, подсовывая при запуске формочку? ;-) ): все видели в инспекторе обьектов закладку Events, а там - список событий, которые есть у компонента. Самые любопытные лезли в недра компонента и натыкались на конструкции вида

property OnClick: TNotifyEvent read FOnClick write FOnClick;

и дальше

TNotifyEvent = procedure (Sender: TObject) of object;

Хм... Вроде ничего особенного - обычный процедурный тип. Смущает только конструкция of object. И смущает правильно.

Если обычный процедурный тип - это указатель на процедуру (т.е. то что мы видим при его обьявлении - параметры, тип результата, модель вызова) - то оно на самом деле и есть, то приписка 'of object' указывает, что переменным такого типа можно присвоить только указатели на метод с соответствующими параметрами, принадлежащий какому-нибудь классу. И здесь уже начинается собственно шоу, так как при вызове метода класса ему передаются не только те параметры, указанные при обьявлении метода :-)

Давайте создадим консольное приложение такого вида (оно нам еще не раз пригодится):

program A;
{$APPTYPE CONSOLE}
uses SysUtils;
type
  CTestClass = class(TObject)
  private

    FText: string;
  public
    property Text: string read FText write FText;
    procedure DoEvent;
  end;


{ CTestClass }

procedure CTestClass.DoEvent;
begin
  // Здесь пока пусто
end;

var t: CTestClass;
begin

  t:=CTestClass.Create; // Создаем главного персонажа нашего шоу
  // здесь пока тоже ничего
  t.Free;
  readln;
end.

Впишем в тело DoEvent строку writeln(Self.FText), а в тело программы после создания t строки

t.Text:='Some text';
  writeln(IntToHex(Cardinal(t),8));
  t.DoEvent;

и поставим на вторую строку брейкпойнт.

Когда выполнение остановтся, открываем View->Debug Windows->CPU (Ctrl+Alt+C) и... внимательно смотрим на вместимое регистров, в частности EAX, и при необходимости выписываем его на бумажку :-) Теперь смотрим (тоже очень внимательно) на наше консольное окно, куда накануне был выведен адрес обьекта t. Если эти два числа у вас не совпали - тогда немедленно начинайте переделывать пример сначала :-)

И так, числа совпали. Что это значит? При вызове функции Дельфи по умолчанию генерит код, который часть параметров передает через регистры процессора. Так что в EAX у нас какой-то параметр. Но какой? И откуда он взялся, ведь DoEvent обьявлена без параметров? Ответом на вопрос есть тот факт, что в EAX оказался адрес обьекта, которому пренадлежит метод DoEvent, в нашем случае это t. И - внимание - этот адрес - это и есть Self!!! Вывод из примера: Self - это один из параметров методов обьектов, самый первый в списке, который Дельфи передает в метод автоматически при его вызове. Внутренняя реализация этого механизма полностью скрыта от программиста, поэтому он может спокойно использовать Self внутри методов, не заботясь о том, как он туда попадает, и быть абсолютно уверенным, что с помощью Self он получит доступ к полям и другим методам класса-владельца вызванного метода (извините что так запутанно, знаю, что педагог из меня никудышний; попробуйте перечитать это предложение еще раз, если что-то непонятно). Также добавлю, что благодаря этой особенности обычные процедурные типы несовместимы на указатели на методы, даже если у них одинаковый набор параметров. Именно это ставит новичков в тупик и вынуждает испытывать нервы и выдержку отвечающих на их вопросы на формумах :D

В общих чертах с Self'ом выяснили. Теперь на очереди Sender. Здесь всё намного проще. Переделаем немного наш пример:

program B;
{$APPTYPE CONSOLE}

uses SysUtils;

type
  CTestClass = class(TObject)
  private
    FText: string;
  public

    property Text: string read FText write FText;
    procedure DoEvent(Sender: TObject);
  end;


{ CTestClass }

procedure CTestClass.DoEvent(Sender: TObject);
begin
  writeln(Self.FText);
  if Assigned(Sender) and (Sender is CTestClass)
    then writeln(CTestClass(Sender).Text)
    else writeln('Sender is nil or Sender is not CTestClass');
  writeln;

end;

var t,t2: CTestClass;

begin
  t:=CTestClass.Create;
  t.Text:='text';
  t2:=CTestClass.Create;
  t2.Text:='text 2';
  //
  t.DoEvent(t2);
  t.DoEvent(nil);
  //

  t.Free; t2.Free;
  readln;
end.

Что мы видим на экране? Первая строка - это вместимое свойства Text обьекта, которому пренадлежит метод DoEvent; так как мы вызываем DoEvent обьекта t, то строка 'text' - вполне прогнозируемое явление. Во второй строке печатается Text обьекта, переданого в качестве параметра Sender (заметьте, что на этот раз нам пришлось его указать явно), или сообщение о том, что в качестве этого параметра было передано некорректное значение. При обьявлении указателей на обработчики событий Sender можно не указывать; и вообще там могут быть любые параметры, которые разработчик считает нужными. Но в большинстве случаев в качестве Sender'а передают указатель на обьект, вызвавший событие, что позволяет внутри метода определить, кто его вызвал. Опять смотрим пример:

program C;
{$APPTYPE CONSOLE}
uses SysUtils;


type
  TEvent = procedure(Sender: TObject; Param: string) of object;

  CTestClass = class(TObject)
  private
    FText: string;
    FEvent: TEvent;
  public

    property Text: string read FText write FText;
    property OnEvent: TEvent read FEvent write FEvent;
    procedure DoEvent;
  end;

  CDummy = class(TObject)
  public

    procedure MyEvent(Sender: TObject; Param: string); 
  end;

{ CTestClass }

procedure CTestClass.DoEvent;
begin
  if Assigned(FEvent) then FEvent(Self,Self.FText);

end;

var t,t2: CTestClass;
    d: CDummy;

{ CDummy }

procedure CDummy.MyEvent(Sender: TObject; Param: string);
begin
  writeln(IntToHex(Cardinal(Sender),8));
  writeln(Param);
  writeln;

end;

begin
  t:=CTestClass.Create;
  t.Text:='text';
  t2:=CTestClass.Create;
  t2.Text:='test 2';
  //
  d:=CDummy.Create;
  t.OnEvent:=d.MyEvent;
  t2.OnEvent:=d.MyEvent;
  //
  t.DoEvent;
  t2.DoEvent;
  //

  t.Free; t2.Free; d.Free;
  readln;
end.

На экране печатаются адреса Sender'ов, передаваемых при вызове определенного нами события. Если кому интересно - можна также напечатать адреса t и t2 и сравнить. В этом примере мы уже видим пример обьявления собственного события, классический метод его вызова (те же любопытные могут на досуге посмотреть исходники VCL и поискать там похожие фрагменты). И - внимание - здесь же содержится первый, классический, вариант назначения событию собственного обработчика. Он состоит в том, чтоб создать класс, не несущий смысловой нагрузки, но (что очень важно и собственно нам и надо) содержащий методы, совместимые с определенными событиями. После создания экземпляра такого класса можно свободно назначать его методы в качестве обработчиков. Такой подход идеологически самый правильный, и именно он рекомендуется к использованию практически во всех случаях.

Но есть в этом способе одна небольшая неудобность. И вот какая. Наш подставной класс не несет никакой смысловой нагрузки (ведь мы его таким спроектировали); более того, скорее всего он будет содержать кучу методов для обработки самых разных событий множества различных обьектов. Плюс при инициализации нужно создать хотя бы один экземпляр этого класса, при завершении работы - не забыть уничтожить. И ради чего всё это? Ради одного факта приналежности процедуры обьекту? Возникает желание искать другие пути написания обработчиков. Я предлагаю еще два.

Первый. Если Self нам не нужен, тогда самое время вспомнить о методах класса. Их можно вызывать, не создавая экземпляра класса, и в тоже время они пренадлежат классу. Первый факт вполне удовлетворяет нас, второй - компилятор :-) Исходя из сказанного, переделаем наш пример:

program A;
{$APPTYPE CONSOLE}

uses SysUtils;

type
  TEvent = procedure(Sender: TObject; Param: string) of object;

  CTestClass = class(TObject)
  private

    FText: string;
    FEvent: TEvent;
  public
    property Text: string read FText write FText;
    property OnEvent: TEvent read FEvent write FEvent;
    procedure DoEvent;
  end;

  CDummy = class(TObject)
  public

    class procedure MyEvent(Sender: TObject; Param: string);
  end;

{ CTestClass }

procedure CTestClass.DoEvent;
begin

  if Assigned(FEvent) then FEvent(Self,Self.FText);
end;

var t,t2: CTestClass;

{ CDummy }

class procedure CDummy.MyEvent(Sender: TObject; Param: string);

begin
  writeln(IntToHex(Cardinal(Sender),8));
  writeln(Param);
  writeln;
end;

begin
  t:=CTestClass.Create;
  t.Text:='text';
  t2:=CTestClass.Create;
  t2.Text:='test 2';
  //

  t.OnEvent:=CDummy.MyEvent;
  t2.OnEvent:=CDummy.MyEvent;
  //
  t.DoEvent;
  t2.DoEvent;
  //
  t.Free; t2.Free;
  readln;
end.

Здесь мы уже не создавали экземпляра CDummy, так как он нам теперь совсем не нужен. Правда, если в теле метода будет обращение к полям этого класса, компилятор скажет вам всё, что о вас думает ;o) И сделает правильно, потому что он не знает, что передавать в качестве Self, следовательно, использовать этт параметр нельзя.

Многие начинающие программисты пытаются назначать в качестве обработчиков событий статичные процедуры. Почему им это не удается - вспоминалось немного выше. Правда, это не значит, что статические процедуры нельзя использовать в такой ипостаси. Итак, дамы и господа...

Способ второй. Мы уже знаем, что обычная процедура и метод отличаются отсутсвием или наличием скрытого параметра Self. А что, если определить его в статической процедуре?

procedure MyEvent(Self, Sender: TObject; Param: string);
begin
  writeln(IntToHex(Cardinal(Sender),8));
  writeln(Param);
  writeln;
end;

Но не тут-то было. Любые попытки назначить эту процедуру в качестве обработчика вызывали отчаяное сопротивление компилятора в виде Invalid typecast. Но решение и здесь оказалось тривиальным:

@t.OnEvent:=@MyEvent;

Теперь переработанный пример:

program A;
{$APPTYPE CONSOLE}
uses SysUtils;


type
  TEvent = procedure(Sender: TObject; Param: string) of object;

  CTestClass = class(TObject)
  private
    FText: string;
    FEvent: TEvent;
  public

    property Text: string read FText write FText;
    property OnEvent: TEvent read FEvent write FEvent;
    procedure DoEvent;
  end;


{ CTestClass }

procedure CTestClass.DoEvent;
begin
  if Assigned(FEvent) then FEvent(Self,Self.FText);
end;


var t,t2: CTestClass;

{ CDummy }

procedure MyEvent(Self, Sender: TObject; Param: string);
begin
  writeln(IntToHex(Cardinal(Sender),8));
  writeln(Param);
  writeln;

end;

begin
  t:=CTestClass.Create;
  t.Text:='text';
  t2:=CTestClass.Create;
  t2.Text:='test 2';
  //
  @t.OnEvent:=@MyEvent;
  @t2.OnEvent:=@MyEvent;
  //
  t.DoEvent;
  t2.DoEvent;
  //

  t.Free; t2.Free;
  readln;
end.

И замечания к этому способу. Self использовать по-прежднему нельзя - в общем случае его значение неопределено, правда, во время моих опытов там всегда был nil, но на это не стоит рассчитывать в больших проектах. Также нужно быть очень осторожным и не пропустить первый параметр - он не обязательно должен быть типа TObject, но его размер должен быть SizeOf(Pointer), иначе возникнут ошибки при обращении к другим параметрам. И наконец рекомендую использовать его только когда совсем нет возможности сделать процедуру методом, т.к. он немного противоречит концепциям, закладенным в ООП.

P.S. Это моя первая статья, и рассчитана она на людей, которые (почему-то) так часто задают простые вопросы на эту тему. Примеры не прилагаю - в тексте есть всё необходимое. Примеры проверены и успешно работают под Delphi 7. С удовольствием приму любые коментарии и замечания, лишь бы они относились к сути статьи или стиля моего изложения. Благодарю за внимание :-)


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=21337