Delphi SysUtils.Supports неожиданно возвращает значение true

Я делаю eventPublisher на основе примера документации Spring4d.

Разница в том, что подписчики должны явно подписываться на события.

Я хочу инициировать их процедуру Handle в зависимости от того, реализуют ли они интерфейс IEventHandler<TEventType>.

Когда входящее событие публикуется, я нахожу ссылку типа IEventHandler<TEventType>, используя имя класса события и TType.FindType('IEventHandler<TEvent1>') Spring4d.

Затем я перебираю своих подписчиков (объекты, реализующие интерфейс IEventHandler) и проверяю, поддерживает ли он, например, тип IEventHandler.

Проблема в том, что метод Supports возвращает true, даже если подписчик не реализует интерфейс.

Кроме того, я попытался перечислить интерфейсы, скажем, типа TMyEventHandler2. Он содержит IEventHandler<TEvent2> ??

Я полагаю, что это связано с ограничением, когда IEventHandler<TEvent2> и IEventHandler<TEvent1> используют один и тот же GUID.

Есть ли обходной путь для этого?

Используя эти классы и интерфейсы:

TEvent1 = class(TObject)
end;

TEvent2 = class(TObject)
end;

IEventHandler = interface(IInvokable)
[guid]
procedure Handle(aEvent : TObject);
end;

IEventHandler<T : class> = interface(IEventHandler)
[guid]
procedure Handle(aEvent : T);
end;

TMyEventHandler1 = class(TObject, IEventHandler, IEventHandler<TEvent1>)
public 
procedure Handle(AEvent : TObject); overload;
procedure Handle(AEvent : TEvent1); overload;
end;

TMyEventHandler2 = class(TObject, IEventHandler, IEventHandler<TEvent2>)
public 
procedure Handle(AEvent : TObject); overload;
procedure Handle(AEvent : TEvent2); overload;
end;

TEventPublisher = class(TObject)
public
  fSubscribers : IList<TValue>;
  procedure Subscribe(aSubscriber : TValue);  // Simply adds the subscriber to the list of subscribers
  procedure Publish(aEvent : TObject); // Publishes an event to the subscribers
end;

procedure TEventPublisher.Publish(const event: TObject; ownsObject: Boolean = True);
const
  IEventSubscriberName = 'IEventSubscriber<*>';
var
  consumerTypeName: string;
  consumerType    : TRttiType;
  intfType        : TRttiInterfaceType;
  subscriber      : TValue;
  subscribed      : IInterface;
  lEventSubscriber: IEventSubscriber;
  lIntfs          : IReadOnlyList<TRttiInterfaceType>;
begin

  consumerTypeName := StringReplace(IEventSubscriberName, '*', GetQualifiedClassName(event), []);
  consumerType     := TType.FindType(consumerTypeName);
  intfType         := consumerType as TRttiInterfaceType;

  for subscriber in fSubscribers do
  begin

    lIntfs := TType.GetType(subscriber.AsObject.ClassInfo).GetInterfaces();

    // lIntfs for TMyEventHandler2 containts IEventHandler<TEvent1> ???

    if Supports(subscriber.AsObject, intfType.GUID, subscribed) then
      if Supports(subscriber.AsObject, IEventSubscriber, lEventSubscriber) then
      begin
        intfType.GetMethod('Handle').Invoke(TValue.From(@subscribed, intfType.Handle), [event])
      end;
  end;

  if ownsObject then
    event.Free;
end;


lEventPublisher := TEventPublisher.Create;
lEventPublisher.Subscribe(TMyEventHandler1.Create);
lEventPublisher.Subscribe(TMyEventHandler2.Create);
lEventPublisher.Publish(TEvent1.Create); // Will both trigger TMyEventHandler1.Handle and TMyEventHandler2.Handle. Why ??

person Ludovic C    schedule 28.02.2018    source источник
comment
Да, это ожидаемо и является следствием единого руководства   -  person David Heffernan    schedule 01.03.2018
comment
Да, это ожидаемо, как говорит @David, и нет, для этого нет обходного пути, кроме как не использовать общие интерфейсы в таких ситуациях.   -  person Rudy Velthuis    schedule 01.03.2018


Ответы (1)


Это происходит потому, что если вы поместите guid в универсальный интерфейс, каждая специализация этого интерфейса будет иметь один и тот же guid независимо от его параметра универсального типа.

Обычно я решаю эту проблему, помещая способ предоставить информацию об этом в интерфейс (например, Spring.Collections.IEnumerable имеет свойство ElementType для получения фактического типа IEnumerable<T>).

