第一章(一) - List 底层源码剖析
提示:个人学习总结,如有错误,敬请指正。
一、List底层
1.New - 构造
List内部是用数组实现,而非链表,并且没有指定指定capacity时,数组大小为0。
2.Add - 添加数据
//将给定对象添加到列表末尾,列表大小+1
//如果需要,在添加新元素前,列表容量增加一倍
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
每次添加新的元素都需要检查容量是否还够,不够就调用EnsureCapacity增加容量。
//如果列表当前容量小于min,则容量将增加到当前容量的两倍或min,以较大者为准
private void EnsureCapacity(int min) {
if (_items.Length < min) {
int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
//在遇到溢出前,允许列表增长到最大可能的容量(大约2GB)
//请注意,即使_items.Length由于Uint强制转换而溢出,此检查仍然有效
if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
在EnsureCapacity()中,每次容量不够,都会扩容一倍,_defaultCapacity表示容量的默认值为4,扩充容量默认为4->8->16->32…
总结:List使用数组形式作为底层数据结构,好处是使用索引方式提取元素很快,但在扩容的时候就会很糟糕,每次new数组都会造成内存垃圾,这给垃圾回收GC带来了很多负担。
3.Remove- 删除数据
Remove接口中包含了 IndexOf 和 RemoveAt,其中用 IndexOf 函数是位了找到元素的索引位置,用 RemoveAt 可以删除指定位置的元素。
简而言之,元素删除的原理其实就是用 Array.Copy 对数组进行覆盖。IndexOf 启用的是 Array.IndexOf 接口来查找元素的索引位置,这个接口本身内部实现是就是按索引顺序从0到n对每个位置的比较,复杂度为O(n)。
4.Insert- 插入数据
与Add接口一样,先检查容量是否足够,不足则扩容。从源码中获悉,Insert插入元素时,使用的用拷贝数组的形式,将数组里的指定元素后面的元素向后移动一个位置。
看到这里,可以我们明白了List的Add,Insert,IndexOf,Remove接口都是没有做过任何形式的优化,都使用的是顺序迭代的方式,如果过于频繁使用的话,会导致效率降低,也会造成不少内存的冗余,使得垃圾回收(GC)时承担了更多的压力。
其他接口也是一样比如AddRange、RemoveRange
5.其他方式
- Sets or Gets:直接使用索引
- Clear::调用时并不删除数组,而是将数组中的所有元素置为默认值(0或者null)然后设置_size为0,用于虚拟地表示当前容量。清零会将对象的引用标记从此集合中移除,从这个角度看,调用Clear清零是必要的。
- Contains : 线性查找方式比较元素,对数组进行迭代,比较每个元素与参数的实例是否一致,如果一致则返回true,全部比较结束还没有找到,则认为查找失败
- ToArray : 重新new了一个指定大小的数组,再将本身数组上的内容考别到新数组上,再返回出来。
- Find: 和Contains一样也是线性查找
- Enumerator: 注意每次获取迭代器时,Enumerator都会被创建(foreach的坑,.Net4.0后已经修复,但仍然不建议大量使用)
- Sort: 快速排序,效率为O(nlgn)。
6.总结
- List 的效率并不高,只是通用性强而已,大部分的算法都使用的是线性复杂度的算法,这种线性算法当遇到规模比较大的计算量级时就会导致CPU的大量损耗。
- List的内存分配方式也不合理,当其中元素不断增加会多次重新分配数组,抛弃原有数组。调用GC时候会造成压力。最好在List初始化的时候就声明Capacity。
- List不是线程安全的,并发情况下无法检查_size++的执行顺序,在多线程情况下使用List时应该注意安全,因此当我们在多线程间使用 List 时加上安全机制。
- List并不高效,但是足够通用