关于C#List底层原理

因为最近在面试过程中有问到是否知道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应该加上安全机制。

最后,List只是一个兼容性强的容器而不是一个高效的容器,反而比数组的效率还要低。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值