Array
面对大量的数据需要存放,或者我们需要一次性遍历大量数据,我们会采取使用Array数组。Array数组是一个存储相同类型元素的固定大小的顺序集合。
Array的优点:在内存中连续存储,遍历对象方便,可以根据索引快速锁定对象。
Array的缺点:定长,在使用的时候长度是固定的,不能后期增长或缩短。
ArrayList
因为Array定长的原因,所以出现了ArrayList,这是一个动态数组,可以根据需要调整数组长度。ArrayList采用Object存储对象,所以它可以存储不同类型的对象。
ArrayList的优点:可以动态调整长度。
ArrayList的缺点:因为是用Object存对象,在存和使用值类型的对象时或发生装箱/拆箱的操作。个位数或几十个影响可能不大,但是我们使用数组就是为了方便管理大量对象,对象过多的时带来的性能影响会很大,并且可能会产生垃圾。
“万能”的List
所以我们需要一个能动态调整长度,并且不会发生装箱/拆箱的一个新的数据结构。List就应运而生了。
List是一个泛型集合,可以等效ArrayList,但是因为泛型的缘故,所以List不会产生装箱/拆箱的问题。List内部实现使用的是Array数组,并且也继承了IList接口。所以List在内存中的存储也是连续的,List也可以像ArrayList一样动态调整长度。
“万能”
List“万能”因为它能执行增删改查的所有操作,在删除元素后,还可以自动移动元素保证数组元素的连续性,能Foreach遍历,能Sort排序,能Contains判断是否包含元素。似乎List把一切对元素的操作都集成了。
会而不精
做一个不恰当的比喻:回家过年,有的人坐飞机回家,有的坐火车回家。飞机比火车块多了,那为什么还是很多人选择坐火车?飞机是快,但是飞机票贵。相比之下火车速度也不是很慢,票价也便宜。那对于List来说,它什么都能做,但是又都做不好。
每次插入或删除新的元素都需要重新移动一次元素,假设把第一个删除或者在第一个位置插入一个新的元素。那么List就需要遍历一遍数组把数组的每一个元素向前或者向后挪动一位。最坏的情况下List插入和删除的时间复杂度为O(n)。在插入和删除上效率不如LinkedList链表。
List查找元素是从头遍历,符合的就会返回,不符合继续向后找,直到结尾。最好的情况下,第一个元素就是我们要找的,但是最坏的情况下,不仅找到末尾才找到,List没有要查找的元素也会完整遍历一遍。查找的时间复杂度为O(n)。在查找的效率上没有Dictionary高。
List还有一个缺点,就是在它的Add方法上。下面是List里的一些字段。_defaultCapacity是List的默认长度。_items就是存放元素的数组。_size为元素的个数
public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
{
private const int _defaultCapacity = 4;
private T[] _items;
[ContractPublicPropertyName("Count")]
private int _size;
private int _version;
[NonSerialized]
private Object _syncRoot;
static readonly T[] _emptyArray = new T[0];
这是List的Add方法。在添加元素的时候会先进行一个判断,判断_size和_items的长度是否一样,一样说明数组已经存满了,需要进行扩容就会执行EnsureCapacity方法。
EnsureCapacity方法就是在计算扩容后的数组长度,如果在new List的时候没有传入大小,_items.Length=0,List会将数组扩容到默认大小,否则扩容到原来数组长度的2倍。
扩容计算后的长度会赋给Capacity 属性,在Set中很明显能看到重新new了一个新长度的数组,并把旧数组Copy过去。所以在内存中就是出现一个垃圾数组。假如在数组中存储了几百万的个元素,List正好扩容,一个有着几百万元素的垃圾数组就出现了,并且Copy几百万个元素也是个消耗性能的事情。
并且,
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
private void EnsureCapacity(int min) {
if (_items.Length < min) {
int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
// Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
public int Capacity {
get {
Contract.Ensures(Contract.Result<int>() >= 0);
return _items.Length;
}
set {
if (value < _size) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
}
Contract.EndContractBlock();
if (value != _items.Length) {
if (value > 0) {
T[] newItems = new T[value];
if (_size > 0) {
Array.Copy(_items, 0, newItems, 0, _size);
}
_items = newItems;
}
else {
_items = _emptyArray;
}
}
}
}
学习数据结构的重要性就出现了,因为“万能”的List“学术不精”,所以要学会选择合适的数据结构。在我最开始的学习中,以为一个List可以打天下,随意使用,导致后面性能受到影响。
个人总结的注意事项:
在能确定长度的情况下选择Array,在不确定长度的情况下优先选择List。
在插入删除较少的环境下使用List,使用List可以自己先指定一个合适的大小。