Unity List底层源码剖析

文章目录

  • 前言
  • 一、List源码
  • 二、Add接口
  • 三、Remove接口
  • 四、Insert接口
  • 五、其他接口
    • 1、[]接口
    • 2、Clear接口
    • 3、Contains接口
    • 4、ToArray接口
    • 5、Find接口
    • 6、Enumerator接口
    • 7、Sort接口
  • 六、线程安全
  • 总结


前言

没有扎实的基础,很多编写的程序会随着软件规模的扩大或扩展而产生诸多问题,然后这些程序很可能会被无情的抛弃并重写。而其中的问题可能只是因为一点点的小问题堆积起来,基础可见其重要。本章我们将深入了解经常使用的List。

我曾经在学校学习过链表、列表等数据结构,但实际上当时并没有真正理解,只是简单地复制粘贴代码。我觉得自己的基础很差。后来在工作中遇到一些基础问题或者想要了解某些内部原理时,总是依赖查找资料。如果你也想深入了解C#,我推荐购买《C#图解教程》当作查阅资料。


一、List源码

List是C#中一个最常见的可伸缩数组组件,通常我们在编写程序时代替数组,因为其不用分配数组大小,很是方便。

首先,我们看下内部构造,源码如下:

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接口

接口源码如下:

// 将给定对象添加到此列表的末尾。列表的大小增加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方法增加容量。而在容量不够的时候会进行扩容操作。

 int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;

也就是扩充一倍,4变8,8变16,愈演愈烈。那么其优缺点就显而易见了,优点是使用索引的方式提取元素十分方便,缺点是扩容导致的new操作造成内存垃圾,给GC带来很大负担。源码中按照2的指数扩容的方式是为了降低GC负担,如果连续申请扩容,会浪费大量的内存空间;如果数据量大的时候1024直接扩容到2048也会造成大量的内存空间的浪费。怎么解决呢,我们先研究下其他的接口再来做决定。

三、Remove接口

接口源码如下:

// 删除给定索引处的元素。列表的大小减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接口

接口源码如下:

// 在给定索引处将元素插入此列表,列表的大小增加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、[]接口

接口源码如下:

// 设置或获取给定索引处的元素
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接口

接口源码如下:

// 清除列表的内容
public void Clear() {
    if (_size>0)
    {
        Array.Clear(_items, 0, _size); // 无须对此进行记录,我们清除了元素,以便gc可以回收引用
        _size = 0;
    }
    _version++;
}

源码中清除操作只是对_size设为0,数组没有变化,那实际项目是不是没有必要进行Clear操作呢?当然不是,我们清除的是对数组元素的引用的标记,不清零,垃圾回收器会认为数组元素还是处于引用状态。

3、Contains接口

接口源码如下:

// 如果指定的元素在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<T>c = EqualityComparer<T>.Default;
        for(int i=0; i<_size; i++) {
            if (c.Equals(_items[i], item)) return true;
        }
        return false;
    }
}

查找操作也是使用线性的比较判断一致性。

4、ToArray接口

接口源码如下:

// ToArray返回一个新的Object数组,其中包含List的内容
// 这需要复制列表,这是一个O(n)操作
public T[] ToArray() {
    Contract.Ensures(Contract.Result<T[]>() != null);
    Contract.Ensures(Contract.Result<T[]>().Length == Count);

    T[] array = new T[_size];
    Array.Copy(_items, 0, array, 0, _size);
    return array;
}

ToArray接口是转化数组的接口,她重新创建了一个指定大小的数组,然后进行复制操作,如果使用过多,就会造成大量内存的分配,在内存上留下很多无用的垃圾,所以不要频繁使用尤其是在循环当中。

5、Find接口

接口源码如下:

public T Find(Predicate<T>match) {
    if( match == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    Contract.EndContractBlock();

    for(int i = 0 ; i<_size; i++) {
        if(match(_items[i])) {
            return _items[i];
        }
    }
    return default(T);
}

Find接口是查找接口,同样是线性查找方式,复杂度为O(n)。

6、Enumerator接口

接口源码如下:

// 返回具有给定删除元素权限的此列表的枚举数
// 如果在进行枚举时对列表进行了修改,
// 则枚举器的MoveNext和GetObject方法将引发异常
public Enumerator GetEnumerator() {
    return new Enumerator(this);
}

/// 仅供内部使用
IEnumerator<T>IEnumerable<T>.GetEnumerator() {
    return new Enumerator(this);
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
    return new Enumerator(this);
}

[Serializable]
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator
{
    private List<T>list;
    private int index;
    private int version;
    private T current;

    internal Enumerator(List<T>list) {
        this.list = list;
        index = 0;
        version = list._version;
        current = default(T);
    }

    public void Dispose() {
    }

    public bool MoveNext() {

        List<T>localList = list;

        if (version == localList._version && ((uint)index<(uint)localList._size))
        {
            current = localList._items[index];
            index++;
            return true;
        }
        return MoveNextRare();
    }

    private bool MoveNextRare()
    {
        if (version != list._version) {
            ThrowHelper.ThrowInvalidOperationException(
                ExceptionResource.InvalidOperation_EnumFailedVersion);
        }

        index = list._size + 1;
        current = default(T);
        return false;
    }

    public T Current {
        get {
            return current;
        }
    }

    Object System.Collections.IEnumerator.Current {
        get {
            if( index == 0 || index == list._size + 1) {
                ThrowHelper.ThrowInvalidOperationException(
                    ExceptionResource.InvalidOperation_EnumOpCantHappen);
            }
            return Current;
        }
    }

    void System.Collections.IEnumerator.Reset() {
        if (version != list._version) {
            ThrowHelper.ThrowInvalidOperationException(
                ExceptionResource.InvalidOperation_EnumFailedVersion);
        }

        index = 0;
        current = default(T);
    }

}

Enumerator接口是枚举迭代部分细节的接口,每次获取迭代器时,Enumerator都会被创建出来,如果大量使用迭代器,比如foreach,就会产生大量的垃圾对象。所以尽量少用foreach。

7、Sort接口

接口源码如下:

// 对列表中一部分元素进行排序
// 排序使用给定的IComparer接口对元素进行比较
// 如果comparer为null,则使用IComparable接口对元素进行比较
// 在这种情况下,该接口必须由列表中的所有元素实现
// 
// 此方法使用Array.Sort方法对元素进行排序
public void Sort(int index, int count, IComparer<T>comparer) {
    if (index<0) {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index,
            ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
    }

    if (count<0) {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count,
            ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
    }

    if (_size - index<count)
        ThrowHelper.ThrowArgumentException(
            ExceptionResource.Argument_InvalidOffLen);
    Contract.EndContractBlock();

    Array.Sort<T>(_items, index, count, comparer);
    _version++;
}

Sort接口是排序接口,它使用了Array.Sort接口进行排序。
Array.Sort接口使用快速排序方式进行排序,从而使我们明白了List的Sort排序的效率为O(nlgn)。

internal static void DepthLimitedQuickSort(T[] keys, int left, int right,
    IComparer<T>comparer, int depthLimit)
{
    do
    {
        if (depthLimit == 0)
        {
            Heapsort(keys, left, right, comparer);
            return;
        }

        int i = left;
        int j = right;

        // 先对低、中(枢轴)和高三种值进行预排序
        // 面对已经排序的数据或由多个排序后的行程组成的数据,
        // 这可以提高性能
        int middle = i + ((j - i)>>1);
        SwapIfGreater(keys, comparer, i, middle);       // 用中间点与低点交换
		SwapIfGreater(keys, comparer, i, j);    // 用高点与低点交换
        SwapIfGreater(keys, comparer, middle, j);       // 用中间点与高点交换

        T x = keys[middle];
        do
        {
            while (comparer.Compare(keys[i], x)<0) i++;
            while (comparer.Compare(x, keys[j])<0) j--;
            Contract.Assert(i>= left && j<= right, "(i>=left && j<=right)
                Sort failed - Is your IComparer bogus?");
            if (i>j) break;
            if (i<j)
            {
                T key = keys[i];
                keys[i] = keys[j];
                keys[j] = key;
            }
            i++;
            j--;
        } while (i<= j);

        // while循环的下一个迭代是“递归”对数组的较大部分进行排序,
        // 随后的调用将会对较小的部分进行递归排序
        // 因此,我们在此处对depthLimit自减一,以便两种排序都能看到新值
        depthLimit--;

        if (j - left<= right - i)
        {
            if (left<j) DepthLimitedQuickSort(keys, left, j, comparer, depthLimit);
            left = i;
        }
        else
        {
            if (i<right) DepthLimitedQuickSort(keys, i, right, comparer, depthLimit);
            right = j;
        }
    } while (left<right);
}

也就是说,List的内部接口使用的是顺序迭代的方式,如果频繁使用,效率就会降低,造成内存的冗余,GC压力倍增。好处也是显而易见的,通用性强大。
项目中想要优化,请根据项目的数据着重在原本的线性算法;分配数组时进行预估以便在开始时进行预设或者改写扩容方式。

六、线程安全

最后提一点,List是线程不安全的,没有考虑多线程加锁或者同步的情况,在并发情况无法判断_size++的执行顺序,因此多线程中不使用List或使用时加上安全机制。


总结

列表是一种灵活、高效的数据结构,适用于各种场景下的数据管理和操作。希望在今后的项目中,大家能够以此为基础,勇于创新、灵活应用,从而不断改进和优化我们的代码结构,提升软件的质量和效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/584325.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

rtl8188ftv debian linux 多架构移植方法

5 块包邮&#xff0c;挂到 x86_64 debian 12 虚拟机&#xff0c;实测下载能到 22Mbps&#xff0c;也可能就2Mbps&#xff0c;上传能到 40Mbps 关键词&#xff1a; rtl8xxxu、rtl8xxxu.ko、rtl8xxxu_8188f.c、mac80211.h、cfg80211.ko、sudo modinfo rtl8xxxu.ko | grep depen…

【Qt】error LNK2001: 无法解析的外部符号

参考&#xff1a;Qt/VS LNK2019/LNK2001&#xff1a;无法解析的外部符号_qt lnk2001无法解析的外部符号-CSDN博客 微软官方报错文档-链接器工具错误 LNK2019 __declspec error LNK2001: 无法解析的外部符号 "__declspec(dllimport) 原因 以这种为前缀的基本上跟库相关…

用Scrapy编写第一个入门项目(基础四件套:spider,pipeline,setting,items)

简介&#xff1a;scrapy是一个用于爬取网页并提取数据的应用框架&#xff0c;也可用于提取API数据 写在前面&#xff1a;只想看scrapy的童鞋子请跳过5-7直接step8&#xff09; step5&#xff0c;6是xpath和css入门&#xff0c;用于提取数据&#xff1b; step7是文件储存方式&…

SoundStream: 下一代的神经网络音频编解码器,实时压缩不牺牲音质

音频编解码技术的目标是&#xff0c;通过减少音频文件的大小来节省存储空间或减轻网络传输的负担。理想的情况下&#xff0c;即使音频被压缩&#xff0c;我们听到的声音与原版也应该没有任何区别。 过去&#xff0c;已经有不少编解码技术被开发出来&#xff0c;满足了这些需求…

【VS Code安装及远程服务器】(未完待续)

目录 一、Python 安装及设置1.1 Python安装1.2 Python设置 二、VScode 安装2.1 VScode安装2.2 中文界面设置及解决中文显示乱码问题2.2.1 中文界面设置2.2.2 解决中文显示乱码问题 2.3 VScode环境变量配置2.4 VScode添加到右键2.5 VScode终端&#xff0c;创建、激活虚拟环境&am…

Docker-Compose单机多容器应用编排与管理

前言 Docker Compose 作为 Docker 生态系统中的一个重要组件&#xff0c;为开发人员提供了一种简单而强大的方式来定义和运行多个容器化应用。本文将介绍 Docker Compose 的使用背景、优劣势以及利用 Docker Compose 简化应用程序的部署和管理。 目录 一、Docker Compose 简…

数据结构复习指导之串

文章目录 串 考纲内容 复习提示 1.串的定义和实现 1.1串的定义 1.2串的存储结构 1.2.1定长顺序存储表示 1.2.2堆分配存储表示 1.2.3块链存储表示 2.串的基本操作 拓展 知识回顾 串 考纲内容 字符串模式匹配 复习提示 本章是统考大纲第6章内容,采纳读者建议单独作为…

ActiveMQ 反序列化漏洞 (CVE-2015-5254)

一、漏洞描述 Apache ActiveMQ 是由美国阿帕奇&#xff08;Apache&#xff09;软件基金会开发的开源消息中间件&#xff0c;支持 Java 消息服务、集群、Spring 框架等。属于消息队列组件(消息队列组件&#xff1a;分布式系统中的重要组件&#xff0c;主要解决应用耦合、异步消息…

宽字符的来历:从ASCII到Unicode,C语言中的宽字符处理

目录 一、ASCII编码&#xff1a;字符世界的开篇 二、Unicode与宽字符的诞生 宽字符类型与宽字符串 三、C语言中的宽字符处理函数 四、宽字符与多字节字符 结语 在计算机科学的发展历程中&#xff0c;字符编码经历了从简单到复杂、从单一语言到全球多语种支持的演变过程。…

十大落地护眼灯有哪些?2024十大落地灯品牌排名

十大落地护眼灯有哪些&#xff1f;想要让孩子在舒适敞亮的光线下学习&#xff0c;不少家长都会给孩子选择入手落地灯&#xff0c;不过市面上却流传着落地灯品质恶劣的负面新闻。我是一名专业测评家居博主&#xff0c;终于搞清楚落地灯负面新闻的原因&#xff0c;其原因主要是因…

回顾python

回顾python 目录 回顾python 1.定义变量 2.分支控制结构 3.for循环 4.while 循环 5.类 面向对象 &#xff11;&#xff09;​方法的定义&#xff1a; &#xff12;&#xff09;类的定义&#xff1a; &#xff13;&#xff09;类的继承 1.定义变量 a23b"张三&quo…

【NOI-题解】1607. 两位数运算1020. 算算和是多少1029. 倒序输出一个四位整数1418. 求一个5位数的各个位之和1608. 三位数运算

文章目录 一、前言二、问题问题&#xff1a;1607. 两位数运算问题&#xff1a;1020. 算算和是多少问题&#xff1a;1029. 倒序输出一个四位整数问题&#xff1a;1418. 求一个5位数的各个位之和问题&#xff1a;1608. 三位数运算 三、感谢 一、前言 本章节主要讲解基本运算中的…

在线商城客服系统,多用户电商系统可API对接客服软件

在当今数字化时代&#xff0c;在线商城客服系统和多用户电商系统之间的无缝API对接已成为电商行业的重要趋势。这种整合为商家提供了更高效的客户服务和管理方式&#xff0c;提升了用户体验和业务效率。其中&#xff0c;商淘云电商客服系统作为一款强大的客服管理工具&#xff…

react props传参

props是父子传参的常用方法。 一、主要功能 1.传参 定义&#xff1a;父级组件向子级组件传递参数。 2.验证数据类型格式 定义&#xff1a;可以指定父组件传递过来数据为指定类型。 3.设置默认值 定义&#xff1a;在参数未使用时&#xff0c;直接默认为指定值。 二、实例代…

OpenSceneGraph

文章目录 关于 OpenSceneGraphScreenshots - OpenMW 关于 OpenSceneGraph 官网&#xff1a;https://openscenegraph.github.io/openscenegraph.io/github : https://github.com/openscenegraph/OpenSceneGraphClasses : https://podsvirov.github.io/osg/reference/opensceneg…

Android系统的硬件抽象层

硬件抽象层 Author: cpu_codeDate: 2020-07-12 22:20:34LastEditTime: 2020-07-13 22:52:02FilePath: \notes\android_bottom\hardware_abstraction_layer.mdGitee: https://gitee.com/cpu_codeGithub: https://github.com/CPU-CodeCSDN: https://blog.csdn.net/qq_44226094Gi…

后端如何处理接口的重复调用

首先是&#xff0c;原理在请求接口之前&#xff0c;使用过滤器拦截数据&#xff0c;来进行判断两次数据是否一致。 1.自定义注解 2.创建一个Handler处理器 3.RepeatSubmitInterceptor的实现类 4.过滤器的配置

thinkphp6 workerman无法使用框架Db/model等类库方法解决方案

thinkphp6 workerman无法使用框架Db/model相关操作解决 执行安装相关扩展 composer require webman/gateway-worker引入成功后编辑服务类文件,直接展示代码 <?phpnamespace app\server\controller;use GatewayWorker\BusinessWorker; use GatewayWorker\Gateway; use Gate…

从0到1手写注册中心Registry之核心接口设计

一. 数据模型 InstanceMeta用于描述服务实例的元信息&#xff1a; schema&#xff1a;比如httphost,&#xff1a;比如127.0.0.1port&#xff1a;比如8082context&#xff1a;比如midnight-rpcstatus&#xff1a;服务上下线&#xff0c;true/falseParameters: 服务携带的参数&…

React 第十一章 Dva

Dva 是一个基于 redux 和 redux-saga 的数据流方案&#xff0c;然后为了简化开发体验&#xff0c;dva 还额外内置了 react-router 和 fetch&#xff0c;所以也可以理解为一个轻量级的应用框架。 Dva 的本意&#xff0c;是将基于 React 技术栈中常用到的库集成到一起。当时&…
最新文章