C#在为我们提供便捷的同时,也遮蔽了很多内部的实现,而研究这些实现能够帮助我们更好的优化程序。本篇主要简单介绍 List < T > 类在使用时的一些注意事项,List < T > 类内部是通过静态数组进行实现的,所以就有会有一下几点问题:
1. 添加元素时数组容量变化
每次添加(Add、Insert)元素时,需要重新评估数组的容量Capacity,如果当前数组的容量不足以容纳新元素,就会创建一个更大的数组,新数组大小 = 当前数组大小 * 2,然后将数组内容拷贝到新数组,此时旧的数组就会成为垃圾等待GC回收。
[__DynamicallyInvokable]
public int Capacity
{
[__DynamicallyInvokable]
get
{
return this._items.Length;
}
[__DynamicallyInvokable]
set
{
if (value < this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
}
if (value != this._items.Length)
{
if (value > 0)
{
T[] array = new T[value];
if (this._size > 0)
{
Array.Copy(this._items, 0, array, 0, this._size);
}
this._items = array;
return;
}
this._items = List<T>._emptyArray;
}
}
}
//评估当前数组的大小,看看是否满足最小长度,如果不满足,则进行扩容
private void EnsureCapacity(int min)
{
if (this._items.Length < min)
{
int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2);
if (num > 2146435071)
{
num = 2146435071;
}
if (num < min)
{
num = min;
}
this.Capacity = num;
}
}
建议:
List< T >默认容量是4,为了避免GC,可以预测列表的大小规模,初始化列表时指定一个合理的容量。
2. 删除元素时进行拷贝
当进行元素删除时,内部会将被移除元素之后的所有元素进行前移,此时就会进行浅拷贝(删除最后一个元素不会进行拷贝,RemoveAll(Predicate< T > match)针对这一点做了优化)。以下是几个Remove函数源码。
public bool Remove(T item)
{
int num = this.IndexOf(item);
if (num >= 0)
{
this.RemoveAt(num);
return true;
}
return false;
}
public void RemoveAt(int index)
{
if (index >= this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
this._size--;
if (index < this._size)
{
Array.Copy(this._items, index + 1, this._items, index, this._size - index);
}
this._items[this._size] = default(T);
this._version++;
}
public void RemoveRange(int index, int count)
{
if (index < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
if (count < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
if (this._size - index < count)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
}
if (count > 0)
{
int size = this._size;
this._size -= count;
if (index < this._size)
{
Array.Copy(this._items, index + count, this._items, index, this._size - index);
}
Array.Clear(this._items, this._size, count);
this._version++;
}
}
public int RemoveAll(Predicate<T> match)
{
if (match == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
int num = 0;
while (num < this._size && !match(this._items[num]))
{
num++;
}
if (num >= this._size)
{
return 0;
}
int i = num + 1;
while (i < this._size)
{
while (i < this._size && match(this._items[i]))
{
i++;
}
if (i < this._size)
{
this._items[num++] = this._items[i++];
}
}
Array.Clear(this._items, num, this._size - num);
int result = this._size - num;
this._size = num;
this._version++;
return result;
}
在删除单个元素上面这几个函数没有太大区别,唯一需要注意的是Remove函数中先调用IndexOf,然后在调用RemoveAt,最坏的情况会执行两边遍历。
在删除多个元素时,会常见到下面类似形式的代码,由于removeAt内部会调用循环拷贝数组,所以下面代码的复杂度为
O(n2)
O
(
n
2
)
。
推荐使用RemoveAll(Predicate< T > match)进行多个元素的删除,值得一读的是RemoveAll 内部实现:
1. 首先找到第一个需要删除的元素的索引 num。
2. 然后将 num 之后不需要删除的元素复制到需要删除的元素之前,这样就保证所要要删除的元素都在列表尾部,这样做的目的是尾部元素的删除,不需要进行拷贝。
3. 最后调用一次Array.Clear函数来完成整个过程。
相对于下面的代码,减少拷贝次数,时间复杂度从
O(n2)
O
(
n
2
)
减少到
O(n)
O
(
n
)
。
for (int i = list.Count-1; i >= 0; i--)
{
if (match(list[i]))
list.RemoveAt(i);
}
其他
待补充