包含动态数组的通用记录列表

时间:2014-01-31 15:18:31

标签: arrays delphi generics record tlist

我有一个通用的记录列表。这些记录包含一个动态数组,如下面的

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

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

然后我创建列表并设置数组长度,如下所示

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);

然后我更改了MyArr中的数据,我也更改了MyRec.Name并将另一项添加到列表中

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

在将第一项添加到列表后MyRec.MyArr更改时,存储到列表中的MyArr也会更改。但是其他记录字段没有。

我的问题是如何防止MyRec.MyArr中的更改反映在已存储在列表项中的数组上。

我是否需要声明多条记录。

3 个答案:

答案 0 :(得分:4)

此示例可以像这样简化,删除对泛型的所有引用:

{$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.

输出结果为:

42
666

原因是动态数组是引用类型。当您分配给动态数组类型的变量时,您将获取另一个引用而不进行复制。

您可以通过强制引用是唯一的(只有一个简单的引用)来解决此问题。有很多方法可以实现这一目标。例如,您可以在要唯一的数组上调用SetLength

{$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.

输出:

42
42

因此,在您的代码中,您可以这样写:

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);

您可以使用多种其他方式来强制执行唯一性,包括Finalize,分配nilCopy等。

documentation中详细介绍了此问题。以下是相关的摘录:

  

如果X和Y是相同动态数组类型的变量,则X:= Y点   X与Y相同的数组。(不需要为X分配内存   在执行此操作之前。)与字符串和静态数组不同,   动态数组不使用copy-on-write,因此它们不是   在写入之前自动复制。例如,之后   这段代码执行:

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

A [0]的值是2.(如果A和B是静态数组,A [0]会   仍然是1.)分配到动态数组索引(例如,   MyFlexibleArray [2]:= 7)不重新分配数组。超出范围   编译时不报告索引。相比之下,做一个   动态数组的独立副本,必须使用全局复制   功能:

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

答案 1 :(得分:4)

...这里是对原始问题争议的观察

对于其他内容,我更愿意在添加值后立即断开变量与列表之间的链接。在几个月内你会忘记你遇到的问题,也许会重构你的程序。如果您将第二个SetLength放在远离List.Add的位置,您可能会忘记该记录仍然保留对列表中相同数组的引用。

  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!

...现在你可以做任何你想做的事 - 但MyRec已经很干净了。

然后,如果你有很多阵列,而不仅仅是一个阵列怎么办? Delphi在幕后使用了一个函数:http://docwiki.embarcadero.com/Libraries/XE5/en/System.Finalize,它可以找到所有要清理的数组。

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!

现在,最后一个选项只是将使用过的代码压缩成一个程序,您可以多次调用它。然后变量将成为本地变量,Delphi会自动Finalize为你。

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 ]);
...

由于MyRec现在是一个局部变量,当它从AddRec退出时会被破坏,它不会保存到该数组的链接,也不会影响你或任何其他开发者,他们会使用你的类型。

答案 2 :(得分:-1)

只需在旧变量中创建一个新变量,每个东西都应该是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);