понедельник, 28 марта 2016 г.

Почему тип анонимных функций не всегда полезен или о проблема присвоения reference to типу неанонимных функций

Сегодня снова об особенностях анонимных функций.
Два предыдущих поста:
  1. Просто баг в коде или особенность счетчика ссылок в замыканиях
  2. Немного про замыкание ( Одно замыкание на несколько анонимных функций в одном контексте)

  У типа анонимный функций есть замечательная возможность. В качестве обработчика можно присваивать не только анонимные функции, но и обычные методы и статические функции. Казалось бы, нет причин почему не использовать тип reference to всегда.
Однако есть ситуации, когда reference to тип может сыграть злую шутку. И я сейчас не про вопрос производительности, о котором я думаю большинство осведомлено. 
Проблема может возникнуть при передаче обычной процедуры или метода в параметр с анонимным типом. Покажем на примере. 
Наиболее распространенный вариант использования процедурных типов в реализации подписки на события.
  Пусть есть какой-нибудь список EventHandlers, и конечно же, как у правильных хипстеров он будет параметризован типом анонимной функций TList<TProc>. 
Заведем два метода для возможности подписаться на событие и отписаться от него:  Subscribe(AHandler: TProc) и Unsubscribe(AHandler: TProc): Boolean соответственно. Однако, крайне вероятно, вас постигнет неудача если в качестве обработчика вы используете неанонимную функцию.  По крайне мере отписаться вы уже не сможете. 

Вот код примера:
program ReferenceToProc;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils, System.Generics.Collections;
var
  eventHandlers: TList<TProc>;

  procedure Subscribe(AHandler: TProc);
  begin
    eventHandlers.Add(AHandler);
  end;

  function Unsubscribe(AHandler: TProc): Boolean;
  begin
    Writeln(eventHandlers.IndexOf(AHandler));  // 0
    Result := eventHandlers.Remove(AHandler) >= 0;
  end;

  procedure HandlerProc;
  begin
    Writeln('Вызов статической процедуры');
  end;

var
  proc: TProc;
begin
   eventHandlers:= TList<TProc>.Create;

   proc := procedure()
     begin
       Writeln('Вызов анонимной функции')
     end;

   Subscribe(proc); // Подписываемся анонимной функцией
   Writeln(Unsubscribe(proc)); // Метод вернул True, Успех:)

   Subscribe(HandlerProc); // Подписываемся не анонимной функцией
   Writeln(Unsubscribe(HandlerProc)); // Метод вернет False Fail:(
   FreeAndNil(eventHandlers);
   readln;
end.

  Вызов Unsubscribe  в строке  Writeln(Unsubscribe(HandlerProc)); вернет False. Причина впрочем прозаична, вызов метода HandlerProc заворачивается в анонимную функцию и каждый раз новую. Потому функция, которую подписали на событие, будет отличаться от функции, которую пытаемся отписать.

  Казалось бы, простая задача - реализовать список с обработчиками и методами подписки и отписки превращается не в самую тривиальную.

  Получается, что хранить обработчики лучше как TMethod, но если все же хотим уметь подписывать универсальные обработчики (анонимные и неанонимные), тогда придется для каждого типа писать свою логику сохранения в список и вызова обработчиков. Это будут либо overload методы, либо будем использовать Rtti  и методы параметризированные типом.
  TEvent = class
    events: TList<TMethod>;
    class procedure Subscribe(AProc: TProc); overload;
    class procedure Subscribe(AProc: TProcedure); overload;
    class procedure Subscribe<T>(AProc: T); overload;
  end;

  А для преобразования анонимного метода в TMethod потребуется сочинять что то вроде того, что написано в Spring4D в Springs.Events.pas:

procedure MethodReferenceToMethodPointer(const AMethodReference; const AMethodPointer);
type
  TVtable = array[0..3] of Pointer;
  PVtable = ^TVtable;
  PPVtable = ^PVtable;
begin
  // 3 is offset of Invoke, after QI, AddRef, Release
  PMethod(@AMethodPointer).Code := PPVtable(AMethodReference)^^[3];
  PMethod(@AMethodPointer).Data := Pointer(AMethodReference);
end;

function TEvent.Cast(const handler): TMethod;
begin
  if fTypeInfo.Kind = tkInterface then
    MethodReferenceToMethodPointer(handler, Result)
  else
    Result := PMethod(@handler)^;
end;


Комментариев нет:

Отправить комментарий