5
votes

I have a generic list of records. these records contains a dynamic array like following

Type
  TMyRec=record
MyArr:Array of Integer;
    Name: string;
    Completed: Boolean;
  end;

var
  MyList:TList<TMyRec>;
  MyRec:TMyRec;

then I create the list and set the array length like followings

MyList:=TList<TMyRec>.Create;
SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

then i change the data in MyArr and i also change MyRec.Name and add another item to the list

MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);

when MyRec.MyArr changes after adding the first item to the list, MyArr which is stored to the list also changes. however the other record fields does not.

My question is how to prevent the changes in MyRec.MyArr to be reflected on the array which is already stored in the list item.

do i need to declare multiple records.

3

3 Answers

4
votes

This example can be simplified like so, removing all reference to generics:

{$APPTYPE CONSOLE}

var
  x, y: array of Integer;

begin
  SetLength(x, 1);
  x[0] := 42;
  y := x;
  Writeln(x[0]);
  y[0] := 666;
  Writeln(x[0]);
end.

The output is:

42
666

The reason for this is that a dynamic array is a reference type. When you assign to a variable of dynamic array type, you are taking another reference and not making a copy.

You can resolve this by forcing a reference to be unique (that is have just a simple reference). There are a number of ways to achieve this. For instance, you can call SetLength on the array that you want to be unique.

{$APPTYPE CONSOLE}

var
  x, y: array of Integer;

begin
  SetLength(x, 1);
  x[0] := 42;
  y := x;
  SetLength(y, Length(y));
  Writeln(x[0]);
  y[0] := 666;
  Writeln(x[0]);
end.

Output:

42
42

So, in your code you can write it like this:

MyList:=TList<TMyRec>.Create;

SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

SetLength(MyRec.MyArr,5); // <-- make the array unique
MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);

You can use a variety of other ways to enforce uniqueness, including Finalize, assigning nil, Copy, etc.

This issue is covered in some detail in the documentation. Here are the pertinent excerpts:

If X and Y are variables of the same dynamic-array type, X := Y points X to the same array as Y. (There is no need to allocate memory for X before performing this operation.) Unlike strings and static arrays, copy-on-write is not employed for dynamic arrays, so they are not automatically copied before they are written to. For example, after this code executes:

 var
   A, B: array of Integer;
   begin
     SetLength(A, 1);
     A[0] := 1;
     B := A;
     B[0] := 2;
   end;

the value of A[0] is 2. (If A and B were static arrays, A[0] would still be 1.) Assigning to a dynamic-array index (for example, MyFlexibleArray[2] := 7) does not reallocate the array. Out-of-range indexes are not reported at compile time. In contrast, to make an independent copy of a dynamic array, you must use the global Copy function:

 var
   A, B: array of Integer;
 begin
   SetLength(A, 1);
   A[0] := 1;
   B := Copy(A);
   B[0] := 2; { B[0] <> A[0] }
 end;
4
votes

...here were observations of the original question controverscies

What for the rest, i'd prefer to break the link between your variable and the list immediately after you put added the value. In few months you would forgot that issue you had and maybe would refactor your program. If you put the 2nd SetLength away from List.Add you may just forget that the record still holds a reference to the same array you have in the list.

  TMyRec=record
    MyArr: TArray< double >; // making it 1D for simplicity
    Name: string;
    Completed: Boolean;
  end;


SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);
MyRec.MyArr := nil; // breaking the parasite link immediately!

...now here you can do anything you want - but MyRec is already clean.

Then, what if you would have many arrays, not just one ? There is one function that Delphi uses itself behind the curtains: http://docwiki.embarcadero.com/Libraries/XE5/en/System.Finalize which would have find all the arrays to clean.

SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);
Finalyze(MyRec); // breaking all the parasite links immediately!

Now, the last option would just to compact the used code into a procedure, that you would be able to call several times. Then the variable would become local one and Delphi would Finalize it for you automatically.

Procedure AddRec( const Name: string; const Compl: boolean; const Data: array of double);
var i: integer; MyRec: TMyRec;
begin
  SetLength(MyRec.MyArr, Length( Data ) );
  for i := 0 to Length(Data) - 1 do
    MyRec.MyArr[i] := Data [i];  

  MyRec.Name := Name;

  MyRec.Completed := Compl;
  MyList.Add(MyRec);
end;

MyList:=TMyList<TMyRec>.create;

AddRec( 'Record 1', True , [ 8 ]);
AddRec( 'Record 2', False, [ 5 ]);
...

Since MyRec is now a local variable, that gets destroyed when exiting from AddRec it would not hold that link to the array and would not conduse neither you nor any other fellow developer, who would use your types.

-1
votes

Just create a new one into old variable, every thing should be Ok,

MyList:=TList<TMyRec>.Create;
SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

MyRec := TMyRec.Create();
SetLength(MyRec.MyArr,5);

MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);