Delphi 如果我没有';I';我要吃什么?

Delphi 如果我没有';I';我要吃什么?,delphi,Delphi,年,他发表了一篇关于他“一直以来最不喜欢的德尔福结构”的评论: 他不喜欢的结构在项目中大量使用,例如: 程序TBSONDocument.ReadStream(F:TStream); 变量 len:整数; 类型:字节; 名称:字符串; 开始 清楚的 f、 读取(len,sizeof(len)); f、 读取(elmtype,sizeof(字节)); 而elmtype BSON_EOF do//Loop 开始 elmname:=\u ReadString(f); 设置长度(FItems,长度(FIt

年,他发表了一篇关于他“一直以来最不喜欢的德尔福结构”的评论:

他不喜欢的结构在项目中大量使用,例如:

程序TBSONDocument.ReadStream(F:TStream);
变量
len:整数;
类型:字节;
名称:字符串;
开始
清楚的
f、 读取(len,sizeof(len));
f、 读取(elmtype,sizeof(字节));
而elmtype BSON_EOF do//Loop
开始
elmname:=\u ReadString(f);
设置长度(FItems,长度(FItems)+1);//这是TOndrej发现的
案例类型
BSON_数组:FItems[高(FItems)]:=TBSONArrayItem.Create;
BSON_BINARY:FItems[high(FItems)]:=tbsonbinary.Create;
...
结束;
f、 读取(elmtype,sizeof(字节));
结束;
结束;

有哪些替代方案?

不是
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()

的错误用法。另一种方法是使用类似
TList
TObjectList
的列表或通用列表


实际上,您发布的部分(
SetLength(Result.FItems,Length(FItems));
)很好。我相信大卫指的是另一部分:
SetLength(FItems,Length(FItems)+1)
应该增长一个动态数组,但在循环中效率很低。

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

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

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

  • 预先分配阵列。有时,这需要迭代两次,一次用于计数项,一次用于填充数组。这可能是非常有效的。事实上,这正是你问题中的
    TBSONDocument.Clone
    所做的
  • 使用
    TList
    TList
    容器。尽管它们使用动态阵列作为底层存储,但它们实现了基于容量的分配方法。当他们吃饱时,他们会分配大量的额外物品,以备将来添置。这也是非常有效的
  • 还有一个选择,作为最后的手段,是离开连续分配的内存。按块分配内存,并使用类或记录的索引属性在索引与实际块和存储所在位置之间映射。这在避免地址空间碎片方面特别有效

当项目数事先未知时,可以使用另一种方法:每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;

TList使用David所指的非连续内存样式。这是对他的回答的补充

这里有一个现实世界的类比:

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

阵列

停车场里有一排车都被挤满了。这排汽车排成一排。实际车辆存储在数组中。内存中数组的大小是所有实际车辆的大小之和

将汽车添加到阵列中

假设您想添加另一辆车,但这一行没有更多的空间。所以,你必须建造一个更大的新的一排,并将所有的汽车移到新的一排。每次要向行中添加一个时,都必须执行此操作。这是一个数组

列表

如果你把汽车的VIN写在一张纸上,把它们列在一个清单上会怎么样?每个VIN表示停车场中的一辆车。纸上的那张单子是一张单子。它只存储对每辆车的引用

将汽车添加到TList

现在,假设您想在列表中添加一辆汽车。嗯,你可以把车停在任何地方。如果你的车满了,就把它停在阿拉斯加。没关系。只需将新VIN添加到列表中,您就完成了。如果你的纸满了,再拿一张纸。但是,不要移动汽车

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


据推测,length应该存储在其他地方,以避免对length的额外调用。或者你可以写
len=Length(FItems);设置长度(Result.FItems,len);对于i:=0到len-1 do
等。我认为这里的代码不适用于此。David的意思是,你不应该在循环中一次增加一个项目的长度。这段代码将长度设置一次,然后循环,所以应该是好的。@Mikael Eriksson:这段代码不是我的,它来自David猜得很好的项目。你说得对,@Mikael。我对问题进行了编辑,以包含问题代码的准确示例。你的意思是它不会招致任何惩罚吗?在我的示例中,它非常低效。在这种情况下最好使用TList。访问数据集可能会引发异常,因此使用
finally
强制始终执行最后一行非常重要,否则
MyArray
结尾可能包含未从查询中读取的空字符串。另一方面,
TList
使用连续块。旧式的
TList
是我在第三篇文章中描述的方法的极限
SetLength(FItems, Length(FItems)+1);
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;