这是预期的 Delphi 动态数组行为吗

dwr*_*udr 8 arrays delphi dynamic

问题是 - 当动态数组被设置为类成员时,它们是如何由 Delphi 内部管理的?它们是通过引用复制还是传递?使用 Delphi 10.3.3。

UpdateArray方法从数组中删除第一个元素。但是数组长度保持为 2。该UpdateArrayWithParam方法还会从数组中删除第一个元素。但是数组长度正确地减少到 1。

这是一个代码示例:

interface

type
  TSomeRec = record
      Name: string;
  end;
  TSomeRecArray = array of TSomeRec;

  TSomeRecUpdate = class
    Arr: TSomeRecArray;
    procedure UpdateArray;
    procedure UpdateArrayWithParam(var ParamArray: TSomeRecArray);
  end;

implementation

procedure TSomeRecUpdate.UpdateArray;
begin
    Delete(Arr, 0, 1);
end;

procedure TSomeRecUpdate.UpdateArrayWithParam(var ParamArray: TSomeRecArray);
begin
    Delete(ParamArray, 0, 1);
end;

procedure Test;
var r: TSomeRec;
    lArr: TSomeRecArray;
    recUpdate: TSomeRecUpdate;
begin
    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate := TSomeRecUpdate.Create;
    recUpdate.Arr := lArr;
    recUpdate.UpdateArray;
    //(('def'), ('def')) <=== this is the result of copy watch value, WHY two values?

    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate.UpdateArrayWithParam(lArr);

    //(('def')) <=== this is the result of copy watch value - WORKS

    recUpdate.Free;
end;
Run Code Online (Sandbox Code Playgroud)

And*_*and 9

这是个有趣的问题!

由于Delete更改了动态数组的长度——就像那样SetLength——它必须重新分配动态数组。并且它还将给它的指针更改为内存中的这个新位置。但显然它不能改变任何其他指向旧动态数组的指针。

所以它应该减少旧动态数组的引用计数并创建一个引用计数为1的Delete新动态数组。给定的指针将被设置为这个新的动态数组。

因此,旧的动态数组应该保持不变(当然,它减少的引用计数除外)。这基本上是为类似的SetLength功能记录的

在调用 , 之后SetLengthS保证引用唯一的字符串或数组——即引用计数为 1 的字符串或数组。

但令人惊讶的是,这在这种情况下并没有发生。

考虑这个最小的例子:

procedure TForm1.FormCreate(Sender: TObject);
var
  a, b: array of Integer;
begin

  a := [$AAAAAAAA, $BBBBBBBB]; {1}
  b := a;                      {2}

  Delete(a, 0, 1);             {3}

end;
Run Code Online (Sandbox Code Playgroud)

我选择了这些值,以便它们在内存中很容易被发现 (Alt+Ctrl+E)。

在(1)之后,在我的测试运行中a指向$02A2C198

02A2C190  02 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB
Run Code Online (Sandbox Code Playgroud)

这里引用计数为 2,数组长度为 2,正如预期的那样。(有关动态数组的内部数据格式,请参阅文档。)

在 (2) 之后,a = b,即Pointer(a) = Pointer(b)。它们都指向同一个动态数组,现在看起来像这样:

02A2C190  03 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,引用计数现在是 3。

现在,让我们看看 (3) 之后会发生什么。a现在2A30F88在我的测试运行中指向一个新的动态数组:

02A30F80  01 00 00 00 01 00 00 00
02A30F88  BB BB BB BB 01 00 00 00
Run Code Online (Sandbox Code Playgroud)

正如预期的那样,这个新的动态数组的引用计数为 1,并且只有“B 元素”。

我希望b仍然指向的旧动态数组看起来像以前一样,但引用计数减少了 2。但现在看起来像这样:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  BB BB BB BB BB BB BB BB
Run Code Online (Sandbox Code Playgroud)

虽然引用计数确实减少到2,但是第一个元素已经改变了。

我的结论是

(1) 它是Delete程序契约的一部分,它使对初始动态数组的所有其他引用无效。

或者

(2) 它应该像我上面概述的那样表现,在这种情况下这是一个错误。

不幸的是,该Delete过程的文档根本没有提到这一点。

感觉是个bug。

更新:RTL 代码

我查看了该Delete程序的源代码,这很有趣。

将行为与以下行为进行比较可能会有所帮助SetLength(因为一个工作正常):

  1. 如果动态数组的引用计数为 1,则SetLength尝试简单地调整堆对象的大小(并更新动态数组的长度字段)。

  2. 否则,SetLength为引用计数为 1 的新动态数组进行新的堆分配。旧数组的引用计数减 1。

这样,就可以保证最终的引用计数始终是1——要么是从头开始的,要么是已经创建的新数组。(您并不总是进行新的堆分配,这是一件好事。例如,如果您有一个引用计数为 1 的大数组,那么简单地将其截断比将其复制到新位置要便宜。)

现在,由于Delete总是使数组变小,因此很容易尝试简单地减小堆对象所在位置的大小。这确实是 RTL 代码在System._DynArrayDelete. 因此,在您的情况下,将BBBBBBBB移至数组的开头。一切都很好。

但随后它调用System.DynArraySetLength,它也被SetLength. 此过程包含以下注释,

// If the heap object isn't shared (ref count = 1), just resize it. Otherwise, we make a copy
Run Code Online (Sandbox Code Playgroud)

在它检测到对象确实是共享的(在我们的例子中,ref count = 3)之前,为一个新的动态数组分配一个新的堆,并将旧的(减少的)复制到这个新位置。它减少旧数组的引用计数,并更新新数组的引用计数、长度和参数指针。

所以无论如何我们最终得到了一个新的动态数组。但是 RTL 程序员忘记了他们已经弄乱了原始数组,它现在由放置在旧数组之上的新数组组成:BBBBBBBB BBBBBBBB.