组合匿名和嵌套过程时的错误代码
Posted
技术标签:
【中文标题】组合匿名和嵌套过程时的错误代码【英文标题】:Wrong code when combining anonymous and nested procedures 【发布时间】:2013-09-07 15:49:52 【问题描述】:我有一些我认为是正确的 Delphi 代码的意外访问冲突,但似乎编译错误。我可以把它减少到
procedure Run(Proc: TProc);
begin
Proc;
end;
procedure Test;
begin
Run(
procedure
var
S: PChar;
procedure Nested;
begin
Run(
procedure
begin
end);
S := 'Hello, world!';
end;
begin
Run(
procedure
begin
S := 'Hello';
end);
Nested;
ShowMessage(S);
end);
end;
对我来说,S := 'Hello, world!'
存储在错误的位置。因此,要么引发访问冲突,要么 ShowMessage(S)
显示“Hello”(有时,在释放用于实现匿名过程的对象时引发访问冲突)。
我正在使用 Delphi XE,已安装所有更新。
我怎么知道这会在哪里引起问题?我知道如何重写我的代码以避免匿名过程,但我无法准确确定它们在哪些情况下会导致错误代码,所以我不知道在哪里可以避免它们。
我很想知道这是否在更高版本的 Delphi 中得到修复,但有趣的是,此时升级不是一个选项。
在 QC 上,我可以找到类似的最新报告 #91876,但在 Delphi XE 中已解决。
更新:
基于AlexSC的cmets,稍作修改:
...
procedure Nested;
begin
Run(
procedure
begin
S := S;
end);
S := 'Hello, world!';
end;
...
确实有效。
生成的机器码
S := 'Hello, world!';
在失败的程序中是
ScratchForm.pas.44: S := 'Hello, world!';
004BD971 B89CD94B00 mov eax,$004bd99c
004BD976 894524 mov [ebp+$24],eax
而正确的版本是
ScratchForm.pas.45: S := 'Hello, world!';
004BD981 B8B0D94B00 mov eax,$004bd9b0
004BD986 8B5508 mov edx,[ebp+$08]
004BD989 8B52FC mov edx,[edx-$04]
004BD98C 89420C mov [edx+$0c],eax
在失败的程序中生成的代码没有看到S
已经被移动到编译器生成的类中,[ebp+$24]
是嵌套方法的外部局部变量如何访问局部变量如何被访问。
【问题讨论】:
在我的测试中,我收到此警告“[DCC 警告] Unit1.pas(45): W1036 变量 '$frame' 可能尚未初始化”。由于我没有声明任何 $frame 变量,我假设它是由编译器在声明实现匿名方法的接口时生成的。该警告表明编译器并未正确完成所有操作,因此这似乎是一个错误。更改代码以将 S 变量声明为字符串会使问题更早地出现。调试表明生成的代码没有正确处理 S 变量。 @AlexSC “可能没有被初始化”检测是出了名的糟糕,有大量的误报并没有指向任何真正的问题,也不会影响生成的代码,所以这是一个警告应该可以安全地忽略它。我还可以在更简单的代码中获得该警告(包括$frame
编译器生成的变量),该代码可以正常工作。
在 XE2 中编译和工作正常
@hvd: 最奇怪的是,如果我添加一行像 S := '';在空的匿名方法中不会发生异常。这强烈表明我在处理 S 变量时确实存在错误;
@SertacAkyuz Damnit,下一个版本。 :)
【参考方案1】:
没有看到整个(程序测试)的整个汇编程序代码,并且只假设您发布的代码段,可能是在失败的代码段上,只有一个指针已被移动,而在正确的版本上也移动了一些数据。
所以似乎 S:=S 或 S:='' 导致编译器自己创建一个引用,甚至可以分配一些内存,这可以解释为什么它会起作用。
我还假设这就是为什么在没有 S:=S 或 S:='' 的情况下会发生访问冲突的原因,因为如果没有为字符串分配内存(请记住您只声明了 S:PChar),则会引发访问冲突,因为访问了未分配的内存。
如果你只是简单地声明 S: String ,这可能不会发生。
扩展评论后的补充:
PChar 只是一个指向数据结构的指针,它必须存在。 PChar 的另一个常见问题是声明局部变量,然后将 PChar 传递给该变量给其他 Procs,因为一旦例程结束,局部变量就会被释放,但 PChar 仍将指向它,然后引发访问冲突一次。
每个文档存在的唯一可能性是声明类似const S: PChar = 'Hello, world!'
的内容,因为编译器可以解析它的相对地址。但这仅适用于常量,不适用于上面示例中的变量。像上面的示例中那样做需要为 PChar 指向的字符串文字分配存储空间,然后指向 S:String; P:PChar; S:='Hello, world!'; P:=PChar(S);
或类似的。
如果声明 String 或 Integer 仍然失败,那么变量可能会在某个地方消失或突然在 proc 中不再可见,但这将是另一个与已经解释的现有 PChar 问题无关的问题。
最终结论:
可以做S:PChar; S:='Hello, world!'
,但是编译器然后简单地将它分配为一个本地或全局常量,就像const S: PChar = 'Hello, world!'
一样,它被保存到可执行文件中,第二个S := 'Hello'
然后创建另一个也保存到可执行文件中的常量和依此类推 - 但 S
然后只指向最后分配的一个,所有其他仍然在可执行文件中,但在不知道确切位置的情况下无法再访问,因为S
只指向最后分配的一个。
所以取决于哪一个是最后一个S
指向Hello, world!
或Hello
。在上面的示例中,我只能猜测哪一个是最后一个,谁知道也许编译器也只能猜测,这取决于优化和其他不可预测的因素 S
可能会突然指向未分配的 Mem 而不是时间的最后一个 @987654333 @ 被执行,然后引发访问冲突。
【讨论】:
我向你保证,我知道指针是如何工作的。我知道指针变量不包含指向的数据。但是字符串文字没有被引用,它们直接存在于可执行映像中,并且没有需要复制的数据,只有指针本身。我没有使用string
,因为像PChar
这样的非托管类型会产生更简单的程序集,从而更容易检查生成的程序集。但是在使用string
的时候也确实会失败,事实上,即使你使用Integer
类型的变量,你也可以看到问题。
也许它也失败了,但你仍然不能简单地分配“你好,世界!”像这样的 PChar 因为它只是一个指向数据的指针,但是在上面的示例中,它指向的地方没有数据。如果声明String
或Integer
仍然失败,那么变量可能会消失在某个地方,或者突然不再对proc 可见。整个 Proc 的完整 ASM 源将有助于揭示其他步骤跟踪槽中真正发生的情况,并观察自己变量消失的位置或访问冲突发生的确切位置以缩小范围。
就像我已经说过的,字符串文字不会被引用,它们不会消失。您可以毫无顾虑地保留指向字符串文字的指针。这不仅仅是一个实现细节,它是文档中明确的承诺,可以安全依赖。 Read it here. 至于检查生成的程序集,我已经这样做了,并在问题中显示了相关细节。变量并没有消失,我已经准确地找到了访问冲突发生的位置。
是的,但是 PChar 本身不是字符串文字,它是指向字符串文字或其他必须存在的数据结构的指针。而且我在上面的示例中没有看到任何字符串文字,只有 PChar 声明。 PChar 的另一个常见问题是声明局部变量,然后将 PChar 传递给该变量到其他 Procs,发生的情况是局部变量在例程结束后被释放,但 PChar 仍将指向它,然后引发访问一旦访问违规。因此,您可以毫无顾虑地保留指向字符串文字的指针并不完全正确。
如果你不知道什么是字符串文字,请查一下。如果您确实知道字符串文字是什么:请重新阅读您的评论,因为它没有任何意义。【参考方案2】:
我怎么知道这会在哪里引起问题?
目前还很难说。 如果我们知道 Delphi XE2 中修复的性质,我们会处于更好的位置。 您所能做的就是避免使用匿名函数。 Delphi 有过程变量,所以对匿名函数的需求并不那么可怕。 见http://www.deltics.co.nz/blog/posts/48。
我很想知道这是否在更高版本的 Delphi 中得到修复
根据@Sertac Akyuz 的说法,这已在 XE2 中修复。
我个人不喜欢匿名方法,并且不得不禁止人们在我的 Java 项目中使用它们,因为我们的代码库中有相当一部分是匿名的(事件处理程序)。 非常适度地使用我可以看到用例。 但是在 Delphi 中,我们有过程变量和嵌套过程......没有那么多。
【讨论】:
对不起,但这并不能回答我的问题。 “那显然是不可知的。”?真的吗?这是嵌套函数和匿名函数的某些组合发生的错误,比我有更多技能或知识的人肯定能够更详细地描述“某些组合”,这就是我要问的。没有必要完全避免它们。 我有一个用例,在不使代码复杂化的情况下很难避免它们:一个返回reference to procedure
的函数,它被传递并最终被其他代码调用。编译器生成的帮助对象会自动释放,因为 reference to procedure
类型是引用计数的。使用procedure of object
,我需要手动添加大量额外代码以确保对象被释放。唯一实用的替代方法是手动定义interface
并让助手类实现它。 Java 有垃圾收集,所以这不是问题。以上是关于组合匿名和嵌套过程时的错误代码的主要内容,如果未能解决你的问题,请参考以下文章