Delphi 数组或列表未按预期在 for 循环中更新

Delphi Array or List not being updated as expected in for loop

我有一个循环更新一维数组 (Arr2) 中的值,然后将数组添加到列表 (ResultPnts):

type
  point = packed record
    case aSInt of
       0: (x, y, z: aFloat);
       1: (v: array [0 .. 2] of aFloat); { vertex }
  end;

  MyPntArr = array of point;

  EntPntArr = record
    enttyp : byte;
    zb, zh : double;    // store zbase and zheight for 2d lines/arcs
    closed : boolean;   // set true if first and last points of lines/arcs are equal
    Pnts   : MyPntArr;
  end;

  tPaths = TList<EntPntArr>;

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  setlength (Arr2.Pnts, 2);
  Arr2.closed := false;
  Arr2.enttyp := entslb;
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    setlength (Arr2.Pnts, 2); // note: I'm not entirely sure why, but without setting length in
                              // each iteration of the loop, the array points are not updated
                              // in each subsequent iteration after the first.
    Arr2.Pnts[0] := Arr1.Pnts[j];
    // add slbthick to Arr1.Pnts[j], with result in the var parameter Arr2.Pnts[1]
    AddPnt (Arr1.Pnts[j], slbthick, Arr2.Pnts[1]);  
    resultPnts.Add(Arr2); 
  end;

最初我没有在 for 循环开始时进行 setlength 调用,在这种情况下,将适当数量的项目添加到 resultPnts,但它们都是相同的(就像分配给 [ 的逻辑一样=20=][0] 和 Arr2.Pnts[1] 并没有在循环的每次迭代中被调用)

我已经通过添加 setlength 调用解决了这个问题,但我真的不明白为什么这是必要的。我很想了解这里发生了什么,以便将来可以更可靠地避免此类问题。

任何人都可以向我解释为什么没有循环中的 setlength 代码无法按预期工作吗?

动态数组是引用计数的。

如果循环内没有 SetLength(),您将 Arr2 变量的多个 副本 添加到 resultPnts,但它们都引用内存中的相同物理数组,您在每次循环迭代时都在修改它。这就是为什么所有条目都以最后一次循环迭代分配的相同数组值结束。

  • 在进入循环之前,Arr2.Pnts从初始SetLength()指向一个数组refcount=1
  • 第一次循环迭代修改现有数组的内容,然后将 Arr2 的副本添加到 resultPnts,将数组的 refcount 递增到 2。
  • 第二次循环迭代修改现有数组的内容,然后将 Arr2 的副本添加到 resultPnts,将数组的 refcount 递增到 3。
  • 以此类推,直到循环结束。
  • PathEntToPntArr() 退出时,只有 Arr2.Pnts 中的引用被清除,递减数组的 refcount,但数组仍然具有来自 [=14] 中所有条目的活动引用=].稍后清除所有这些引用后,将释放该数组。

SetLength() 有一个副作用,即如果新大小 > 0,它会将现有动态数组强制为 refcount=1,即使数组的现有大小是相同的值。如果数组有 refcount=1SetLength() 将就地修改数组,否则它将递减数组的 refcount,然后分配一个新数组 refcount=1.

因此,在循环中使用 SetLength(),您的 resultPnts 个条目将以分配给它们的唯一数组结束。

  • 在进入循环之前,Arr2.Pnts从初始SetLength()指向一个数组refcount=1
  • 第一个循环迭代调用SetLength(),保持当前数组不变,然后修改该数组的内容,然后将Arr2的副本添加到resultPnts,递增该数组的内容refcount 到 2.
  • 第二次循环迭代调用SetLength(),将当前数组的refcount递减为1并用refcount=1创建一个新数组,然后修改该新数组的内容,然后添加Arr2resultPnts 的副本,将该数组的 refcount 递增到 2.
  • 以此类推,直到循环结束。
  • PathEntToPntArr()退出时,只有Arr2.Pnts中的引用被清除,递减最后创建的数组的refcountresultPnts 中的每个数组将在稍后清除其条目时单独释放。

XE7+ 中的 RTL 有一个 public DynArrayUnique() 函数,其作用与您的 SetLength() 调用相同,例如:

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  SetLength(Arr2.Pnts, 2);
  ...
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    DynArrayUnique(Pointer(Arr2.Pnts), TypeInfo(point)); // <--
    ...
    resultPnts.Add(Arr2); 
  end;
  ...
end;

或者,您可以使用 System.Copy() 代替:

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  SetLength(Arr2.Pnts, 2);
  ...
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    Arr2.Pnts := Copy(Arr2.Pnts{0, Length(Arr2.Pnts)}); // <--
    ...
    resultPnts.Add(Arr2); 
  end;
  ...
end;