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.mzph.cn/pingmian/5125.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(06)vite与ts的结合

文章目录 系列全集package.json在根目录创建 tsconfig.json 文件在根目录创建 vite.config.ts 文件index.html额外的类型声明 系列全集 &#xff08;01&#xff09;vite 从启动服务器开始 &#xff08;02&#xff09;vite环境变量配置 &#xff08;03&#xff09;vite 处理 c…

R语言 数据整理篇之结构重塑

《Cookbook for R》Manipulating Data ~ Restructuring 数据整理篇之数据结构重塑 Converting between data frames and contingency tables 在数据框和列联表之间转换 表示同样的内容但是三种不同的表现形式&#xff1a; 1、呈现个案观测值的数据框 cases &#xff1a;每行代…

【报错】Could not autowire. No beans of ‘SysUserRoleService‘ type found

1.查看对应service层是否添加Service 2.可以修改成Resource注入&#xff0c; Autowired和Resource区别一个是默认按照类型&#xff0c;一个默认按照名字 3.解决办法是&#xff1a;降低Autowired检测的级别&#xff0c;将Severity的级别由之前的error改成warning或其它可以忽略…

【C++干货基地】探索C++模板的魅力:如何构建高性能、灵活且通用的代码库(文末送书)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

http1.1和http2.0的同源请求数限制

判断协议版本 :scheme: 在请求头中表示使用的是HTTP/2协议。即 出现 :开头的请求头Chrome 只支持查看 HTTP/1.x 的 Raw Headers&#xff0c;对这种请求&#xff0c;会给出 view source 选项。HTTP2.0不给出。可继续学习 https://www.cnblogs.com/kirito-c/p/10360868.html抓包…

App在某个页面静止造成卡死假象

今天遇到一个bug记录下来,供后续积累经验。 预期结果:App在冷启动时会加载一个“广告页”,“广告页”有个定时器,定时器结束后会自动跳转到App首页;然而当用户开启指纹或人脸识别登录之后,App直接进入验证界面,验证通过后直接进入App首页。 bug场景:进入App需要指纹识别…

用什么模型算法可以预测足球胜平负

预测足球胜平负的模型算法有很多种&#xff0c;每种算法都有其特点和适用场景。以下是一些常见的模型算法&#xff1a; Elo预测法&#xff1a; 这是一种通过研究主客场球队在比赛前的积分情况来预测胜负的方法。Elo预测法通过计算两队之间的积分差&#xff0c;根据特定的公式&…

uTorrent Pro一款轻量级的Torrent磁力下载工具去广告绿色版 v3.6.0.47044

01 uTorrent Pro v3.6.0.47044 Torrent是一款俄罗斯号称全球排名第一的免费BT下载工具,海外最受欢迎的BT下载客户端软件。支持UPnP&#xff0c;支持流行的 BT 扩展协议&#xff0c;磁力链接(Magnet Links)&#xff0c;IPv6&#xff0c;用户来源交换&#xff0c;DHT和uTP&#…

【python】python基础-常用方法

通用方法&#xff1a; del obj&#xff1a;删除对象&#xff0c;不能删除可变对象的元素 li [1,2,3,4,5] del li :删除列表 del li[0]:删除列表中第一个元素 max(obj):返回对象中的最大值 字符串方法&#xff1a; 语法&#xff1a;字符串.方法名([参数]) str.upper():将字符…

计算机科学与技术就业方向和前景怎么样

计算机科学与技术专业的就业方向极为广泛&#xff0c;方向可以是软件开发与工程、网络与信息安全、数据科学与大数据分析等&#xff0c;几乎渗透到现代社会的每一个角落。以下是上大学网 &#xff08;www.sdaxue.com)对计算机科学与技术专业一些主要的就业方向及其前景分析&…

ROS2安装问题记录

0. import rclpy出错 安装ros2官网上的教程安装完&#xff0c;发现colcon build后可以通过ros2 run 运行&#xff0c;但是无法通过python直接运行.py文件&#xff0c;原因是在 import rclpy的情况下会报以下错误 The C extension /opt/ros/humble/lib/python3.10/site-packag…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-8.1--C语言LED驱动程序

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

流水线工作流程

java编译命令&#xff1a; java -jar xxx.jar (其它参数已忽略) docker镜像构建命令&#xff1a; docker build -t [镜像名称:latest] -f 指定[Dockerfile] [指定工作目录] 推送镜像 jenkinsfile: 主要流程登录镜像仓库&#xff0c;打包镜像&#xff0c;推送到镜像仓库

情感类ppt素材

小清新手绘插画风毕业季毕业相册同学录画册纪念册PPT下载 - 觅知网这是一张关于清新毕业相册的PPT模板&#xff0c;清新风格设计&#xff0c;加上风为装饰元素&#xff0c;包含毕业相册、毕业季、毕业、同学、纪念等主题内容&#xff0c;也可用作毕业相册PPT、毕业季PPT、毕业P…

Springboot+Vue项目-基于Java+MySQL的校园疫情防控系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

蓝桥杯如何准备国赛?

目录 一、赛前准备 1、如何刷题&#xff0c;刷哪些题&#xff1f; 2、记录&#xff08;主要看个人习惯&#xff09; CSDN博客 写注释 3、暴力骗分 4、从出题人的角度出发&#xff0c;应该如何骗分 二、赛中注意事项 一、赛前准备 1、如何刷题&#xff0c;刷哪些题&…

(51单片机)第十三章-STC系列51单片机功能介绍

13.1 单片机空闲与掉电模式的应用 1. 空闲模式 当单片机进入空闲模式时&#xff0c;除CPU处于休眠状态外&#xff0c;其余硬件全部处于活动状态&#xff0c;芯片中程序未涉及的数据存储器和特殊功能寄存器中的数据在空闲模式期间都将保持原值。假若定时器正在运行&#xff0c;…

第十二章 案例二:配置Trunk,实现相同VLAN的跨交换机通信

1、实验环境 公司的员工人数已达到 100 人&#xff0c;其网络设备如图12.13所示&#xff0c;现在的网络环境导致广播较多网速慢&#xff0c;并且也不安全&#xff0c;公司希望按照部门划分网络&#xff0c;并且能够保证一定的网络安全性 图12.13 实验案例二拓扑图 其网络规划…

node服务器——处理原始的请求流

const express require(express); const fs require(fs); const app express(); const port 3333;app.post(/, (req, res) > {const chunks []; // 用来存储请求体的数据块 req.on(data, (chunk) > {chunks.push(chunk); // 将接收到的数据块添加到数组中 });req.…

KKView远程控制2.0版本发布,TeamViewer面临巨大挑战

KKView远程控制2.0版本发布&#xff0c;TeamViewer面临巨大挑战 近日&#xff0c;备受瞩目的远程控制软件KKView发布了其全新2.0版本&#xff0c;KKView以其独特的创新性和用户友好的设计&#xff0c;为远程办公、远程培训等领域提供了更加高效、便捷的解决方案。 KKView远程…