Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A generic list of records that contains dynamic array

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.

like image 587
Khalid Avatar asked Jan 31 '14 15:01

Khalid


1 Answers

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;
like image 191
David Heffernan Avatar answered Sep 25 '22 23:09

David Heffernan