Delphi: 测试事件处理程序分配

10

如果构造函数中没有分配事件处理程序,我想要分配一个事件处理程序。因此,我想在析构函数中删除最终分配的事件处理程序。我编写的代码如下,但无法编译。

constructor TSomeControl.Create(Panel: TPanel);
begin
  inherited Create;
  FPanel := Panel;
  if not Assigned(FPanel.OnResize) then
    FPanel.OnResize := HandlePanelResize;
end;

destructor TSomeControl.Destroy;
begin
  if @FPanel.OnResize = @HandlePanelResize then // [dcc32 Error] E2036 Variable required
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

如何适当地测试它?我知道一个解决方案是使用一个变量来记录是否已经赋值了OnResize。但我不想采用这个解决方案。


3个回答

13

不需要编写任何自定义代码,因为您可以使用已经存在的比较器来自Generics.Defaults:

destructor TSomeControl.Destroy;
begin
  if Assigned(FPanel) and TEqualityComparer<TNotifyEvent>.Default.Equals(
    FPanel.OnResize, HandlePanelResize) then
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

TEqualityComparer 也适用于 Event<T>,像下面这样的代码也可以工作:function TForm1.Button1OnClickIsMultiCast: Boolean; begin Result := TEqualityComparer.Default.Equals( Button1.OnClick, FMultiCastEvents); end; - Edwin Yip
@EdwinYip 这是因为 Event<T> 有一个隐式转换运算符到 T ;) - Stefan Glienke

6
这一点变得复杂,因为OnResize是一个属性而不是一个变量。而且很难直接引用方法,否则编译器会认为你要调用该方法。这就是Pascal的方便之处,允许您在不使用括号的情况下调用过程的缺点所在。
所有这些使得在一行代码中完成此操作相当困难。据我所见,您需要像这样做:
destructor TSomeControl.Destroy;
var
  Method1, Method2: TNotifyEvent;
begin
  if Assigned(FPanel) then
  begin
    Method1 := FPanel.OnResize;
    Method2 := HandlePanelResize;
    if TMethod(Method1) = TMethod(Method2) then
      FPanel.OnResize := nil;
  end;
  FPanel := nil;
  inherited;
end;

这依赖于现代 Delphi 中的 TMethod 记录,它包括一个重载的等号运算符来使得 = 测试工作。

如果我需要多次执行此操作,我会将其封装成通用方法。可能看起来像这样:

type
  TEventComparer = class
    class function Equal<T>(const lhs, rhs: T): Boolean; static;
  end;

class function TEventComparer.Equal<T>(const lhs, rhs: T): Boolean;
begin
  Assert(SizeOf(T)=SizeOf(TMethod));
  Result := TMethod((@lhs)^)=TMethod((@rhs)^);
end;

您需要这样调用它:
destructor TSomeControl.Destroy;
begin
  if Assigned(FPanel) and TEventComparer.Equal<TNotifyEvent>(FPanel.OnResize, 
    HandlePanelResize) then
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

这件事突出了一个问题,那就是可用的通用约束并不允许您将类型限制为方法指针。因此,需要进行基本的检查,即 T 的大小与方法的大小相同。但是,这并不能提供太多的安全性。您可以传递 Int64 或者 Double 来调用此方法。我很想看到有人能否提出更简洁的方案。


您的代码完美运作。我很想知道是否可以将该代码编写为一些通用的辅助函数?方法类型可能不仅限于 TNotifyEvent。我已经尝试,但是当我将其包装为通用的辅助函数时,编译器会抱怨 TMethod(Method1) = TMethod(Method2) - stanleyxu2005
非常棒的技巧。我的错误是使用了 lhs 而不是 (@lhs)^ - stanleyxu2005
类似这样的代码:var p: PTypeInfo; begin p := TypeInfo(T); Assert(p.Kind = tkMethod); - LU RD
@StefanGlienke 是的,这很不错。特别是现在Emba修复了他们方法比较器中的错误! - David Heffernan
@DavidHeffernan 由于Stefan的解决方案更本地化,我接受了他的答案。希望你能同意。 - stanleyxu2005
显示剩余2条评论

1

没有必要使用Generics.Defaults或任何泛型。在System单元中声明了TMethod记录,因此这可能是最简单的方法:

destructor TSomeControl.Destroy;
var
  Event: TNotifyEvent;
begin
  Event := HandlePanelResize;
  if TMethod(FPanel.OnResize).Code = Addr(Event) then
    FPanel.OnResize := nil;
  FPanel := nil;
  inherited;
end;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接