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?
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.
Arr2.Pnts
points to an array with refcount=1
from the initial SetLength()
.Arr2
to resultPnts
, incrementing the array's refcount
to 2.Arr2
to resultPnts
, incrementing the array's refcount
to 3.PathEntToPntArr()
exits, only the reference in Arr2.Pnts
is cleared, decrementing the array's refcount
, but the array still has active references from all of the entries in resultPnts
. The array will be freed when all of those references are cleared later.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.
Arr2.Pnts
points to an array with refcount=1
from the initial SetLength()
.SetLength()
, leaving the current array intact, then modifies the content of that array, then adds a copy of Arr2
to resultPnts
, incrementing that array's refcount
to 2.SetLength()
, decrementing the current array's refcount
to 1 and creating a new array with refcount=1
, then modifies the content of that new array, then adds a copy of Arr2
to resultPnts
, incrementing that array's refcount
to 2.PathEntToPntArr()
exits, only the reference in Arr2.Pnts
is cleared, decrementing the refcount
of the last array created. Each array in resultPnts
will be freed individually as their entries are cleared later.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;