Delphi 如何检查对过程的引用是否为零?

Delphi 如何检查对过程的引用是否为零?,delphi,delphi-xe3,Delphi,Delphi Xe3,在下面的代码示例中,调用AssertTestObj()会导致访问冲突 Project InvokeTest2.exe引发异常类$C0000005,并显示消息 '0x00000000处的访问冲突:读取地址0x00000000' 调试时,我可以看到TSafeCall.Invoke()中的Assigned(NotifyProc)测试未按预期工作-因此Invoke()尝试执行NotifyProc,该测试为nil,从而导致访问冲突 你知道为什么会失败以及如何解决吗 program InvokeTest2;

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

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

调试时,我可以看到
TSafeCall.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.
程序调用测试2;
{$APPTYPE控制台}
使用
System.SysUtils;
类型
TSafeCall=类
公众的
类型
TNotifyProc=程序参考(项目:T);
类过程调用(NotifyProc:TNotifyProc;项:T);超载;
结束;
TOnObj=对象的过程(值:字符串);
{TSafeCall}
类过程TSafeCall.Invoke(NotifyProc:TNotifyProc;项:T);
开始
如果已分配(NotifyProc),则
NotifyProc(项目);
结束;
过程AssertTestObj(onexceptionbj:TOnObj;Value:String);
开始
TSafeCall.Invoke(onexceptionbj,Value);
结束;
开始
尝试
调用(nil,“按预期工作”);
AssertTestObj(nil,“这会导致访问冲突!”);
除了
关于E:Exception-do
Writeln(E.ClassName,“:”,E.Message);
结束;
结束。

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

{$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.
你可能想知道我为什么简化。你的代码极好地再现了这个问题。然而,我想让它尽可能的简单,这样我才能真正确定问题是我所相信的。所以我删除了泛型和类

现在,使用
分配的测试是正确的。你期望它按你的意愿行事是对的。问题是,当编译器从
CallInvokeOnObject
生成调用
Invoke
的代码时,它需要将object的方法包装在引用过程接口中。为了正确地执行此操作,需要测试对象的方法是否已指定。如果不是,则不应创建包装器接口,并且应传递
Invoke
nil

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

Project1.dpr.16: begin // this is the beginning of CallInvokeOnObject 004064D8 55 push ebp 004064D9 8BEC mov ebp,esp 004064DB 6A00 push $00 004064DD 53 push ebx 004064DE 33C0 xor eax,eax 004064E0 55 push ebp 004064E1 683B654000 push $0040653b 004064E6 64FF30 push dword ptr fs:[eax] 004064E9 648920 mov fs:[eax],esp 004064EC B201 mov dl,$01 004064EE A1F4634000 mov eax,[$004063f4] 004064F3 E8DCDAFFFF call TObject.Create 004064F8 8BD8 mov ebx,eax 004064FA 8D45FC lea eax,[ebp-$04] 004064FD 8BD3 mov edx,ebx 004064FF 85D2 test edx,edx 00406501 7403 jz $00406506 00406503 83EAF8 sub edx,-$08 00406506 E881F2FFFF call @IntfCopy 0040650B 8B4508 mov eax,[ebp+$08] 0040650E 894310 mov [ebx+$10],eax 00406511 8B450C mov eax,[ebp+$0c] 00406514 894314 mov [ebx+$14],eax Project18.dpr.17: Invoke(OnObject); 00406517 8BC3 mov eax,ebx 00406519 85C0 test eax,eax 0040651B 7403 jz $00406520 0040651D 83E8E8 sub eax,-$18 00406520 E8DFFDFFFF call Invoke Project1.dpr.16:begin//这是CallInvokeOnObject的开始 004064D8 55推式ebp 004064D9 8BEC mov ebp,esp 004064DB 6A00推送$00 004064DD 53推式ebx 004064DE 33C0异或eax,eax 004064E0 55推式ebp 004064E1 683B654000推送$0040653b 004064E6 64FF30推送dword ptr fs:[eax] 004064E9 648920移动fs:[eax],esp 004064EC B201移动dl,$01 004064EE A1F4634000 mov eax,[$004063f4] 004064F3 E8DCDAFFF调用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子edx,-$08 00406506 E881F2FFF呼叫@IntfCopy 0040650B 8B4508 mov eax,[ebp+$08] 0040650E 894310 mov[ebx+$10],eax 00406511 8B450C mov eax,[ebp+$0c] 00406514 894314 mov[ebx+$14],eax 项目18.dpr.17:调用(OnObject); 00406517 8BC3 mov eax,ebx 00406519 85C0测试eax,eax 0040651B 7403 jz$00406520 0040651D 83E8E8子eax,-$18 00406520 E8DFFDFFFF调用 调用
TObject.Create
是将object方法封装在参考过程接口中的方法。请注意,接口是无条件创建的,然后传递给
Invoke

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


您唯一可行的解决方法是添加一个额外分配的签入
CallInvokeOnObject

@Martin您可以将版本更改为XE7 update 1,版本号为21.0.17707.5020。这是最新的,也是我做测试的地方。 Project1.dpr.16: begin // this is the beginning of CallInvokeOnObject 004064D8 55 push ebp 004064D9 8BEC mov ebp,esp 004064DB 6A00 push $00 004064DD 53 push ebx 004064DE 33C0 xor eax,eax 004064E0 55 push ebp 004064E1 683B654000 push $0040653b 004064E6 64FF30 push dword ptr fs:[eax] 004064E9 648920 mov fs:[eax],esp 004064EC B201 mov dl,$01 004064EE A1F4634000 mov eax,[$004063f4] 004064F3 E8DCDAFFFF call TObject.Create 004064F8 8BD8 mov ebx,eax 004064FA 8D45FC lea eax,[ebp-$04] 004064FD 8BD3 mov edx,ebx 004064FF 85D2 test edx,edx 00406501 7403 jz $00406506 00406503 83EAF8 sub edx,-$08 00406506 E881F2FFFF call @IntfCopy 0040650B 8B4508 mov eax,[ebp+$08] 0040650E 894310 mov [ebx+$10],eax 00406511 8B450C mov eax,[ebp+$0c] 00406514 894314 mov [ebx+$14],eax Project18.dpr.17: Invoke(OnObject); 00406517 8BC3 mov eax,ebx 00406519 85C0 test eax,eax 0040651B 7403 jz $00406520 0040651D 83E8E8 sub eax,-$18 00406520 E8DFFDFFFF call Invoke