当我事先不知道我将要拥有多少物品时,我应该如何为物品清单分配空间?

时间:2011-12-07 08:46:11

标签: delphi

another question中,David Heffernan发表了关于他“所有时间最不喜欢的德尔福构造”的评论:

SetLength(FItems, Length(FItems)+1);

他不喜欢的构造在Pebongo项目中大量使用,例如在this excerpt中:

procedure TBSONDocument.ReadStream( F: TStream ); 
var 
  len : integer; 
  elmtype : byte; 
  elmname : string; 
begin 
  Clear; 
  f.Read( len, sizeof( len ) ); 
  f.Read( elmtype, sizeof( byte ) ); 

  while elmtype <> BSON_EOF do // Loop
  begin 
    elmname := _ReadString( f ); 
    SetLength( FItems, length( FItems ) + 1 ); // This, spotted by TOndrej
    case elmtype of 
      BSON_ARRAY: FItems[high( FItems )] := TBSONArrayItem.Create; 
      BSON_BINARY: FItems[high( FItems )] := TBSONBinaryItem.Create; 
      ...
    end; 
    f.Read( elmtype, sizeof( byte ) ); 
  end; 
end; 

有哪些替代方案?

5 个答案:

答案 0 :(得分:9)

SetLength()本身并不是坏事,而是增加了循环中的长度。坏代码示例:

SetLength( Result.FItems, 0 ); 

for i := 0 to high( FItems ) do 
begin 
  SetLength(Result.FItems, Length(Result.Fitems)+1);
  Result.FItems[i] := FItems[i].Clone; 
end; 

在这种情况下,数组被重新排列并在每次迭代时重新分配内存。您发布的示例未显示SetLength()

的错误使用情况

答案 1 :(得分:7)

我所说的是一次增加动态数组1个元素:

SetLength(FItems, Length(FItems)+1);

在循环中,对于大型数组,这可能导致内存地址碎片。发生这种情况时,即使总可用地址空间很大,您也会发现自己无法分配大量连续的内存块。如果受到32位地址空间的限制,这可能是一个非常现实的问题。此外,性能也可能是一个问题。

有多种方法可以避免这个问题:

  • 预分配数组。有时这涉及迭代两次,一次计数项目,一次填充数组。这可以非常有效。事实上,这正是你问题中的TBSONDocument.Clone
  • 使用TListTList<T>容器。虽然这些使用动态数组作为其底层存储,但它们实现了基于容量的分配方法。当它们满了时,它们会分配大量额外的物品,以备将来添加。这再次非常有效。
  • 另一个用作最后手段的选择是远离连续分配的内存。以块为单位分配内存,并使用类或记录的索引属性在索引与存储所在的实际块和位置之间进行映射。这在避免地址空间碎片方面特别有效。

答案 2 :(得分:3)

另一种方法是使用TListTObjectList或通用列表等列表。

实际上,您发布的部分(SetLength(Result.FItems, Length(FItems));)没问题。我相信大卫指的是另一个部分:SetLength(FItems, Length(FItems) + 1);它应该是一个动态数组,并且在循环中可能效率低下。

答案 3 :(得分:3)

预先知道项目数量时可以使用的替代方法:每隔X项增加数组的长度,最后将数组的长度调整为添加的项目数。

示例:

Count := 0;
try
  while not Query.EOF do
  begin
    if Length(MyArray) = Count then
      SetLength(MyArray, Length(MyArray) + 50); //increase length with 50 items
    MyArray[Count] := Query.Fields(0).AsString;
    Inc(Count);
    Query.Next;
  end;
finally
  SetLength(MyArray, Count); //adjust length to number of items added
end;

答案 4 :(得分:0)

TList使用David引用的非连续内存样式。这是他答案的补充。

这是一个真实世界的比喻:

“停车场中的汽车”:对象列表与记录数组的类比

数组

停车场里有一排汽车已经装满了。这排汽车是一个阵列。实际的汽车存储在阵列中。内存中数组的大小是所有真实汽车组合的大小。

将汽车添加到数组

假设您要添加另一辆车,但行中没有空间。因此,您必须构建一个更大的新行并将所有车辆移动到新行中。每次要在行中添加一个时,都必须这样做。那是一个数组。

列表

如果将汽车写入一张纸上,将汽车列入清单怎么办?每个VIN指的是停车场的车。纸上的那份清单是TList。它只存储每辆车的参考。

将汽车添加到TList

现在,假设您要将汽车添加到列表中。那么,你可以把车停在任何地方。如果您的地段已满,请将其停放在阿拉斯加州。没关系。只需将新的VIN添加到列表中即可。如果你的纸张填满,再拿一张纸。但是,不要移动汽车。

  

TList很好,因为实际的列表项不存储在列表中。   它只是指向项目的指针。那些指针相对较小,   加上TList预先分配空间(在类比中,每张纸)。