我有一个通用的记录列表。这些记录包含一个动态数组,如下面的
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
中的更改反映在已存储在列表项中的数组上。
我是否需要声明多条记录。
答案 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
,分配nil
,Copy
等。
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);