在以下示例代码中,对AssertTestObj()的调用会导致访问冲突.
Project InvokeTest2.exe raised exception class $C0000005 with message
‘access violation at 0x00000000: read of address 0x00000000’.
调试时,我可以看到TSafeCall< T> .Invoke()中的Assigned(NotifyProc)测试不能按预期的方式运行 – 所以Invoke()会尝试执行NotifyProc,否则会导致访问冲突.
任何想法为什么会失败,如何解决?
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.
解决方法
这是一个编译器错误.这是我简化的复制品:
{$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的代码时,它需要在引用过程接口中包装对象的方法.为了正确地做到这一点,需要测试对象的方法是否被分配.如果没有,那么不应该创建包装器接口,并且Invoke应该被传递为零.
编译器无法做到这一点.它无条件地将对象的方法包装在引用过程接口中.您可以在为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
对TObject.Create的调用是在引用过程接口中包含对象的方法.请注意,该接口是无条件创建的,然后传递给Invoke.
没有办法在Invoke内部解决这个问题.到代码到达那里的时候太晚了.您无法检测到该方法未分配.这应该报告给Embarcadero作为一个错误.