关于C#List底层原理

这篇具有很好参考价值的文章主要介绍了关于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只是一个兼容性强的容器而不是一个高效的容器,反而比数组的效率还要低。文章来源地址https://www.toymoban.com/news/detail-736817.html

到了这里,关于关于C#List底层原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 关于C#中的HashSet<T>与List<T>

    HashSetT 表示值的集合。这个集合的元素是无须列表,同时元素不能重复。由于这个集合基于散列值,不能通过数组下标访问。 ListT 表示可通过索引访问的对象的强类型列表。内部是用数组保存数据,不是链表。元素可重复,是有序列表,根据调用add的时间先后进行排序。每次

    2024年01月24日
    浏览(34)
  • Unity List相关问题

    1、list随机数值,重复的数量不超过指定大小。 2、获取list集合中,相同数量最少的一个元素 3、将list集合顺序打乱

    2024年02月11日
    浏览(31)
  • Redis追本溯源(二)数据结构:String、List、Hash、Set、Zset底层数据结构原理

    Redis 并没有直接用 C 语言的字符串,而是自己搞了一个 sds 的结构体来表示字符串,这个 sds 的全称是 Simple Dynamic String,翻译过来就是“简单的动态字符串”。 安全的二进制存储 资源。关于sds的扩容和缩容下面会进行详细的介绍,这里先不赘述了。 在一些情况中,我们需要

    2024年02月16日
    浏览(55)
  • 深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)&&模拟实现正反向迭代器【容器适配器模式】

    1.一个模板参数 在模拟实现list之前,我们要理解list中的迭代器是如何实现的。 在vector中迭代器可以看成一个指针,指向vector中的数据。它的解引用会访问到具体的数据本身,++会移动到下一个数据位置上去,这些都是因为vector具有天生的优势:空间上是连续的数组,这样指

    2024年02月15日
    浏览(46)
  • 数组(Arrays)和列表(Lists) — Unity C#

    数组是 C# 提供的最基本的集合。将它们视为一组值的容器,在编程术语中称为元素,每个值都可以单独访问或修改。 · 数组可以存储任何类型的值;所有元素必须属于同一类型。 · 数组的长度或元素数量是在创建时设置的。 · 如果创建时没有指定初始值,则每个元素都会被

    2024年01月23日
    浏览(67)
  • [Unity] 基于迭代器的协程底层原理详解

    Unity 是单线程设计的游戏引擎, 所有对于 Unity 的调用都应该在主线程执行. 倘若我们要实现另外再执行一个任务, 该怎么做呢? 答案就是协程. 协程本质上是基于 C# yield 迭代器的, 使用 yield 语法生成的返回迭代器的方法, 其内部的逻辑执行, 是 “懒” 的, 只有在调用 MoveNext 的时

    2024年01月16日
    浏览(43)
  • 解密list的底层奥秘

    🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨ 🐻强烈推荐优质专栏: 🍔🍟🌯C++的世界(持续更新中) 🐻推荐专栏1: 🍔🍟🌯C语言初阶 🐻推荐专栏2: 🍔🍟🌯C语言进阶 🔑个人信条: 🌵知行合一 金句分享: ✨即使人生还有无数次失望的可能性,✨ ✨但我还是想活在理想主义的乌托邦

    2024年02月08日
    浏览(30)
  • (C++) list底层模拟实现

     个人主页:Lei宝啊  愿所有美好如期而遇 首先,list底层是一个带头双向循环链表,再一个,我们还要解决一个问题,list的迭代器,vector和string的迭代器可以直接++,是因为他们的地址空间是连续的,而链表不是,所以链表的迭代器封装的不是原生指针,我们需要想办法解决

    2024年01月21日
    浏览(45)
  • 【STL】list用法&试做_底层实现

    目录 一,list  使用 1. list 文档介绍  2. 常见接口 1.   list中的sort 2. list  + sort 与 vector  + sort效率对比 3. 关于迭代器失效 4. clear 二,list 实现 1.框架搭建  2. 迭代器类——核心框架 3. operator-  实现  4. const——迭代器 5. insert 6. erase 7. clear——实现 8. 拷贝构造  首先实现迭

    2024年02月16日
    浏览(38)
  • C++ stl容器list的底层模拟实现

    目录 前言: 1.创建节点 2.普通迭代器的封装 3.反向迭代器的封装 为什么要对正向迭代器进行封装? 4.const迭代器 5.构造函数 6.拷贝构造 7.赋值重载 8.insert 9.erase 10.析构 11.头插头删,尾插尾删 12.完整代码+简单测试 总结: 1.创建节点 注意给缺省值,这样全缺省就会被当做默认

    2024年04月23日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包