这是Delphi动态数组的行为吗? [英] Is this Delphi dynamic array behaviour expected

查看:146
本文介绍了这是Delphi动态数组的行为吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问题是-当动态数组设置为类成员时,如何在内部对其进行动态管理?它们是复制还是通过引用传递?使用了Delphi 10.3.3.

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

这是一个代码示例:

 界面类型TSomeRec =记录名称:字符串;结尾;TSomeRecArray = TSomeRec的数组;TSomeRecUpdate =类Arr:TSomeRecArray;过程UpdateArray;过程UpdateArrayWithParam(var ParamArray:TSomeRecArray);结尾;执行过程TSomeRecUpdate.UpdateArray;开始Delete(Arr,0,1);结尾;过程TSomeRecUpdate.UpdateArrayWithParam(var ParamArray:TSomeRecArray);开始Delete(ParamArray,0,1);结尾;程序测试;var r:TSomeRec;lArr:TSomeRecArray;recUpdate:TSomeRecUpdate;开始lArr:= [];r.Name:='abc';lArr:= lArr + [r];r.Name:='def';lArr:= lArr + [r];recUpdate:= TSomeRecUpdate.Create;recUpdate.Arr:= lArr;recUpdate.UpdateArray;//((('def'),('def'))< ===这是复制监视值的结果,为什么是两个值?lArr:= [];r.Name:='abc';lArr:= lArr + [r];r.Name:='def';lArr:= lArr + [r];recUpdate.UpdateArrayWithParam(lArr);//((('def'))< ===这是复制监视值的结果-WORKSrecUpdate.Free;结尾; 

解决方案

这是一个有趣的问题!

因为 Delete 更改了动态数组-就像 SetLength 一样-必须重新分配动态数组.并且它还将赋予它的指针更改为内存中的这个新位置.但是显然,它无法更改指向旧动态数组的任何其他指针.

因此,它应减少旧动态数组的引用计数,并创建一个引用计数为1的新动态数组.赋予 Delete 的指针将设置为此新动态数组./p>

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

在调用 SetLength 之后,保证 S 引用唯一的字符串或数组-即引用计数为1的字符串或数组.

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

考虑以下最小示例:

 过程TForm1.FormCreate(Sender:TObject);变种a,b:整数数组;开始一个:= [$ AAAAAAAA,$ BBBBBBBB];{1}b:= a;{2}Delete(a,0,1);{3}结尾; 

我选择了这些值,以便可以轻松在内存中发现它们(Alt + Ctrl + E).

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

  02A2C190 02 00 00 00 02 00 00 0002A2C198 AA AA AA AA AA BB BB BB BB BB 

在这里,引用计数为2,数组长度为2,与预期的一样.(有关动态数组,请参见内部数据格式的文档.)

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

  02A2C190 03 00 00 00 02 00 00 0002A2C198 AA AA AA AA AA BB BB BB BB BB 

按预期,参考计数现在为3.

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

  02A30F80 01 00 00 00 01 00 00 0002A30F88 BB BB BB BB 01 00 00 00 

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

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

  02A2C190 02 00 00 00 02 00 00 0002A2C198 BB BB BB BB BB BB BB BB BB BB 

尽管引用计数确实减少为2,但第一个元素已更改.

我的结论是

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

(2)它的行为应与我上面概述的一样,在这种情况下,这是一个错误.

很遗憾, 删除过程完全没有提及.

感觉像个虫子.

更新:RTL代码

我看了 Delete 过程的源代码,这很有趣.

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

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

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

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

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

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

 //如果不共享堆对象(引用计数= 1),则只需调整其大小即可.否则,我们会复制 

在检测到对象确实是共享对象之前(在我们的例子中,引用计数= 3),为新的动态数组进行新的堆分配,然后将旧的(精简的)对象复制到该新位置.它减少了旧数组的引用计数,并更新了新数组的引用计数,长度和参数指针.

