【问题标题】:How to check if a reference to procedure is nil?如何检查对程序的引用是否为零?
【发布时间】:2015-03-16 09:55:35
【问题描述】:

在以下示例代码中,对AssertTestObj() 的调用会导致访问冲突。

Project InvokeTest2.exe 引发异常类 $C0000005 并带有消息 '0x00000000 处的访问冲突:读取地址 0x00000000'。

调试时我可以看到TSafeCall<T>.Invoke() 中的Assigned(NotifyProc) 测试没有按预期工作 - 因此Invoke() 尝试执行NotifyProc 这是nil,从而导致访问冲突。

任何想法为什么会失败以及如何解决它?

program InvokeTest2;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  TSafeCall<T> = class
  public
    type
      TNotifyProc = reference to procedure (Item: T);
    class procedure Invoke(NotifyProc: TNotifyProc; Item: T); overload;
  end;

  TOnObj = procedure (Value: String) of object;

{ TSafeCall<T> }

class procedure TSafeCall<T>.Invoke(NotifyProc: TNotifyProc; Item: T);
begin
  if Assigned(NotifyProc) then
    NotifyProc(Item);
end;

procedure AssertTestObj(OnExceptionObj_: TOnObj; Value_: String);
begin
  TSafeCall<String>.Invoke(OnExceptionObj_, Value_);
end;

begin
  try
    TSafeCall<String>.Invoke(nil, 'works as expected');

    AssertTestObj(nil, 'this causes an access violation!');
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

【问题讨论】:

    标签: delphi delphi-xe3


    【解决方案1】:

    这是一个编译器错误。这是我的简化复制:

    {$APPTYPE CONSOLE}
    
    type
      TProc = reference to procedure;
      TOnObject = procedure of object;
    
    procedure Invoke(Proc: TProc);
    begin
      if Assigned(Proc) then
        Proc();
    end;
    
    procedure CallInvokeOnObject(OnObject: TOnObject);
    begin
      Invoke(OnObject);
    end;
    
    begin
      Invoke(nil); // succeeds
      CallInvokeOnObject(nil); // results in AV
    end.
    

    您可能想知道我为什么要简化。您的代码很好地再现了这个问题。但是,我想让它尽可能简单,以便我真的可以确定问题是我所相信的。所以我删除了泛型和类。

    现在,使用Assigned 的测试是正确的。您期望它会按照您的意图运行是正确的。问题是当编译器生成代码从CallInvokeOnObject调用Invoke时,它需要将object的方法包装在一个引用过程接口中。为了正确执行此操作,需要测试是否分配了 object 的方法。如果不是,则不应创建包装器接口,并且应将Invoke 传递给nil

    编译器无法做到这一点。它无条件地将对象的方法包装在引用过程接口中。您可以在为CallInvokeOnObject 发出的代码中看到这一点。

    Project1.dpr.16: begin // 这是 CallInvokeOnObject 的开始 004064D8 55推ebp 004064D9 8BEC 移动 ebp,esp 004064DB 6A00 推 00 美元 004064DD 53 推 ebx 004064DE 33C0 xor eax,eax 004064E0 55 推送 ebp 004064E1 683B654000 推 $0040653b 004064E6 64FF30 推双字 ptr fs:[eax] 004064E9 648920 mov fs:[eax],esp 004064EC B201 移动 dl,01 美元 004064EE A1F4634000 移动 eax,[$004063f4] 004064F3 E8DCDAFFFF 调用 TObject.Create 004064F8 8BD8 mov ebx,eax 004064FA 8D45FC lea eax,[ebp-$04] 004064FD 8BD3 mov edx,ebx 004064FF 85D2 测试 edx,edx 00406501 7403 jz $00406506 00406503 83EAF8 sub edx,-$08 00406506 E881F2FFFF 调用@IntfCopy 0040650B 8B4508 移动 eax,[ebp+$08] 0040650E 894310 mov [ebx+$10],eax 00406511 8B450C 移动 eax,[ebp+$0c] 00406514 894314 mov [ebx+$14],eax Project18.dpr.17:调用(OnObject); 00406517 8BC3 mov eax,ebx 00406519 85C0 测试 eax,eax 0040651B 7403 jz $00406520 0040651D 83E8E8 sub eax,-$18 00406520 E8DFFDFFFF 调用调用

    TObject.Create 的调用是将对象的方法包装在引用过程接口中。请注意,该接口是无条件创建的,然后传递给Invoke

    您无法从Invoke 内部解决此问题。当代码到达那里时,为时已晚。您无法检测到方法未分配。这应该作为错误报告给 Embarcadero。

    您唯一可行的解​​决方法是在CallInvokeOnObject 中添加一个额外分配的检查。

    【讨论】:

    • @Martin 您可以有效地将版本更改为 XE7 更新 1,版本号为 21.0.17707.5020。这是最新的,也是我进行测试的地方。
    猜你喜欢
    • 2011-04-13
    • 2014-06-24
    • 1970-01-01
    • 2012-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-04
    相关资源
    最近更新 更多