Unity List底层源码剖析

文章目录


前言

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

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


一、List源码

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

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

csharp 复制代码
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接口

接口源码如下:

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

csharp 复制代码
 int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;

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

三、Remove接口

接口源码如下:

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

接口源码如下:

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

接口源码如下:

csharp 复制代码
// 设置或获取给定索引处的元素
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接口 接口源码如下: ```csharp // 清除列表的内容 public void Clear() { if (_size>0) { Array.Clear(_items, 0, _size); // 无须对此进行记录,我们清除了元素,以便gc可以回收引用 _size = 0; } _version++; } ``` 源码中清除操作只是对_size设为0,数组没有变化,那实际项目是不是没有必要进行Clear操作呢?当然不是,我们清除的是对数组元素的引用的标记,不清零,垃圾回收器会认为数组元素还是处于引用状态。 ### 3、Contains接口 接口源码如下: ```csharp // 如果指定的元素在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 { EqualityComparerc = EqualityComparer.Default; for(int i=0; i<_size; i++) { if (c.Equals(_items[i], item)) return true; } return false; } } ``` 查找操作也是使用线性的比较判断一致性。 ### 4、ToArray接口 接口源码如下: ```csharp // ToArray返回一个新的Object数组,其中包含List的内容 // 这需要复制列表,这是一个O(n)操作 public T[] ToArray() { Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().Length == Count); T[] array = new T[_size]; Array.Copy(_items, 0, array, 0, _size); return array; } ``` ToArray接口是转化数组的接口,她重新创建了一个指定大小的数组,然后进行复制操作,如果使用过多,就会造成大量内存的分配,在内存上留下很多无用的垃圾,所以不要频繁使用尤其是在循环当中。 ### 5、Find接口 接口源码如下: ```csharp public T Find(Predicatematch) { 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接口 接口源码如下: ```csharp // 返回具有给定删除元素权限的此列表的枚举数 // 如果在进行枚举时对列表进行了修改, // 则枚举器的MoveNext和GetObject方法将引发异常 public Enumerator GetEnumerator() { return new Enumerator(this); } /// 仅供内部使用 IEnumeratorIEnumerable.GetEnumerator() { return new Enumerator(this); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return new Enumerator(this); } [Serializable] public struct Enumerator : IEnumerator, System.Collections.IEnumerator { private Listlist; private int index; private int version; private T current; internal Enumerator(Listlist) { this.list = list; index = 0; version = list._version; current = default(T); } public void Dispose() { } public bool MoveNext() { ListlocalList = 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接口 接口源码如下: ```csharp // 对列表中一部分元素进行排序 // 排序使用给定的IComparer接口对元素进行比较 // 如果comparer为null,则使用IComparable接口对元素进行比较 // 在这种情况下,该接口必须由列表中的所有元素实现 // // 此方法使用Array.Sort方法对元素进行排序 public void Sort(int index, int count, IComparercomparer) { if (index<0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } if (count<0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } if (_size - index(_items, index, count, comparer); _version++; } ``` Sort接口是排序接口,它使用了Array.Sort接口进行排序。 Array.Sort接口使用快速排序方式进行排序,从而使我们明白了List的Sort排序的效率为O(nlgn)。 ```csharp internal static void DepthLimitedQuickSort(T[] keys, int left, int right, IComparercomparer, 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

相关推荐
“抚琴”的人16 小时前
【机械视觉】C#+VisionPro联合编程———【六、visionPro连接工业相机设备】
c#·工业相机·visionpro·机械视觉
owde17 小时前
顺序容器 -list双向链表
数据结构·c++·链表·list
第404块砖头17 小时前
分享宝藏之List转Markdown
数据结构·list
omegayy17 小时前
Unity 2022.3.x部分Android设备播放视频黑屏问题
android·unity·视频播放·黑屏
FAREWELL0007518 小时前
C#核心学习(七)面向对象--封装(6)C#中的拓展方法与运算符重载: 让代码更“聪明”的魔法
学习·c#·面向对象·运算符重载·oop·拓展方法
tadus_zeng18 小时前
Windows C++ 排查死锁
c++·windows
EverestVIP18 小时前
VS中动态库(外部库)导出与使用
开发语言·c++·windows
CodeCraft Studio18 小时前
Excel处理控件Spire.XLS系列教程:C# 合并、或取消合并 Excel 单元格
前端·c#·excel
勘察加熊人20 小时前
forms实现连连看
c#
hvinsion20 小时前
PPT助手:一款集计时、远程控制与多屏切换于一身的PPT辅助工具
c#·powerpoint·ppt·ppt助手·ppt翻页