所以我们还是以一个新的动态数组结束了.但是RTL程序员忘记了他们已经弄乱了原来的数组,该数组现在由放置在旧数组之上的新数组组成: BBBBBBBB BBBBBBBB .

The question is - how dynamic arrays are managed internally by Delphi when they are set as a class member? Are they copied or passed by reference? Delphi 10.3.3 used.

The UpdateArray method deletes the first element from the array. But the array length stays 2. The UpdateArrayWithParam method also deletes the first element from the array. But the array length is correctly reduced to 1.

Here is a code sample:

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;

解决方案

This is an interesting question!

Since Delete changes the length of the dynamic array -- just as SetLength does -- it has to reallocate the dynamic array. And it also changes the pointer given to it to this new location in memory. But obviously it cannot change any other pointers to the old dynamic array.

So it should decrease the reference count of the old dynamic array and create a new dynamic array with a reference count of 1. The pointer given to Delete will be set to this new dynamic array.

Hence, the old dynamic array should be untouched (except for its reduced reference count, of course). This is essentially documented for the similar SetLength function:

Following a call to SetLength, S is guaranteed to reference a unique string or array -- that is, a string or array with a reference count of one.

But surprisingly, this doesn't quite happen in this case.

Consider this minimal example:

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;

I have chosen the values so they are easy to spot in memory (Alt+Ctrl+E).

After (1), a points to $02A2C198 in my test run:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

Here the reference count is 2 and the array length is 2, as expected. (See the documentation for the internal data format for dynamic arrays.)

After (2), a = b, that is, Pointer(a) = Pointer(b). They both point to the same dynamic array, which now looks like this:

02A2C190  03 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

As expected, the reference count is now 3.

Now, let's see what happens after (3). a now points to a new dynamic array at 2A30F88 in my test run:

02A30F80  01 00 00 00 01 00 00 00
02A30F88  BB BB BB BB 01 00 00 00

As expected, this new dynamic array has a reference count of 1 and only the "B element".

I would expect the old dynamic array, which b is still pointing to, to look as before but with a reduced reference count of 2. But it looks like this now:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  BB BB BB BB BB BB BB BB

Although the reference count is indeed reduced to 2, the first element has been changed.

My conclusion is that

(1) It is part of the contract of the Delete procedure that it invalidates all other references to the initial dynamic array.

or

(2) It should behave as I outlined above, in which case this is a bug.

Unfortunately, the documentation for the Delete procedure doesn't mention this at all.

It feels like a bug.

Update: The RTL Code

I had a look at the source code of the Delete procedure, and this is rather interesting.

It might be helpful to compare the behaviour with that of SetLength (because that one works correctly):

  1. If the reference count of the dynamic array is 1, SetLength tries simply to resize the heap object (and update the dynamic array's length field).

  2. Otherwise, SetLength makes a new heap allocation for a new dynamic array with a reference count of 1. The reference count of the old array is decreased by 1.

This way, it is guaranteed that the final reference count is always 1 -- either it was that from the beginning or a new array has been created. (It is a good thing that you don't always make a new heap allocation. For instance, if you have a large array with a reference count of 1, simply truncating it is cheaper than copying it to a new location.)

Now, since Delete always makes the array smaller, it is tempting to attempt simply to reduce the size of the heap object where it is. And this is indeed what the RTL code attempts in System._DynArrayDelete. Hence, in your case, the BBBBBBBB is moved to the beginning of the array. All is well.

But then it calls System.DynArraySetLength, which is also used by SetLength. And this procedure contains the following comment,

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

before it detects that the object is indeed shared (in our case, ref count = 3), makes a new heap allocation for a new dynamic array, and copies the old (reduced) one to this new location. It reduces the ref count of the old array, and updates the ref count, length, and argument pointer of the new one.

So we ended up with a new dynamic array anyway. But the RTL programmers forgot that they had already messed up the original array, which now consists of the new array placed on top of the old one: BBBBBBBB BBBBBBBB.

这篇关于这是Delphi动态数组的行为吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