arraysfor-loopdelphitlist

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


I have a loop that is updating the values in a 1 dimensional array (Arr2) and then adding the array to a list (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;

Originally I did not have the setlength call at the start of the for loop, and in this case the appropriate number of items were being added to resultPnts, but they were all the same (as though the logic assigning to Arr2.Pnts[0] and Arr2.Pnts[1] was not being called in every iteration of the loop)

I have fixed the problem by adding the setlength call, but I don't really understand why that is necessary. I would love to understand what is going on here so that I can more reliably avoid this sort of problem in future.

Can anybody explain to me why the code is not working as expected without the setlength in the loop?


Solution

  • Dynamic arrays are reference counted.

    Without the SetLength() inside the loop, you are adding multiple copies of the Arr2 variable to resultPnts but they all refer to the same physical array in memory, which you are modifying on each loop iteration. That is why all of the entries end up with the same array values assigned by the last loop iteration.

    SetLength() has a side effect that it forces an existing dynamic array to refcount=1 if the new size is > 0, even if the array's existing size is the same value. If the array has refcount=1, SetLength() will modify the array in-place, otherwise it will decrement the array's refcount and then allocate a new array with refcount=1.

    Thus, with SetLength() inside the loop, your resultPnts entries will each end up with a unique array assigned to them.

    The RTL in XE7+ has a public DynArrayUnique() function that serves the same purpose as your SetLength() call, eg:

    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;
    

    Or, you can use System.Copy() instead:

    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;