9

我需要澄清这个案子。

根据我的测试,Result 变量从第一行定义为:Boolean=False、Integer=0、String=''、Object=nil 等。但我从未见过官方的参考资料。这也很有意义,因为这给出了提示。

[DCC 警告] Unit1.pas(35): H2077 分配给“TForm1.Test”的值从未使用过

function TForm1.Test: Boolean;
begin
  Result := False;
  // Some arbitrary code here

  Result := True;
end;

但是,如果我注释掉第一行并且在最后一行之前的某个地方出现异常,会发生什么?结果 = False 吗?

如果 Result 未定义,这意味着我总是必须通过定义 Result 来启动每个函数,以防以后出现异常。这对我来说毫无意义。

4

3 回答 3

16

正如官方 Delphi 文档所述,结果是:

  • CPU 寄存器 (AL / AX / EAX / RAX / EAX:EDX) 用于寄存器中包含的序数值和元素;
  • FPU 寄存器 (st(0) / XMM1);
  • 作为最新参数传递的附加变量。

一般规则是默认情况下不定义结果值。你必须设置它。编译器会警告您任何丢失的结果集。

对于字符串、动态数组、方法指针或变体结果,效果与将函数结果声明为声明参数后的附加 var 参数一样。换句话说,调用者传递了一个额外的 32 位指针,该指针指向一个返回函数结果的变量。

准确地说,该var参数不仅适用于托管类型,而且适用于recordobject结果,它们在调用之前分配在堆栈上,因此具有相同的行为。

也就是说,例如,如果您的结果是 a string,它将作为附加var参数传递。因此默认情况下它将包含调用之前的值。一开始会是'',然后如果你多次调用该函数,它将包含以前的值。

function GetString: string;
// is compiled as procedure GetString(var result: string);
begin
  if result='' then
    result := 'test' else
    writeln('result=',result);
end;

function GetRaise: string;
// is compiled as procedure GetRaise(var result: string);
begin
  result := 'toto';
  raise Exception.Create('Problem');
end;

var s: string;
begin
  // here s=''
  s := GetString; // called as GetString(s);
  // here s='test'
  s := GetString; // called as GetString(s);
  // will write 'result=test' on the console
  try
    s := GetRaise; // called as GetRaise(s);
  finally
    // here s='toto'
  end;
end;

所以我的建议是:

  • 修复所有关于未设置结果的编译器警告;
  • 不要假设结果字符串被初始化为 '' (它可能一开始是,但不是在第二次调用时) - 这是作为var参数传递的,而不是作为out参数传递的;
  • Anyexception照常处理,即运行流程将跳转到下一个finallyexcept块 - 但如果您将结果作为var参数传输,并且已经分配了某些内容result,则将设置该值;
  • 这并不是因为在大多数情况下,未设置的结果序数值(例如布尔值)为 0(因为在返回之前的 asm 代码中 EAX=0),它会是下一次(我在客户端看到随机问题由于这种未设置的结果变量:它大部分时间都有效,然后有时代码会失败......);
  • 在较新版本的 Delphi 上,您可以使用该exit()语法返回一个值。
于 2012-04-10T07:23:46.227 回答
10

你说:

如果 Result 未定义,这意味着我总是必须通过定义 Result 来启动每个函数,以防以后出现异常。

如果函数引发异常,您担心函数的返回值未定义。但这应该没关系。考虑以下代码:

x := fn();

如果函数体fn引发异常,x则不应将其分配给调用站点。从逻辑上讲,上面的单行可以被认为是两行:

  1. 称呼fn()
  2. 将返回值分配给x

如果在第 1 行引发异常,则第 2 行永远不会发生,x也不应该被分配给。

因此,如果在分配之前引发了异常,Result那么这根本不是问题,因为如果函数引发异常,则永远不应使用函数的返回值。


你实际上应该关心的是一个相关的问题。如果您分配给Result然后引发异常怎么办您分配的值是否有可能Result传播到函数之外?可悲的是,答案是肯定的。

对于许多结果类型(例如整数、布尔值等),Result如果该函数引发异常,您分配给的值不会传播到该函数之外。到目前为止,一切都很好。

但是对于某些结果类型(字符串、动态数组、接口引用、变体等),有一个实现细节使事情变得复杂。返回值作为var参数传递给函数。事实证明,您可以从函数外部初始化返回值。像这样:

s := 'my string';
s := fn();

当 的主体fn开始执行时,Result具有值'my string'。好像fn是这样声明的:

procedure fn(var Result: string);

这意味着您可以分配给Result变量并在调用站点查看修改,即使您的函数随后引发异常。没有干净的方法可以解决它。您可以做的最好的事情是分配给函数中的局部变量,并且仅将 Result 分配为函数的最终行为。

function fn: string;
var
  s: string;
begin
  s := ...
  ... blah blah, maybe raise exception
  Result := s;
end;

return这里强烈地感觉到缺乏 C 风格的陈述。


很难准确地说明哪种类型的结果变量容易受到上述问题的影响。最初我认为问题只是影响了托管类型。但 Arnaud 在评论中指出,记录和对象也受到影响。好吧,如果记录或对象是堆栈分配的,那就是真的。如果它是全局变量或堆分配的(例如类的成员),那么编译器会以不同的方式处理它。对于堆分配记录,使用隐式堆栈分配变量返回函数结果。只有当函数返回时,它才会被复制到堆分配的变量中。因此,您在调用站点为函数结果变量分配的值会影响函数本身的语义!


在我看来,这非常清楚地说明了为什么在语言设计中函数返回值具有var语义而不是语义是一个可怕的错误out

于 2012-04-10T07:00:43.403 回答
6

不,Result没有(保证的)默认值。除非你给它一个值,否则它是未定义的。文档暗示了这一点,其中指出

如果函数在没有为 Result 或函数名赋值的情况下退出,那么函数的返回值是未定义的。

我刚试过

function test: integer;
begin
  ShowMessage(IntToStr(result));
end;

并收到一条带有文本的消息35531136

于 2012-04-10T06:40:23.060 回答