因为最近在面试过程中有问到是否知道List底层是如何实现的,但由于本人非科班出身也只是自学C#,日常也只是使用并没有从底层特地了解过C#很多底层逻辑。感觉这部分知识比较欠缺,所以博客记录一下。
首先List是C#中非常常见的一个数据结构类型,相比于普通数组,他是可以伸缩的,在平常使用中我经常使用它去代替数组,但并不了解其底层是如何做到自动扩展的。接下来分析一下。
剖析底层代码
构造部分:
public class List<T> :
IList<T>,
ICollection<T>,
IEnumerable<T>,
IEnumerable,
IList,
ICollection,
IReadOnlyList<T>,
IReadOnlyCollection<T>
{
private const int _defaultCapacity = 4;
private T[] _items;
private int _size;
private int _version;
[NonSerialized]
private object _syncRoot;
private static readonly T[] _emptyArray = new T[0];
[__DynamicallyInvokable]
public List() => this._items = List<T>._emptyArray;
[__DynamicallyInvokable]
public List(int capacity)
{
if (capacity < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
if (capacity == 0)
this._items = List<T>._emptyArray;
else
this._items = new T[capacity];
}
首先这是List的构造部分,我们可以发现,List内部是用数组实现的,当没有给定初始大小的时候,初始容量为0。因此可以推测出,当我们使用Add或者Remove等方法对List进行扩容的时候,本质上是对现有的数组复制然后放入新生成的数组中。
Add方法:
public void Add(T item)
{
if (this._size == this._items.Length)
this.EnsureCapacity(this._size + 1);
this._items[this._size++] = item;
++this._version;
}
private void EnsureCapacity(int min)
{
if (this._items.Length >= min)
return;
int num = this._items.Length == 0 ? 4 : this._items.Length * 2;
if ((uint) num > 2146435071U)
num = 2146435071;
if (num < min)
num = min;
this.Capacity = num;
}
同个上面的源码中,我们可以发现,每次增加一个元素时,首先会检查容量是否足够,若不足够,会调用
EnsureCapacity()方法来进行扩容。在EnsureCapacity()方法中我们可以发现,当每次容量不足的时候,整个数组会扩容一倍,因此可以发现整个扩容是呈现2的指数次幂。
由此我们可以发现,List其实和数组一样,有这索引快的优点,但同时在扩容方面效果也是一样的,因为每次针对数组进行new的操作都会造成内存垃圾,但是在List中因为是按2的指数扩容,可以略微降低GC的负担,但是若频繁的申请扩容依然会造成较大的GC开销。例如在代码中频繁的使用Add。与此同时,当我们数量设置的不合理的时候会造成内存空间的浪费。例如,当我们List中拥有514个元素的时候,此时List会被扩容到1024个,但我们不使用到后面510个内存空间,这就造成了内存空间的浪费。
Remove方法:
public bool Remove(T item)
{
int index = this.IndexOf(item);
if (index < 0)
return false;
this.RemoveAt(index);
return true;
}
public int IndexOf(T item) => Array.IndexOf<T>(this._items, item, 0, this._size);
public void RemoveAt(int index)
{
if ((uint) index >= (uint) this._size)
ThrowHelper.ThrowArgumentOutOfRangeException();
--this._size;
if (index < this._size)
Array.Copy((Array) this._items, index + 1, (Array) this._items, index, this._size - index);
this._items[this._size] = default (T);
++this._version;
}
Remove()方法中运用了两个函数方法分别是IndexOf(),RemoveAt()。其中IndexOf()是为了查找元素的索引位置,然后使用RemoveAt()可以移除指定位置的元素。
再由RemoveAt()方法中我们可以发现,其删除的原理就是使用Array.Copy对原数组进行覆盖,容器的大小也不会发生改变。
Insert方法:
public void Insert(int index, T item)
{
if ((uint) index > (uint) this._size)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert);
if (this._size == this._items.Length)
this.EnsureCapacity(this._size + 1);
if (index < this._size)
Array.Copy((Array) this._items, index, (Array) this._items, index + 1, this._size - index);
this._items[index] = item;
++this._size;
++this._version;
}
该方法和Add一样,首先需要检查当前容量是否足够,若不足则扩容。同时我们可以发现,在使用Insert进行插入时,使用的是复制数组的形式,将数组中指定位置的元素后面的所有元素都向后移一位。
对以上三个方法剖析后,我们可以发现在List中这些增加,移除,查找,插入的方法都是没有做过任何形式优化的,若我们使用的过于频繁,效率会变低的同时也会造成不少的内存冗余和导致CG的压力变大。
小结
当我们再看其他List的方法时候,我们还可以发现List的效率并不高,同时它的内存分配也不合理,当List元素不断增加的时候,就会重新分配更长长度的数组,那么每次就会重新开辟一段新的内存空间,原来的内存空间就会被GC回收,那么就造成了GC回收的压力。当然我们可以在每次创建List的时候对List长度进行初始化。
此外List的线程是不安全的,其没有对多线程做任何加锁或其他同步操作。由于并发的情况下我们无法判断_size++的执行顺序,因此在多线程中使用List应该加上安全机制。文章来源:https://www.toymoban.com/news/detail-736817.html
最后,List只是一个兼容性强的容器而不是一个高效的容器,反而比数组的效率还要低。文章来源地址https://www.toymoban.com/news/detail-736817.html
到了这里,关于关于C#List底层原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!