这个问题的根源是我的代码中的一个错误,因为我没有意识到存在一个由编译器创建的隐式接口变量。当拥有它的过程完成时,此变量已完成。这又导致了一个错误,由于变量的生命周期比我预期的更长。
现在,我有一个简单的项目来说明编译器的一些有趣的行为:
program ImplicitInterfaceLocals; {$APPTYPE CONSOLE} uses Classes; function Create: IInterface; begin Result := TInterfacedObject.Create; end; procedure StoreToLocal; var I: IInterface; begin I := Create; end; procedure StoreViaPointerToLocal; var I: IInterface; P: ^IInterface; begin P := @I; P^ := Create; end; begin StoreToLocal; StoreViaPointerToLocal; end.
StoreToLocal编译就像你想象的。局部变量I,函数的结果,作为一个隐式var参数传递给Create。 StoreToLocal的整理结果是单次调用IntfClear。没有惊喜。
但是,StoreViaPointerToLocal的处理方式不同。编译器创建一个隐式局部变量,它传递给Create。当创建返回时,执行对P ^的赋值。这使得该例程具有保持对接口的引用的两个局部变量。 StoreViaPointerToLocal的整理结果导致两次调用IntfClear。
StoreViaPointerToLocal的编译代码如下:
ImplicitInterfaceLocals.dpr.24: begin 00435C50 55 push ebp 00435C51 8BEC mov ebp,esp 00435C53 6A00 push $00 00435C55 6A00 push $00 00435C57 6A00 push $00 00435C59 33C0 xor eax,eax 00435C5B 55 push ebp 00435C5C 689E5C4300 push $00435c9e 00435C61 64FF30 push dword ptr fs:[eax] 00435C64 648920 mov fs:[eax],esp ImplicitInterfaceLocals.dpr.25: P := @I; 00435C67 8D45FC lea eax,[ebp-$04] 00435C6A 8945F8 mov [ebp-$08],eax ImplicitInterfaceLocals.dpr.26: P^ := Create; 00435C6D 8D45F4 lea eax,[ebp-$0c] 00435C70 E873FFFFFF call Create 00435C75 8B55F4 mov edx,[ebp-$0c] 00435C78 8B45F8 mov eax,[ebp-$08] 00435C7B E81032FDFF call @IntfCopy ImplicitInterfaceLocals.dpr.27: end; 00435C80 33C0 xor eax,eax 00435C82 5A pop edx 00435C83 59 pop ecx 00435C84 59 pop ecx 00435C85 648910 mov fs:[eax],edx 00435C88 68A55C4300 push $00435ca5 00435C8D 8D45F4 lea eax,[ebp-$0c] 00435C90 E8E331FDFF call @IntfClear 00435C95 8D45FC lea eax,[ebp-$04] 00435C98 E8DB31FDFF call @IntfClear 00435C9D C3 ret
我可以猜测为什么编译器这样做。当它可以证明分配给结果变量不会引发异常(即如果变量是一个局部变量),那么它直接使用结果变量。否则,它使用一个隐式局部,并且一旦函数返回就复制该接口,从而确保在异常情况下我们不会泄漏引用。
但我在文档中找不到任何声明。这很重要,因为接口生命是重要的,作为一个程序员,你需要能够影响它的场合。
所以,有没有人知道是否有这种行为的任何文档?如果没有人有更多的知识吗?如何处理实例字段,我还没有检查过。当然,我可以尝试一切为自己,但我正在寻找一个更正式的声明,总是宁愿避免依赖于实施细节通过试验和错误。
更新1
为了回答Remy的问题,当我需要在执行另一个完成操作之前完成接口背后的对象时,对我来说很重要。
begin AcquirePythonGIL; try PyObject := CreatePythonObject; try //do stuff with PyObject finally Finalize(PyObject); end; finally ReleasePythonGIL; end; end;
像这样写就好了。但在实际代码中,我有一个第二个隐式局部,在GIL被释放和被轰炸后被最终确定。我解决了这个问题,通过将Acquire / Release GIL中的代码提取到一个单独的方法中,从而缩小了接口变量的范围。
解决方法
procedure UseInterface(foo: IInterface); begin end; procedure Test() begin UseInterface(Create()); end;
编译器必须创建一个隐式的temp变量来保存Create的结果,因为它被传递到UseInterface,以确保接口的生命周期> = UseInterface调用的生命周期。该隐式temp变量将被放置在拥有它的过程的结尾,在这种情况下在Test()过程的结尾。
可能你的指针赋值情况可能和传递中间接口值作为函数参数落在同一个桶中,因为编译器不能“看到”值将去哪里。
我记得在这个领域有几个错误多年来。很久以前(D3?D4?),编译器根本没有引用计数中间值。它在大多数时间工作,但在参数别名的情况下遇到麻烦。一旦解决了,我就相信有一个关于const param的跟进。总是有一个愿望,在中断值接口的移动处置尽可能快的语句后,它需要,但我不认为曾经实现在Win32优化器,因为编译器没有设置用于处理语句或块粒度的处理。