List <T>真的是C#中的秘密数组吗?

Ant*_*Tun 34 .net c#

我一直在使用ILSpy查看.NET库,并List<T>System.Collections.Generic命名空间中遇到了类定义.我看到该类使用类似这样的方法:

// System.Collections.Generic.List<T>
/// <summary>Removes all elements from the <see cref="T:System.Collections.Generic.List`1" />.</summary>
public void Clear()
{
    if (this._size > 0)
    {
        Array.Clear(this._items, 0, this._size);
        this._size = 0;
    }
    this._version++;
}
Run Code Online (Sandbox Code Playgroud)

所以,类的Clear()方法List<T>实际上使用Array.Clear方法.我已经看到许多其他List<T>方法在体内使用Array的东西.

这是否意味着List<T>实际上是一个秘密数组或List只使用了一些Array方法?

我知道列表是类型安全的,不需要装箱/拆箱,但这让我有点困惑.

Dav*_*nan 44

列表类本身不是数组.换句话说,它不是从数组派生的.相反,它封装了一个数组,该实现用于保存列表的成员元素.

由于List<T>提供对其元素的随机访问,并且这些元素被索引0..Count-1,因此使用数组来存储元素是显而易见的实现.

  • 它不是"升级阵列".它是一个列表,实现IList,并支持List样式的操作,如枚举,插入,追加等.它的*内部实现*是一个数组,但就是这样 - 一个内部实现细节.如果它被实现为链接列表,它将暴露相同的接口和相同的功能,只是不同的性能特征. (22认同)
  • 哦,了解List的性能特征非常重要,这样您就可以正确使用它.但是*它不是数组*.它不是升级的阵列.它是一个`List`,它的实现基于一个数组. (6认同)

Han*_*ant 31

这往往会让知道std :: list的C++程序员感到惊讶.一个链表,包含在.NET中以及LinkedList类.并且具有相同的性能特征,O(1)用于插入和删除.

但是你通常应该避免它.链接列表在现代处理器上表现不佳.这在很大程度上依赖于cpu缓存来获得合理的性能,其内存比执行核心慢很多倍.到目前为止,一个简单的数组是最有利于缓存的数据结构.访问元素给出了很高的可能性,即后续元素也存在于缓存中.对于链表而言情况并非如此,元素往往分散在整个地址空间中,可能导致缓存未命中.它们可能非常昂贵,多达200个周期,而cpu只是在等待内存子系统提供数据.

但是请记住perf特性,添加或删除不在List成本O(n)末尾的元素,就像数组一样.由于需要扩展阵列,因此大型List会产生大量垃圾,预先设置Capacity属性可以帮助避免这种情况.在这个答案中更多关于这一点.而对于std :: vector <>则完全相同.


Jer*_*odd 21

是的,List<T>在内部使用数组来存储项目,尽管在大多数情况下,数组实际上大于集合中的元素数量 - 它在末尾有一些额外的"填充",这样你就可以添加新的项目而不需要它每次重新分配内存.它使用单独的字段跟踪集合的实际大小(您可以this._size在生成的代码中看到).当您添加的元素多于当前数组所具有的空间时,它会自动分配一个新的更大的数组 - 我认为是两倍大 - 并复制所有现有元素.

如果您担心List<T>使用的内存超过必要的数量,可以使用接受参数的构造函数覆盖显式设置数组的大小capacity,如果您事先知道大小,或者调用TrimExcess()方法以确保数组是(接近)到集合的实际大小.

  • @PieterGeerkens不*平均插入*时间.这意味着在大多数情况下,人们可以在恒定时间内插入任何索引.相反,过度分配的动态数组提供在Omega(1)时间附加的*摊销的最坏情况*.也就是说,即使在阵列上最糟糕的操作序列中,每个"append"调用的分摊时间也是不变的:虽然任何一个调用都可能需要线性时间,但在此之后你可以经常在O(1)时间内追加它最终会平衡. (4认同)
  • 值得补充的是,动态_growing一个数组的标准方法是在每次增加时将其大小加倍,在数组大小上保持_linear_,从而继续提供O(1)_average_插入时间. (3认同)