文章目录
前言
没有扎实的基础,很多编写的程序会随着软件规模的扩大或扩展而产生诸多问题,然后这些程序很可能会被无情的抛弃并重写。而其中的问题可能只是因为一点点的小问题堆积起来,基础可见其重要。本章我们将深入了解经常使用的List。
我曾经在学校学习过链表、列表等数据结构,但实际上当时并没有真正理解,只是简单地复制粘贴代码。我觉得自己的基础很差。后来在工作中遇到一些基础问题或者想要了解某些内部原理时,总是依赖查找资料。如果你也想深入了解C#,我推荐购买《C#图解教程》当作查阅资料。
一、List源码
List是C#中一个最常见的可伸缩数组组件,通常我们在编写程序时代替数组,因为其不用分配数组大小,很是方便。
首先,我们看下内部构造,源码如下:
csharp
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];
// 构建一个列表,该列表最初是空的,容量为零
// 将第一个元素添加到列表后,容量将增加到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继承IList、IReadOnlyList两个接口,list内部其实还是数组实现的,不是链表,初始容量为0。那我们不经思考,我们进行添加操作和删除时内部如何运行?
List源码网址为:官方跳转链接。
IList源码网址为:官方跳转链接。
IReadOnlyList源码网址为:官方跳转链接。
二、Add接口
接口源码如下:
csharp
// 将给定对象添加到此列表的末尾。列表的大小增加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;
}
}
在添加数据的时候首先会检测数组的容量够不够,够就将新的数据进行赋值,不够则调用EnsureCapacity方法增加容量。而在容量不够的时候会进行扩容操作。
csharp
int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
也就是扩充一倍,4变8,8变16,愈演愈烈。那么其优缺点就显而易见了,优点是使用索引的方式提取元素十分方便,缺点是扩容导致的new操作造成内存垃圾,给GC带来很大负担。源码中按照2的指数扩容的方式是为了降低GC负担,如果连续申请扩容,会浪费大量的内存空间;如果数据量大的时候1024直接扩容到2048也会造成大量的内存空间的浪费。怎么解决呢,我们先研究下其他的接口再来做决定。
三、Remove接口
接口源码如下:
csharp
// 删除给定索引处的元素。列表的大小减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++;
}
删除的原理就是使用Array.Copy对数组进行覆盖。而在覆盖之前查找元素索引位置的方法IndexOf,内部实现是按索引顺序从0到n进行比较,复杂度O(n)。
四、Insert接口
接口源码如下:
csharp
// 在给定索引处将元素插入此列表,列表的大小增加1
// 如果需要,在插入新元素之前,列表的容量会增加一倍
public void Insert(int index, T item) {
// 请注意,结尾处的插入是合法的
if ((uint) index>(uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index,
ExceptionResource.ArgumentOutOfRange_ListInsert);
}
Contract.EndContractBlock();
if (_size == _items.Length) EnsureCapacity(_size + 1);
if (index<_size) {
Array.Copy(_items, index, _items, index + 1, _size - index);
}
_items[index] = item;
_size++;
_version++;
}
插入元素时,和Add接口一样先检查容量,不足则扩容。插入时,使用的方法为复制数组的形式,将数组指定元素后面的所有元素向后移动。
五、其他接口
1、[]接口
接口源码如下:
csharp
// 设置或获取给定索引处的元素
public T this[int index] {
get {
// 跟随技巧可以将范围检查减少一半
if ((uint) index>= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
return _items[index];
}
set {
if ((uint) index>= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
Contract.EndContractBlock();
_items[index] = value;
_version++;
}
}
\]接口的实现是直接使用数组的索引方式获取元素。
### 2、Clear接口
接口源码如下:
```csharp
// 清除列表的内容
public void Clear() {
if (_size>0)
{
Array.Clear(_items, 0, _size); // 无须对此进行记录,我们清除了元素,以便gc可以回收引用
_size = 0;
}
_version++;
}
```
源码中清除操作只是对_size设为0,数组没有变化,那实际项目是不是没有必要进行Clear操作呢?当然不是,我们清除的是对数组元素的引用的标记,不清零,垃圾回收器会认为数组元素还是处于引用状态。
### 3、Contains接口
接口源码如下:
```csharp
// 如果指定的元素在List中,则Contains返回true// 它执行线性O(n)搜索。平等是通过调用item.Equals()来确定的
public bool Contains(T item) {
if ((Object) item == null) {
for(int i=0; i<_size; i++)
if ((Object) _items[i] == null)
return true;
return false;
}
else {
EqualityComparer