5

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

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反映在已存储在列表项中的数组上。

我需要声明多条记录吗?

4

3 回答 3

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等。

这个问题在文档中有一些详细的介绍。以下是相关摘录:

如果 X 和 Y 是相同动态数组类型的变量,则 X := Y 将 X 指向与 Y 相同的数组。(在执行此操作之前无需为 X 分配内存。)与字符串和静态数组不同,复制-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)不会重新分配数组。在编译时不报告超出范围的索引。相反,要制作动态数组的独立副本,您必须使用全局 Copy 函数:

 var
   A, B: array of Integer;
 begin
   SetLength(A, 1);
   A[0] := 1;
   B := Copy(A);
   B[0] := 2; { B[0] <> A[0] }
 end;
于 2014-01-31T15:30:17.543 回答
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它时会被破坏,因此不会保留与数组的链接,也不会影响您或任何其他将使用您的类型的开发人员。

于 2014-01-31T15:42:14.233 回答
-1

只需在旧变量中创建一个新变量,一切都应该没问题,

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);
于 2014-01-31T17:54:25.663 回答