delphi TArray结果不总是在for循环中初始化()?

6ju8rftf  于 2023-04-20  发布在  其他
关注(0)|答案(2)|浏览(219)

测试中的Result并非始终初始为()
我发现我需要在初始化时设置动态数组的长度吗?,但是我不完全理解这个答案
更重要的是,“断开”Result/A连接的最佳方法是什么(我需要循环)?也许有某种方法可以强制编译器“正确”初始化?手动添加Result := nil作为Test中的第一行?

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;
4c8rllxm

4c8rllxm1#

引用的问题是关于类内部的字段,它们都是零初始化的,并且托管类型在示例析构期间正确地终结。
你的代码是关于在循环内调用具有托管返回类型的函数。托管类型的局部变量被初始化一次-在例程的开始。底层的托管返回类型被编译器视为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会创建一个副本,因为编译器引入了第二个变量,它临时用来传递Result,在调用Test之后,它会把Result赋值给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是IecxResult(前面提到的Result var参数总是最后一个)。我们看到它使用了堆栈上的不同位置([ebp-$04]A变量,[ebp-$08]是编译器引入的变量)。在调用之后,我们看到编译器插入了一个对System._DynArrayAsg的额外调用,然后将编译器为Result引入的临时变量分配给A
下面是第二次调用Test的屏幕截图:

rkttyhzu

rkttyhzu2#

虽然我很犹豫是否将此For编译器优化称为bug,但如果直接修改数组元素,这肯定是没有帮助的:

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;

经过调查,我得出结论,影响整个数组的函数(例如,SetLengthCopy或其他返回TArray<Integer>的函数)将“破坏”For循环创建的Result/A一致性。
看起来最安全的方法是(根据原始帖子中链接的答案)将Result := nil;作为测试的第一行。
如果没有其他建议,我最终会接受这个答案。
注意:作为一个额外的好处,从Result := nil开始可以防止数组被SetLength复制--这是显而易见的,但是对于一个100000的数组来说,这个小小的修改可以使执行时间加快40

相关问题