Таким образом, реализация будет выглядеть так:

program GenericEventPublisher;

{$APPTYPE CONSOLE}

uses
  Spring,
  Spring.Collections,
  System.SysUtils;

type
  IEventHandler = interface
    ['{2E4BD8F4-4EB8-4B33-84F4-B70F42EF9208}']
    procedure Handle(const event: TObject);
  end;

  IEventHandler<T: class> = interface
    ['{82B7521E-D719-4051-BE2C-2EC449A92B22}']
    procedure Handle(const event: T);
    function GetHandledClass: TClass;
  end;

  IEventPublisher = interface
    ['{2A460EF0-AE27-480F-ACEA-1B897F2DE056}']
    procedure Subscribe(const subscriber: IEventHandler);
    procedure Publish(const event: TObject; ownsObject: Boolean = True);
  end;

  TEventHandlerBase<T: class> = class(TInterfacedObject, IEventHandler, IEventHandler<T>)
  private
    function GetHandledClass: TClass;
    procedure Handle(const event: TObject); overload;
  public
    procedure Handle(const event: T); overload; virtual; abstract;
  end;

  TEvent1 = class
  end;

  TEvent2 = class
  end;

  TMyEventHandler1 = class(TEventHandlerBase<TEvent1>)
  public
    procedure Handle(const event: TEvent1); override;
  end;

  TMyEventHandler2 = class(TEventHandlerBase<TEvent2>)
  public
    procedure Handle(const event: TEvent2); override;
  end;

  TEventPublisher = class(TInterfacedObject, IEventPublisher)
  private
    fSubscribers: IList<IEventHandler>;
  public
    constructor Create;
    procedure Subscribe(const subscriber: IEventHandler);
    procedure Publish(const event: TObject; ownsObject: Boolean = True);
  end;

{ TEventPublisher }

constructor TEventPublisher.Create;
begin
  fSubscribers := TCollections.CreateList<IEventHandler>;
end;

procedure TEventPublisher.Publish(const event: TObject; ownsObject: Boolean);
var
  subscriber: IEventHandler;
  eventSubscriber: IEventHandler<TObject>;
begin
  for subscriber in fSubscribers do
    if Supports(subscriber, IEventHandler<TObject>, eventSubscriber)
      and (eventSubscriber.GetHandledClass = event.ClassType) then
        eventSubscriber.Handle(event);

  if ownsObject then
    event.Free;
end;

procedure TEventPublisher.Subscribe(const subscriber: IEventHandler);
begin
  fSubscribers.Add(subscriber)
end;

{ TEventHandlerBase<T> }

function TEventHandlerBase<T>.GetHandledClass: TClass;
begin
  Result := T;
end;

procedure TEventHandlerBase<T>.Handle(const event: TObject);
begin
  Assert(event is T);
  Handle(T(event));
end;

{ TMyEventHandler1 }

procedure TMyEventHandler1.Handle(const event: TEvent1);
begin
  Writeln(event.ClassName, ' handled by ', ClassName);
end;

{ TMyEventHandler2 }

procedure TMyEventHandler2.Handle(const event: TEvent2);
begin
  Writeln(event.ClassName, ' handled by ', ClassName);
end;

var
  eventPublisher: IEventPublisher;
begin
  eventPublisher := TEventPublisher.Create;
  eventPublisher.Subscribe(TMyEventHandler1.Create);
  eventPublisher.Subscribe(TMyEventHandler2.Create);
  eventPublisher.Publish(TEvent1.Create);
  eventPublisher.Publish(TEvent2.Create);
end.

Поскольку для интерфейса существует ограничение класса, мы можем быть уверены, что интерфейсы бинарно совместимы независимо от типа T (поскольку они могут быть только объектами). Кроме того, использование базового типа для универсального обработчика событий позволяет сократить объем дополнительного кода. Он просто перенаправляет не универсальный метод Handle на общий, который должен быть реализован в конкретной реализации.

Кроме того, поскольку базовый класс реализует оба интерфейса, нам не нужно хранить обработчики в списке TValue, но мы можем использовать неуниверсальный тип интерфейса и легко получать к ним доступ без необходимости использования RTTI.

Теперь метод Publish использует небольшой трюк, вызывая Support с IEventHandler<TObject> — поскольку eventSubscriber относится к этому типу, мы можем передать параметр event его методу Handle, который оказывается правильным — это из-за бинарной совместимости, которую я объяснил ранее, потому что мы просто имеем дело с разными классами, поскольку тип Т-истории был бы совершенно другим, если бы у нас не было этого ограничения класса.

person Stefan Glienke    schedule 02.03.2018