Result
最初并不总是()
我发现我需要在初始化时设置动态数组的长度吗?,但是我不完全理解那个答案
更重要的是,“中断”Result/A 连接(我需要循环)的最佳方法是什么?也许有某种方法可以强制编译器“正确”初始化?手动添加
Result := nil
作为测试中的第一行?
function Test(var A: TArray<Integer>): TArray<Integer>;
begin
SetLength(Result, 3); // Breakpoint here
Result[0] := 2;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
A: TArray<Integer>; // Does not help whether A is local or global
I: Integer;
begin
for I := 1 to 3 do
A := Test(A); // At Test breakpoint:
// * FIRST loop: Result is ()
// * NEXT loops: Result is (2, 0, 0)
// modifying Result changes A (immediately)
A := Test(A); // Result is again ()
end;
引用的问题是关于类内部的字段,它们都是零初始化的,并且托管类型在实例销毁期间正确完成。
您的代码是关于在循环中调用具有托管返回类型的函数。托管类型的局部变量在例程开始时初始化一次。底层的托管返回类型被编译器视为 var 参数。因此,在第一次调用之后,它将看起来像
A
的内容传递给 Test
两次 - 作为 A
参数和 Result
.
但是您认为修改
Result
也会影响A
(参数)的评估是不正确的,我们可以通过稍微更改代码来证明:
function Test(var A: TArray<Integer>; I: Integer): TArray<Integer>;
begin
SetLength(Result, 3); // Breakpoint here
Result[0] := I;
end;
procedure Main;
var
A: TArray<Integer>;
I: Integer;
begin
for I := 1 to 3 do
A := Test(A, I);
A := Test(A, 0);
end;
当您单步执行
Test
时,您会看到更改 Result[0]
不会更改 A
。这是因为 SetLength
将创建一个副本,因为编译器引入了它临时用于传递结果的第二个变量,并且在调用 Test
之后它将其分配给 A
(局部变量) - 你可以在反汇编视图对于循环中的行看起来类似于此(我使用 $O+ 使代码比没有优化时更密集):
Project1.dpr.21: A := Test(A, I);
0041A3BD 8D4DF8 lea ecx,[ebp-$08]
0041A3C0 8D45FC lea eax,
0041A3C3 8BD3 mov edx,ebx
0041A3C5 E8B2FFFFFF call Test
0041A3CA 8B55F8 mov edx,[ebp-$08]
0041A3CD 8D45FC lea eax,[ebp-$04]
0041A3D0 8B0DC8244000 mov ecx,[$004024c8]
0041A3D6 E855E7FEFF call @DynArrayAsg
0041A3DB 43 inc ebx
知道默认调用约定是eax,edx,ecx中的前三个参数,我们知道eax是
A
参数,edx是I
和ecx
是Result
(前面提到的Result var参数总是最后一个).我们看到它使用了堆栈上的不同位置([ebp-$04]
是 A
变量和 [ebp-$08]
是编译器引入的变量)。在调用之后,我们看到编译器插入了对 System._DynArrayAsg
的额外调用,然后将编译器为 Result
引入的临时变量分配给 A
.
这里是第二次调用
Test
的截图:
虽然我犹豫是否将此
For
编译器优化称为错误,但如果直接修改数组元素,这肯定没有帮助:
function Test(var A: TArray<Integer>): TArray<Integer>;
begin
if Length(Result) > 0 then // Breakpoint
Result[1] := 66; // A modified!
SetLength(Result, 3);
Result[0] := Result[0] + 1; // A not modified
Exit;
A[9] := 666; // Force linker not to eliminate A
end;
经过调查,我得出结论,影响整个数组的函数(例如
SetLength
、Copy
或其他返回 TArray<Integer>
的函数)将——不出所料——“打破”由 创建的结果/A 相同性For
循环。
看来最安全的方法是(根据原始帖子中链接的答案)将
Result := nil;
作为测试的第一行。
如果没有进一步的建议,我最终会接受这个作为答案。
注意: 作为一个额外的好处,以
Result := nil
开头可以防止数组被 SetLength
复制——很明显,但是对于例如一个 100000 的数组被循环 100000 次这个小小的修改使执行时间快了约 40%