1、List底层代码剖析
List是C#中一个最常见的可伸缩数组组件,我们常用它来代替数组。因为它是可伸缩的,所以我们在编写程序的时候不用手动去分配大小,接下来我们来看看list的底层实现。
public class list<T> :IList <T>,System.Collections.IList, IReadOnlyList <T>
{
private const int _ defaultCapacity = 4;
private T[] _items;
private int _size;
private int _version;
private Object _syncRoot;
static readonly T[] _emptyArray = new T[0];
//构建一个列表,该列表最初是空的,容量为0
//将第一个元素添加到列表后,容量将增加到16,然后根据需要以2的倍数增加
public List()
{
_items = _emptyArray;
}
//构造具有给定容量的List。该列表最初为空的。但是在需要重新分配之前,会为给定数量的元素流出空间
public List(int capacity)
{
if(capacity < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(
ExceptionArgument.capacity,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
Contract.EndContractBlock();
if(capacity == 0)
_items = _emptyArray;
else
_items = new T[capacity];
}
//******其他内容
}
从构造函数部分可以知道List内部使用数组实现的,而不是链表,并且当没有给予指定容量时,初始的容量为0。也就是说,List组件在被Add()、Remove()两个函数调用时,都是采用“在数组上对元素进行转移的操作,或者从原数组复制生成到新数组”的方式工作的。
2.Add接口剖析
Add接口源码如下
//将给定对象添加到此列表的末尾。列表的大小增加1
//如果需要,再添加新元素之前,列表的容量会增加1
public void Add(T item)
{
if(_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
//如果列表的当前容量小于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
}
}
上述List源码中的Add函数,每次增加一个元素的数据,Add接口都会首先检查容量够不够,如果不够就调用EnsureCapacity函数来增加容量。每次容量不够的时候,整个数组的容量都会扩大一倍,_defaultCapacity表示容量默认大小为4,因此扩充路线为4、8、16、32、64、128、256、512、1024...........以此类推。
List使用数组形式作为底层数据结构,优点是使用索引方式获取元素很快。缺点是扩容时会很糟糕,每次针对数组进行new操作都会造成内存垃圾,这会给垃圾回收(GC)带来很大负担。
这里以2的倍数扩容的方式可以为GC减轻负担,但是如果数组被连续申请扩容,还是会造成GC的不小负担,特别是代码中的List频繁使用Add时。此外如果数量使用不当,会浪费大量内存空间,例如当元素的数量为520时,List会被扩容到1024个元素,如果不使用剩余的504个空间单位,就会造成大部分内存空间的浪费。
3.Remove接口剖析
下面为Remove接口的源码
//删除给定索引处的元素。列表的大小减1
public bool Remove(T item)
{
int index = IndexOf(item);
if(index >= 0)
{
RemoveAt(index);
return true;
}
return false;
}
//返回此列表范围内给定值首次出现的索引
//该列表从头到尾向前搜索
//使用Object.Equals方法将列表中的元素与给定值进行比较
//此方法使用Array.IndexOf方法进行搜索
public int IndexOf(T item)
{
Contract.Ensures(Contract.Result<int>() >= -1)
Contract.Ensures(Contract.Result<int>() < Count)
return Array.IndexOf(_items, item, 0, _size);
}
//删除给定索引处的元素,列表的大小减1
public void RemoveAt(int index)
{
if((uint)index >= (uint)_size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
_size--;
if(index < _size)
{
Array.Copy(_items, index + 1, _items, index, _size - index);
}
_items[_size] = default(T);
_version++;
}
Remove函数中包含IndexOf和RemoveAt函数,其中使用IndexOf函数是为了找到元素的索引位置,使用RemoveAt可以删除指定位置的元素。
从源码中可以看到,元素删除的原理就是使用Array.Copy对数组进行覆盖。IndexOf是用Array.IndexOf接口来查找元素的索引位置,这个接口本身的内部实现就是按索引顺序从0到n对每个位置进行比较。