剖析.NET Framework源码—数据结构List<T> (一)
List<T>类表示可通过索引访问的对象的强类型列表。提供对列表进行搜索,排序和操作的方法。
//List<T>内部使用一个泛型数组T[]来存储元素。为这个内部数组分配的长度就
是List的容量Capacity(最大能存储的元素个数),当向这个List添加新元素
时.如果List已满或无法再添加更多元素,该List的容量就会按照某种机制自动
增长以便容纳更多的新元素,其容量的增长是通过重新分配内部数组T[]实现的。
public class List<T> : IList<T>, System.Collections.IList
{
private const int _defaultCapacity = 4;
private T[] _items;
private int _size;
private int _version;
static T[] _emptyArray = new T[0];
}
以上是List<T>的四个私有字段和一个静态字段:
(1)_items 就是List内部实际存储元素的地方——数组。我们往List里面添加元素,其实就是将元素放到List内部的数组中。
(2)_size 表示内部数组中实际存储的元素个数。注意这个_size与Capacity的区别!
(3)_version可以理解为版本号。这个版本号存在的目的是为了保证在对该List进行迭代的过程中不对其原有元素进行修改。
(4)_defaultCapacity表示该List默认的容量。
(5)_emptyArray就是一个长度为0的空数组!
【构造函数】
接着我们看看List的构造函数:
(1)默认无参构造函数
//构造一个List。这个List初始为空并且容量为0。
public List()
{
_items = _emptyArray;
}
(2)参数表示List的容量的构造函数
//用一个指定的初始容量capacity构造List。
public List(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException();
_items = new T[capacity];
}
(3)用一个实现了IEnumerable接口的collection构造List
//构造一个List,并将collection中的所有元素复制到List中,这个新List 的size和capacity都等于给定collection的大小。
public List(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException();
ICollection<T> c = collection as ICollection<T>;
if (c != null)
{
int count = c.Count;
_items = new T[count];
c.CopyTo(_items, 0);
_size = count;
}
else
{
_size = 0;
_items = new T[_defaultCapacity];
using (IEnumerator<T> en = collection.GetEnumerator())
{
while (en.MoveNext())
{
Add(en.Current);
}
}
}
}
让我们来分析一下这个相对比较复杂的构造函数。首先进行的是参数合法性验证,这里只简单的检查了传入的collection是否为空;紧接着会尝试着将collection转换成ICollection接口,如果转换成功,执行if语句中的代码,转换失败则执行else语句中的代码!
假设转换成功,则说明传入的参数collection是实现了ICollection接口的。接下来的代码很简单,大概就是分配一个数组,该数组的长度就是collection中元素的个数,接着把collection中的元素全部拷贝到新分配的数组中。
如果转换失败,那么执行else语句块。首先会按照默认容量_defaultCapacity分配一个新数组,接着利用collection的枚举器将collection中的元素一个一个添加到新分配的数组中。
【属性】
接着,我们介绍List中几个比较重要的属性
(1)Capacity:代表List的容量。即是其内部数组_items的长度。
//获取或设置该List的容量,当设置时,其内部数组会按照指定value重新分 配。
public int Capacity
{
get { return _items.Length; }
set
{
if (value != _items.Length)
{
if (value < _size)
{
throw new ArgumentOutOfRangeException();
}
if (value > 0)
{
T[] newItems = new T[value]; //reallocate
amp;nbsp; if (_size > 0)
{
Array.Copy(_items, 0, newItems, 0, _size);
}
_items = newItems;
}
else
{
_items = _emptyArray;
}
}
}
}
当设置Capacity时,首先会进行合法性验证,值得注意的是,当要设置的Capacity的值value小于当前List的实际元素个数_size时,会抛出异常!接着,按照value的值重新分配一个新数组newItems,然后把原来数组_items中的所有元素(_size个)全部拷贝到新数组newItems中。旧数组随后被抛弃,等待GC回收。
(2)Count
//只读属性。简单的返回_size,用于描述List中实际有多少个元素。
public int Count
{
get { return _size; }
}
(3)Item:获取或设置指定索引处的元素
public T this[int index]
{
get
{
if ((uint)index >= (uint)_size)
{
throw new ArgumentOutOfRangeException();
}
return _items[index];
}
set
{
if ((uint)index >= (uint)_size)
{
throw new ArgumentOutOfRangeException();
}
_items[index] = value;
_version++;
}
}
以上代码都很简单,值得说明的一点是当每次设置指定索引处的元素值时,字段_version都会加1,表示该List又被修改了一次。其具体作用会在List的迭代器中详细说明。
重要方法】
首先,我们展示两个私有方法:
(1)IsCompatibleObject
//判断对象value的类型是否与数组类型T相容
private static bool IsCompatibleObject(object value)
{
if ((value is T) || (value == null && !typeof(T).IsValueType))
{
return true;
}
return false;
}
可以看到。类型value与数组类型T相容有以下2种情况:
1. value的类型就是T类型或是T类型的子类
2. value为null时,数组类型T必须是引用类型
(2)VerifyValueType
//利用上面的IsCompatibleObject方法进行对象类型相容性检查
private static void VerifyValueType(object value)
{
if (!IsCompatibleObject(value))
{
throw new ArgumentException();
}
}
然后,我们看看在List中比较重要的公共方法:
首先,我们看一个List用于扩展其容量的方法,这个方法很重要,决定了List的性能。
(1)EnsureCapacity
//确保List的容量至少为给定的最小值min。
//如果List的当前容量比给定最小值min小,那 么其容量会增至当前容量的两倍,或是增至最小值min。
private void EnsureCapacity(int min)
{
if (_items.Length < min)
{
int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
上面的代码首先确定List新的容量newCapacity是多少,然后设置List的属性Capacity的值为newCapacity。
newCapacity值的确定过程是很简单却又十分曲折!
1. 如果当前的List是才被new出来,还没有添加任何元素,那么_items.Length等于0,
此时newCapacity的值会被设置为默认容量_defaultCapacity=4。
2. 如果数组中已经有了元素,那么newCapacity的值会被设置为当前容量的两倍。
3. 如果经过计算后newCapacity仍然小于给定的最小值min,那么就直接把newCapacity的值设置为给定的最小值min。