组合匿名和嵌套过程时的错误代码

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 因为它只是一个指向数据的指针,但是在上面的示例中,它指向的地方没有数据。如果声明StringInteger 仍然失败,那么变量可能会消失在某个地方,或者突然不再对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 有垃圾收集,所以这不是问题。

以上是关于组合匿名和嵌套过程时的错误代码的主要内容,如果未能解决你的问题,请参考以下文章

如何解决在 Ionic Vue 方法的嵌套范围内定义“this”时的 ESLint 错误

匿名块和过程中出现的问题

组合查询时的 SQL-server 语法错误(传递查询)

Golang之继承,多重继承(struct)

创建存储过程时的语法错误

Greenplum匿名代码块错误?