为什么Generics.Collections.TObjectList.List不安全? [英] Why is Generics.Collections.TObjectList.List unsafe?
问题描述
TList
和 TOjectList
位于 Generics.Collections
中有一个 .List
属性,这是一个枚举器。 例如:
oList:= TObjectList< TItem> .Create;
//将项目添加到oList
用于oList.List中的项目do begin
//用Item
做一些事情end;
这很整齐,但后果很严重。 .List
只读 FList
( TList
和 TObjectList
),它只是一个 arrayofT
。
由于动态数组的大小增加了一倍,所以每当一个项目被添加超过其大小时,这意味着它有空间用于未使用的项目。
如果您添加了3个 TItem
s,实际 FList
长度为4个项目,第四个(也是最后一个)项目为 nil
。
因此,使用 TObjectList
的 .List
不安全,因为如果 TObjectList
没有 .Count,它可能会抛出访问冲突
值为2(例如1,2,4,8,16等)。
以下代码可能会抛出访问冲突:
用于oList.List中的项目do begin
Writeln(Item.ClassName);
end;
当然,安全的解决方案是使用 .Count
:
for I:= 0 to oList.Count - 1 do begin
Item:= oList.Items [I];
Writeln(Item.ClassName);
end;
这与枚举器并不相同。 (当然,你也可以检查 Item
是否是 nil
。)
我的问题是这样的:
- 为什么
.List
isn' t是一个实际的枚举器? -
TList / TObjectList
以下是 TForm $ c $的示例c>(其中
btn1
只需添加一行, mmo1
是 TMemo $ c
$ b
procedure TForm2.btn1Click(Sender:TObject);
var
Line:string;
begin
Line:='Line';
mmo1.Lines.Add(Line);
fList.Add(Line);
mmo1.Lines.Add(Format('Count:%d; Actual length:%d',[fList.Count,Length(fList.List)]));
for line in fList.List do begin
mmo1.Lines.Add(Format('Found:%s',[Line]));
end;
end;
现在,使用 string
不会抛出访问冲突。但是,当我点击3次,我得到以下内容:
Count:3;实际长度:4
找到:线
找到:线
找到:线
找到:
TList< T>
和TOjectList< T>
在中Generics.Collections
有一个List
属性,这是一个枚举器。
没有那么简单。 List
属性是一个动态数组。动态数组支持枚举。
由于动态数组的大小在其大小增加一倍时会增加一倍,意味着它有空间用于未使用的物品。
这也不正确。动态数组不会自动调整大小。他们必须通过调用 SetLength
来显式调整大小。 TList< T>
类通过调用 SetLength
来管理存储列表内容的底层动态数组的容量。 / p>
为什么
List
不是实际的枚举器?
<$ c $如果我记得,c> List 属性最近被添加了,在XE3中。其目的是允许直接访问可能没有其他方式的底层列表。
有时候为了提高效率,如果您想修改列表的内容,您可能更愿意避免使用副本。例如,假设您的列表包含大小为1KB的记录。如果您想修改每条记录中的单个布尔值,而不使用 List
属性,则最终将复制整个1KB记录两次。只是为了修改一个布尔值。
当然,获得底层存储访问的成本是您可以接触到内部实现细节。 Embarcadero可能已经实现了以更安全的方式为您提供访问权限的功能,但无论出于何种原因他们选择了此路线。我想我可能做了一个索引属性,返回一个指向项目的指针。
所以,这是 List
属性的唯一用例。除非您确实需要直接访问底层存储,否则请勿使用 List
。当然,如果有文档可以解释这一点,那就太好了,但它就是这样。
是否
TList< T>
有一个实际的枚举器?
是的。 b
$ b
var
Item:SomeType;
MyList:TList< SomeType>;
....
用于MyList中的Item do
Item.Foo();
TList
and TOjectList
in Generics.Collections
have a .List
property, which is an enumerator.
For instance:
oList := TObjectList<TItem>.Create;
// Add items to oList
for Item in oList.List do begin
// Do something with Item
end;
This is neat, but has a drastic consequence. .List
just reads FList
(a private declaration on TList
and TObjectList
), which is merely an arrayofT
.
Since a dynamic array is doubled in size whenever an item is added beyond its size, it means it has space for non-used items.
In case you have added 3 TItem
s, the actual FList
is 4 items long, with the fourth (and last) item being nil
.
Thus using TObjectList
's .List
is unsafe, because it will likely throw an access violation, if your TObjectList
doesn't have a .Count
value with a power of 2 (e.g. 1, 2, 4, 8, 16, etc.).
The following code could likely throw an access violation:
for Item in oList.List do begin
Writeln(Item.ClassName);
end;
Of course, the safe solution is a simple iteration using .Count
:
for I := 0 to oList.Count - 1 do begin
Item := oList.Items[I];
Writeln(Item.ClassName);
end;
This is just not as pretty as the enumerator. (You can also check if Item
is nil
, of course.)
My questions are thus:
- Why
.List
isn't an actual enumerator? - And does
TList
/TObjectList
have an actual enumerator?
Here is an example from a TForm
(where btn1
simply adds a line and mmo1
is a TMemo
).
procedure TForm2.btn1Click(Sender: TObject);
var
Line: string;
begin
Line := 'Line';
mmo1.Lines.Add(Line);
fList.Add(Line);
mmo1.Lines.Add(Format('Count: %d; Actual length: %d', [fList.Count, Length(fList.List)]));
for Line in fList.List do begin
mmo1.Lines.Add(Format('Found: "%s"', [Line]));
end;
end;
Now, using string
does not throw an access violation. But when I have clicked 3 times, I get the following:
Count: 3; Actual length: 4
Found: "Line"
Found: "Line"
Found: "Line"
Found: ""
TList<T>
andTOjectList<T>
inGenerics.Collections
have aList
property, which is an enumerator.
No that is not so. The List
property is a dynamic array. And dynamic arrays have built in support for enumeration.
Since a dynamic array is doubled in size whenever an item is added beyond its size, it means it has space for non-used items.
That is also not true. Dynamic arrays are not automatically resized. They have to be explicitly resized with a call to SetLength
. The TList<T>
class manages capacity of the underlying dynamic array that stores the list content using calls to SetLength
.
Why is
List
not an actual enumerator?
The List
property was added recently, in XE3 if I recall. Its purpose is to allow direct access to the underlying list which is possible no other way.
Sometimes for the sake of efficiency you might prefer to avoid taking copies if you want to modify the content of the list. For instance suppose your list contains records that are 1KB in size. If you want to modify a single boolean within each record, without using the List
property, you end up copying the entire 1KB record twice. Just to modify one boolean.
Of course the cost for gaining access to the underlying storage is that you are exposed to the internal implementation details. Embarcadero could have implemented functionality that would give you this access in a safer way but for whatever reason they chose this route. I think I might have made an indexed property that returns a pointer to an item.
So, that is really the only use case for the List
property. Unless you really need direct access to the underlying storage, do not use List
. Of course, it would have been nice had the documentation bothered to explain any of this, but there it is.
Does
TList<T>
have an actual enumerator?
Yes it does.
var
Item: SomeType;
MyList: TList<SomeType>;
....
for Item in MyList do
Item.Foo();
这篇关于为什么Generics.Collections.TObjectList.List不安全?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!