Unity中List的底层源码剖析

1、List底层代码剖析

List是C#中一个最常见的可伸缩数组组件,我们常用它来代替数组。因为它是可伸缩的,所以我们在编写程序的时候不用手动去分配大小,接下来我们来看看list的底层实现。

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];

    //构建一个列表,该列表最初是空的,容量为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内部使用数组实现的,而不是链表,并且当没有给予指定容量时,初始的容量为0。也就是说,List组件在被Add()、Remove()两个函数调用时,都是采用“在数组上对元素进行转移的操作,或者从原数组复制生成到新数组”的方式工作的

2.Add接口剖析

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
    }
}

上述List源码中的Add函数,每次增加一个元素的数据,Add接口都会首先检查容量够不够,如果不够就调用EnsureCapacity函数来增加容量。每次容量不够的时候,整个数组的容量都会扩大一倍,_defaultCapacity表示容量默认大小为4,因此扩充路线为4、8、16、32、64、128、256、512、1024...........以此类推。

List使用数组形式作为底层数据结构,优点是使用索引方式获取元素很快。缺点是扩容时会很糟糕,每次针对数组进行new操作都会造成内存垃圾,这会给垃圾回收(GC)带来很大负担。

这里以2的倍数扩容的方式可以为GC减轻负担,但是如果数组被连续申请扩容,还是会造成GC的不小负担,特别是代码中的List频繁使用Add时。此外如果数量使用不当,会浪费大量内存空间,例如当元素的数量为520时,List会被扩容到1024个元素,如果不使用剩余的504个空间单位,就会造成大部分内存空间的浪费。

3.Remove接口剖析

下面为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++;
}

Remove函数中包含IndexOf和RemoveAt函数,其中使用IndexOf函数是为了找到元素的索引位置,使用RemoveAt可以删除指定位置的元素。

从源码中可以看到,元素删除的原理就是使用Array.Copy对数组进行覆盖。IndexOf是用Array.IndexOf接口来查找元素的索引位置,这个接口本身的内部实现就是按索引顺序从0到n对每个位置进行比较。

猜你喜欢

转载自blog.csdn.net/qq_42720695/article/details/124951471