【问题标题】:A generic list of records that contains dynamic array包含动态数组的通用记录列表
【发布时间】:2014-01-31 15:18:31
【问题描述】:

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

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

我需要声明多条记录吗?

【问题讨论】:

    标签: arrays delphi generics record tlist


    【解决方案1】:

    这个例子可以这样简化,去掉所有对泛型的引用:

    {$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 分配内存 在执行此操作之前。)与字符串和静态数组不同, 写时复制不适用于动态数组,因此它们不是 在写入之前自动复制。例如,之后 这段代码执行:

     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;
    

    【讨论】:

    • +1,我很好奇为什么没有直接的语言元素来进行深度复制。 Clone() 是个好名字。或者引入 COW 动态数组(我已经有了)。
    • @LURD 深拷贝是说起来容易做起来难的事情之一。在语言级别上,您需要每种类型都能够执行深度复制。这对指针意味着什么?也许有类型的指针是可行的,但是无类型的指针呢?你如何深度复制一个界面?所以我认为这本质上是困难的。感谢紫外线顺便说一句。我必须说在这里投票很奇怪。不回答的人得更多票。
    • @lurd 可能是因为字符串由简单的字符组成,但数组可以由对象、接口和谁知道什么组成。它将隐藏导致甚至失败的复杂性,例如开始这个主题的期望。
    • 对于字符串、接口和like,只要增加引用计数即可。无类型指针会被随意使用,但不会与 Copy() 命令不同。
    • @alcalde Python 类型确实需要实现挂钩才能使 deepcopy 工作。钩子定义明确,但需要演员的合作。
    【解决方案2】:

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

    剩下的,我宁愿在你添加值后立即断开你的变量和列表之间的链接。几个月后,您会忘记您遇到的问题,并且可能会重构您的程序。如果您将第二个 SetLengthList.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 退出时它会被销毁,它不会保存到数组的链接,也不会让您或任何其他使用您的类型的开发人员同胞发生冲突。

    【讨论】:

    • 我刚刚解决了这个问题
    • @DavidHeffernan 但您不知道主题启动器的实际含义。两个部分相互矛盾,你冒昧地决定你自己哪一个是错的。如果他放弃堆栈溢出,我认为这是值得的,所以让这个问题对任何人都有利而不是对逃离的 TS 有利是有道理的。然而 TS 在这里,他可以按照他真正意图的方向解决问题。
    • 这是我的错。 MyArr: array of integer 是正确的声明。问题由大卫修改
    • @DavidHeffernan 你能在答案中添加 nil、finalyze 和 local var 吗?那么我的可以被删除
    • @DavidHeffernan mmm,提到他们“他们存在”并不是展示例子和解释利弊。不能同意你已经这样做了:-) 甚至没有提到本地变量。也不是立即断开链接。
    【解决方案3】:

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

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

    【讨论】:

    • TMyRec 是一条记录。它没有 Create 方法。没有解释的答案从来都不是理想的。编程应该是关于理解而不是魔法咒语。
    猜你喜欢
    • 1970-01-01
    • 2010-12-26
    • 1970-01-01
    • 2023-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多