是否有必要给Delphi函数返回的变量赋一个默认值?



我逐渐使用了更多的变体—它们在某些地方对于携带编译时不知道的数据类型非常有用。一个有用的值是UnAssigned("我没有值给你")。我想我很久以前就发现了函数:

function DoSomething : variant;
begin
  If SomeBoolean then
    Result := 4.5
end;

似乎相当于:

function DoSomething : variant;
begin 
  If SomeBoolean then
    Result := 4.5
   else
   Result := Unassigned; // <<<<
end;

我假定这个推理,一个变量必须动态创建,如果SomeBoolean为FALSE,编译器已经创建了它,但它是'未分配' (<> nil?)。为了进一步鼓励这种想法,如果省略赋值Result,编译器不会报告任何警告。

刚才我发现了一个讨厌的bug,我的第一个例子(其中'Result'不是显式默认为'nil')实际上从其他地方返回了一个'旧'值。

我应该总是分配结果(当我使用预定义类型时)当返回一个变量?

是的,您总是需要初始化函数Result,即使它是托管类型(如stringVariant)。编译器确实生成一些代码来初始化Variant函数的未来返回值(至少Delphi 2010编译器我用于测试目的),但编译器不保证你的结果被初始化;这只会使测试变得更加困难,因为您可能会遇到这样的情况:您的Result被初始化,您的决策基于此,只是后来发现您的代码有问题,因为在某些情况下Result 没有初始化。

从我的调查中,我注意到:

  • 如果你的结果被赋值给一个全局变量,你的函数将被调用一个初始化的隐藏临时变量,从而产生一种结果被神奇地初始化的错觉。
  • 如果你对同一个全局变量进行两次赋值,你会得到两个不同的隐藏临时变量,强化了结果被初始化的错觉。
  • 如果你对同一个全局变量进行两次赋值,但在两次调用之间不使用全局变量,编译器只使用1个隐藏的临时变量,打破了之前的规则!
  • 如果变量是调用过程的本地变量,则根本不使用中间隐藏的本地变量,因此Result 不会初始化

示范:

首先,这里是一个函数返回Variant接收var Result: Variant隐藏参数的证明。下面两个编译到完全相同的汇编器,如下所示:

procedure RetVarProc(var V:Variant);
begin
  V := 1;
end;
function RetVarFunc: Variant;
begin
  Result := 1;
end;
// Generated assembler:
push ebx // needs to be saved
mov ebx, eax // EAX contains the address of the return Variant, copies that to EBX
mov eax, ebx // ... not a very smart compiler
mov edx, $00000001
mov cl, $01
call @VarFromInt
pop ebx
ret

接下来,看看编译器如何设置对两者的调用是很有趣的。下面是对具有var X:Variant参数的过程的调用:

procedure Test;
var X: Variant;
begin
  ProcThatTakesOneVarParameter(X);
end;
// compiles to:
lea eax, [ebp - $10]; // EAX gets the address of the local variable X
call ProcThatTakesOneVarParameter

如果我们将"X"作为全局变量,并调用返回变量的函数,我们得到以下代码:

var X: Variant;
procedure Test;
begin
  X := FuncReturningVar;
end;
// compiles to:
lea eax, [ebp-$10] // EAX gets the address of a HIDDEN local variable.
call FuncReturningVar // Calls our function with the local variable as parameter
lea edx, [ebp-$10] // EDX gets the address of the same HIDDEN local variable.
mov eax, $00123445 // EAX is loaded with the address of the global variable X
call @VarCopy // This moves the result of FuncReturningVar into the global variable X

如果你看一下这个函数的序言,你会注意到作为FuncReturningVar调用的临时参数的局部变量被初始化为ZERO。如果函数不包含任何Result :=语句,则X将为"未初始化"。如果我们再次调用该函数,将使用一个不同的临时和隐藏变量!下面是一段示例代码:

var X: Variant; // global variable
procedure Test;
begin
  X := FuncReturningVar;
  WriteLn(X); // Make sure we use "X"
  X := FuncReturningVar;
  WriteLn(X); // Again, make sure we use "X"
end;
// compiles to:
lea eax, [ebp-$10] // first local temporary
call FuncReturningVar
lea edx, [ebp-$10]
mov eax, $00123456
call @VarCopy
// [call to WriteLn using the actual address of X removed]
lea eax, [ebp-$20] // a DIFFERENT local temporary, again, initialized to Unassigned
call FuncReturningVar
// [ same as before, removed for brevity ]
查看该代码时,您会认为返回Variant的函数的"结果"是总是被调用方初始化为Unassigned。不正确的。如果在前面的测试中,我们将"X"变量设置为局部变量(而不是全局变量),编译器将不再使用两个单独的局部临时变量。我们有两种不同的情况编译器生成不同的代码。换句话说,不要做任何假设,始终分配Result

我对不同行为的猜测:如果Variant变量可以在当前作用域之外访问,作为全局变量(或类字段),编译器会生成使用线程安全的@VarCopy函数的代码。如果变量是函数的局部变量,则没有多线程问题,因此编译器可以自由地进行直接赋值(不再调用@VarCopy)。

我应该总是像我这样分配结果吗当使用预定义类型时)返回一个变体?

是的。

测试:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    if SomeBoolean then
        Result := 1
end;

像这样使用函数:

var
    xx: Variant;
begin
    xx := DoSomething(True);
    if xx <> Unassigned then
        ShowMessage('Assigned');
    xx := DoSomething(False);
    if xx <> Unassigned then
        ShowMessage('Assigned');
end;
在第二次调用DoSomething后,

xx仍然会被赋值。

将函数更改为:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    Result := Unassigned;
    if SomeBoolean then
        Result := 1
end;

并且xx在第二次调用DoSomething后不会被赋值

相关内容

最新更新