【系统】Unity3D中的AssetBundle资源管理器(二)

前言

在之前的那片文章上写了我们可以根据Asset的依赖,来做ab包的加载。这样可以在Asset不在用的时候及时释放内存中的资源,减少内存的峰值。也可以减少一次性回收资源太多,导致cpu峰值。

正文

设计思路

准备阶段

首先我们需要在打资源的时候做一次资源相互依赖的配置。然后把这份配置放置在StreamingAssets下,然后可以动态热更下来。在使用的时候,我们先根据ab包里总的Minifest来把每个ab包中有那些资源给读取出来,然后再根据资源依赖关系配置,我们就可以把这两个文件关联起来,asset->depedencyAssets->assetbundle。

加载阶段

根据这样的关系我们在加载的时候,当需要加载一个asset的时候,我们可以去读取它所关联的depedencyAssets资源,然后判断这个资源有在被使用,如果被使用了,我们就给他的引用计数+1;如果没有我们就根据这个asset去找它在哪个assetbundle中,然后把这个assetbundle给加载出来,再看下它所引用的asset有没有加载出来,加载出来就引用计数加1。在这里我们可以看到,只在第一次加载的时候会去给它所依赖的Asset做一次引用计数+1,因为依赖的Asset只需要去关心它是不是被依赖了,而不需要关心依赖它的那个资源被加载几次;因为只要它还在被引用,那就自然而然依赖的资源就不能被回收释放的。

卸载阶段

当我们需要卸载一块资源的时候,我们首先要做的就是,把这个资源的引用计数给-1,然后当他已经完全没有被引用了,那我们就去寻找它所依赖的资源,然后也都减1;然后我们查找这个资源所在的assetbundle包,去遍历它里面的Asset是不是都不被引用了,如果都不被引用了,我们就给这个资源做个标记要被回收的;不立刻被回收而是去做标记的好处就是,当我们的下一次还要用ab包里的资源的时候,我们可以不用再去加载ab包了,直接使用从这个ab包里拿资源,然后把这个ab包在状态给修改回去。我们将找一个空余的时间去把所有不用的ab包给回收掉。

关注点


这张图我们看到没有我们要加载的资源

这张图是我们第一次加载资源的时候它所被引用到的对象
这个时候当我们调用Resources.UnloadAsset卸载资源,它就会变成第一张图,这个资源就从内存里消失了,然后我们不去卸载ab包,第二次我们直接调用资源加载,我们看到的情况和之前是一样的。
但是,如果我们这个时候把ab包给卸载了,然后重新加载ab再加载asset之后,我们将看到这样的资源依赖图

这个时候我们再去调用Resources.UnloadAsset卸载资源你会看到资源没有被依赖,但是没有从内存中消失。

这个时候我们要卸载这个图片必须得调用assetbundle.Unload(true)来卸载,或者调用Resources.UnloadUnusedAssets()这来卸载,后一种卸载方式是会循环遍历所有的资源然后去卸载,所有会有很大的消耗;前一种的卸载方式是把资源依赖给强行解放了,所以当你实际上资源还在使用的话,就会变紫色,丢资源。
在这种Asset没有被回收的情况下,我们assetbundle没有回收,然后我们再从assetbundle中把资源加载出来,实际上使用的资源还是这个,它会被重新被引用上。但是如果我们调用了assetbundle.Unload(false)来把ab给回收了,但是没有去调用Resources.UnloadUnusedAssets()来回收asset,那你会发现这个丢失引用的资源就成了冗余资源,我再次从新加载ab,加载asset的时候,就是重新生成一份资源了

就成这样的情况了。

总结

源码地址
我们在做asset的资源管理的时候,一般来说直接调用true来做一次强行的资源回收,但是如果需要很在意表现,不想表现穿帮,对于一些资源就算冗余也无所谓的情况下,那最后调用一次Resources.UnloadUnusedAssets()来做一次资源的回收兜底行为。下一次我是做场景加载呢,还是做一个资源打包编辑器呢?看时间吧!鸽的前兆。

【编程基础】c#中的List源码解析

前言

我们在使用一些组件的时候,很容易在不了解它底层实现的情况下,使用出一些性能的问题。作为一个良好的程序员来说,能在在遇到这种问题的时候给出解决方案,算是一个基本的职业素质。所以我们需要在使用组件的时候知其所以然。

为什么需要了解List

List 这个组件是一个可伸缩的数组组件,我们会常常用它来代替数组,因为是可伸缩的,所以我们使用的地方太多了。

需要了解哪些东西

  1. 增加元素
  2. 删除元素
  3. 修改元素
  4. 怎么伸缩(这个其实在上面应该会说)

源码地址

正文

构造部分

首先我们来看下List的构造部分,源码如下:

public class List<T> : IList<T>,System.Collections.IList,IReadOnlyList<T>
{
    private const int _DefaultCapacity = 4;
    private T[] _items;
    [ContractPublicPropertyName("Count")]
    private int  _size;
    private int _version;
    [NoSerialized]
    private Object _syncRoot;
    static readonly T[] _emptyArray = new T[0];
    public List(){
        _items = _emptyArray;
    }
    public List(int capacity){
        if(capacity<0) ThrowHelper.ThrowArgumentOutOfRangeException(ExcptionArgument.capacity,ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        Contract.EndContractBlock();
        if(capacity == 0)
            _items = _emptyArray;
        else
            _items = new T[capacity];
    }
    public List(IEnumerable<T> collection){
        if (collection==null)
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
            Contract.EndContractBlock();
            ICollection<T> c = collection as ICollection<T>;
            if( c != null) {
                int count = c.Count;
                if (count == 0)
                {
                    _items = _emptyArray;
                }
                else {
                    _items = new T[count];
                    c.CopyTo(_items, 0);
                    _size = count;
                }
            }
            else {
                _size = 0;
                _items = _emptyArray;
                using(IEnumerator<T> en = collection.GetEnumerator()) {
                    while(en.MoveNext()) {
                        Add(en.Current);
                    }
                }
            }
        //...
        //其他内容
    }
}

从源码中我们可以知道,List继承IList,和IReadOnlyList这两个接口,IList是提供主要的接口,IReadOnlyList提供了迭代接口。
我们还能知道List内部是用数组来实现的,它有三种构造方法,一个是啥都不传的,默认初始化一个长度0的数组,一个是初始化数组长度的构造函数,一个是复制给定集合内容的构造函数,长度和内容都和传进来的数组一样。

添加

当我们看到第三种构造函数的时候,我们会发现最下面有一个Add这个函数的调用,这个函数就是List中添加。
源码如下:

public void Add(T item){
    if(_size == _item.Length) ExsureCapacity(_size + 1);
    _item[_size++] = item;
    _version++;
}
private void EnsureCapacity(int min){
    if(_item.Length < min){
        int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
        if((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
        if(newCapacity < min) newCapacity = min;
        Capacity = newCapacity;
    }
}
public int Capacity{
    get{
        Contract.Ensures(Contract.Result<int>() >= 0);
        return _item.Length;
    }
    set{
        if(value < _size){
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }
        Contract.EndContractBlock();
        if(value != _items.Length){
            if(value > 0){
                T[] newItems = new T[value];
                if(_size > 0){
                    Array.Copy(_items,0,newItems,0,_size);
                }
                _items = newItems;
            }else{
                _items = _emptyArray;
            }
        }
    }
}

从上面的源码我们可以看出来,当List在Add一个item的时候,会先去判断这个数组是不是已经满了,如果满了就拓容,从4,16,32 ...n(上一次数组大小)*2 这样的大小来拓容的,当拓容之后,会新建一个新的数组,把原来数组内容给拷贝进去,然后再去添加新的元素。

  1. 如果你直接调用Capacity赋值数组商都为负数的时候,你会出现数组被清理成默认的,但是数组的标记size(这个代表当前添加到数组第几个位置)却没有清空,下一次你要用的时候,会浪费之前那么多的内存空间。

  2. 当你在添加一堆数据的时候(例如128个数据)在一开始没有给List赋值长度的话,它就会在你添加的时候进行六次扩容,然后这六次扩容中有五次是需要拷贝元素数据的,这样每次拷贝都会造成新的内存垃圾,这样就会给GC带来很多负担。

  3. 如果添加的数量不得当,也会照成内存空间的浪费,比如元素数量为520的时候,List就会扩容到1024个元素,这样就会多出502个空间单位没有被使用。

删除

删除我们主要看Remove这个函数
源码如下:

public bool Remove(T item){
    int index = indexOf(item);
    if (index>=0){
        RemoveAt(index);
        return true;
    }
    return false;
}
public int indexof(T item){
    Contract.Ensures(Contract.Result<int>() >= -1);
    Contract.Ensures(Contract.Result<int>() < Count);
    return Array.IndexOf(_items,item,0,_size);
}
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);
    }
    _item[size] = default(T);
    _version++;
}

Remove接口中包含了IndexOf和RemoveAt,这两个方法,IndexOf是为了定位要删除的元素的位置,这个接口的实现是按索引从0到n个位置做意义比较,时间复杂度为O(n);RemoveAt这个方法是用来删除指定位置的元素,但是它的实现是用Array.Copy来进行数组的覆盖。

插入

源码如下:

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

从源码中我们可以看到,Insert和Add接口一样,都会先检查容量是否足够,如果不够就扩容,插入的位置是否是数组的最后,如果不是进行数组拷贝,将index后面的元素集体后移一位。
在这里我们可以很清楚一个概念,就是最坏的情况下,我们插入一个数据需要做2次数据拷贝,会造成GC时的压力。

思考

其他相关的接口比如AddRange,RemoveRange,InsertRange的原理其实和Add,Remove,Insert一样,区别就是从单个元素变成以容易为单位的操作,都是做一些重复覆盖的操作,和扩容。他们没有进行任何形式的优化,都是使用顺序迭代的方式。如果频繁调用,会照成不小的内存冗余和GC的压力。

其他的一些接口说明

比如[]的实现

public T this[int index] {
            get {
                // Following trick can reduce the range check by one
                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++;
            }
        }

这个就是直接使用数组的的索引方式来获取元素,赋值元素,

Clear清除接口

public void Clear() {
            if (_size > 0)
            {
                Array.Clear(_items, 0, _size); // Don't need to doc this but we clear the elements so that the gc can reclaim the references.
                _size = 0;
            }
            _version++;
        }

调用的时候不会去删除数组内的元素,只是将元素清零,然后设置_size为0;

Contains接口

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

我们看到它就是做了一次线性比较的方式来对数组进行迭代查找,并且返回的是第一个比对上的元素。

ToArray接口

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

它的实现就是新建一个数组,然后把元素拷贝进去再返回出来。

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

同样适用的是线性查找,对每个元素都进行比较,时间复杂度为O(n).

Enumerator 迭代的实现

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);
        }
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这个接口在每次被获取的时候都是new出来的,如果使用大量的迭代器的时候就会有大量的对象,照成GC压力

Sort接口

public void Sort()
        {
            Sort(0, Count, null);
        }
 public void Sort(IComparer<T> comparer)
        {
            Sort(0, Count, comparer);
        }
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++;
        }

        public void Sort(Comparison<T> comparison) {
            if( comparison == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.EndContractBlock();

            if( _size > 0) {
                IComparer<T> comparer = new Array.FunctorComparer<T>(comparison);
                Array.Sort(_items, 0, _size, comparer);
            }
        }
// Array.Sort
internal void Sort(int left, int length)
            {
#if FEATURE_CORECLR
                // Since QuickSort and IntrospectiveSort produce different sorting sequence for equal keys the upgrade 
                // to IntrospectiveSort was quirked. However since the phone builds always shipped with the new sort aka 
                // IntrospectiveSort and we would want to continue using this sort moving forward CoreCLR always uses the new sort.

                IntrospectiveSort(left, length);
#else
                if (BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
                {
                    IntrospectiveSort(left, length);
                }
                else
                {
                    DepthLimitedQuickSort(left, length + left - 1, IntrospectiveSortUtilities.QuickSortDepthThreshold);
                }
#endif
            }
             private void DepthLimitedQuickSort(int left, int right, int depthLimit)
            {
                // Can use the much faster jit helpers for array access.
                do
                {
                    if (depthLimit == 0)
                    {
                        // Add a try block here to detect IComparers (or their
                        // underlying IComparables, etc) that are bogus.
                        try
                        {
                            Heapsort(left, right);
                            return;
                        }
                        catch (IndexOutOfRangeException)
                        {
                            throw new ArgumentException(Environment.GetResourceString("Arg_BogusIComparer", comparer));
                        }
                        catch (Exception e)
                        {
                            throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                        }
                    }

                    int i = left;
                    int j = right;

                    // pre-sort the low, middle (pivot), and high values in place.
                    // this improves performance in the face of already sorted data, or 
                    // data that is made up of multiple sorted runs appended together.
                    int middle = GetMedian(i, j);

                    // Add a try block here to detect IComparers (or their
                    // underlying IComparables, etc) that are bogus.
                    try
                    {
                        SwapIfGreaterWithItems(i, middle); // swap the low with the mid point
                        SwapIfGreaterWithItems(i, j);      // swap the low with the high
                        SwapIfGreaterWithItems(middle, j); // swap the middle with the high
                    }
                    catch (Exception e)
                    {
                        throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                    }
                    Object x = keys[middle];
                    do
                    {
                        // Add a try block here to detect IComparers (or their
                        // underlying IComparables, etc) that are bogus.
                        try
                        {
                            while (comparer.Compare(keys[i], x) < 0) i++;
                            while (comparer.Compare(x, keys[j]) < 0) j--;
                        }
                        catch (IndexOutOfRangeException)
                        {
                            throw new ArgumentException(Environment.GetResourceString("Arg_BogusIComparer", comparer));
                        }
                        catch (Exception e)
                        {
                            throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e);
                        }
                        Contract.Assert(i >= left && j <= right, "(i>=left && j<=right)  Sort failed - Is your IComparer bogus?");
                        if (i > j) break;
                        if (i < j)
                        {
                            Object key = keys[i];
                            keys[i] = keys[j];
                            keys[j] = key;
                            if (items != null)
                            {
                                Object item = items[i];
                                items[i] = items[j];
                                items[j] = item;
                            }
                        }
                        i++;
                        j--;
                    } while (i <= j);

                    // The next iteration of the while loop is to "recursively" sort the larger half of the array and the
                    // following calls recrusively sort the smaller half.  So we subtrack one from depthLimit here so
                    // both sorts see the new value.
                    depthLimit--;

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

我们可以看到使用的排序方法是用的快排,所以它的时间复杂度为O(nlogn)

总结

在分析完之后,我们可以知道List的效率并不高,只是通用性强,方便而已,大部分的算法都是使用线性复杂度的算法,这种算法遇到规模比较大的计算量级的时候就会照成大量的CPU的消耗。而且它的内存分配方式极其的不合理,只要元素不断的增加就会多次new数组,然后抛弃原来的数组,造成GC压力。所以List是一个好用但不高效的组件。

[算法]归并排序

概念

使用的思想是分治思想。把需要排序的数组中重建分成两部分,然后前后两部分分别排序,需要一直分解下去,直到能两个值相互比较。然后再将排序好的两个部分合并在一起。

合并

我们需要申请一个临时数组,这个数组的大小和我们排序的两个组的大小之和一样。然后我们用两个下标i,j,来标识我们当前读到的是两个组的第几个位置,然后这两个下标的值来做比较,但一个值满足要求的时候,我们就把这个值放在临时数组里,然后这个下标后移一位,继续比较。当任意一个下标到达那个组的最后一位的时候,我们就不需要做比较了,直接把另外一组剩下的放在临时数组的最后。

void merge_sort_c(int [] a,int p,int r)
            {
                if (p >= r)
                    return;
                int q = (p + r) / 2;
                merge_sort_c(a,p, q);
                merge_sort_c(a, q + 1, r);
                merage(a, p, q, r);
            }
            void merage(int[]a,int p,int q,int r)
            {
                int i = p;
                int j = q+1;
                int k = 0;
                int[] temp = new int[r - p+1];
                while (i <= q && j <= r)
                {
                    if (a[i] <= a[j])
                    {
                        temp[k++] = a[i++];
                    }
                    else
                    {
                        temp[k++] = a[j++];
                    }
                }
                int start = i;
                int end = q;
                if (j <= r)
                {
                    start = j;
                    end = r;
                }
                while (start <= end)
                {
                    temp[k++] = a[start++];
                }
                for(int ii = 0; ii < r - p+1; ii++)
                {
                    a[p + ii] = temp[ii];
                }
            }
  1. 当我们把第二组的多出来的数据整体放在第一组后面,那归并排序是稳定排序

  2. 归并排序的时间复杂度o(nlogn)

  3. 归并排序的空间复杂度不是原地排序,因为在合并的时候需要临时空间。o(n)

[算法]选择排序

概念

也分为未排序区和排序区。但是选择排序每次都从未排序中找到我们要的元素放在排序区的最后。

int[] t = new int[10] { 43, 23, 432, 5, 2, 6, 43, 2, 67, 4 };
                for(int i = 0; i < t.Length; i++)
                {
                    int v = t[i];
                    int index = i;
                    for (int j = t.Length-1; j > i; --j)
                    {
                        if (v > t[j])
                        {
                            index = j;
                            v = t[j];
                        }
                    }
                    t[index] = t[i];
                    t[i] = v;
                }
                for(int i = 0; i < t.Length; i++)
                {
                    Console.WriteLine("{0}={1}", i, t[i]);
                }
  1. 因为只会交换一次我们找到满足条件的数据放在已排序的最后,所以这个排序的空间复杂度为o(1),是原地排序算法

  2. 这个排序不是稳定排序算法,因为每次都会去找剩下满足的元素和前面的元素进行交换

  3. 最坏,最好,和平均时间复杂度都是o(n*n).

[算法]插入排序

概念

插入排序就是把数组分为两个区间,一个是有序区间,一个是未排序区间。初始的时候有序区间就只有一个元素,那就是数组的第一个元素,然后取未排序区间中的元素往有序区间中插进去,一直等到未排序区间中的所有元素为空。

int[] t = new int[10] { 54, 23, 43, 2, 3, 5, 1, 2, 6, 7 };
                for (int i = 0; i < t.Length; i++)
                {
                    int v = t[i];
                    int j = i - 1;
                    for (; j > 0; --j)
                    {
                        if (v > t[j])
                        {
                            t[j + 1] = t[j];
                        }
                        else
                        {
                            break;
                        }
                    }
                }
                for(int i = 0; i < t.Length; i++)
                {
                    Console.WriteLine("{0}={1}", i, t[i]);
                }
  1. 插入排序算法不需要额外的空间,所以它的空间复杂度为o(1),是原地排序算法。

  2. 插入的时候只是插到它所需要的位置,不会修改它后面位置的顺序,所以是稳定排序算法。

  3. 最好情况下,我们要排序的数据是有序的,所以我们只需要做o(n)的时间复杂度。最坏的情况下,我们要排序的数据是逆序的,所以我们要做o(nn)的时间复杂度。在数组中插入一个数据的时间平均时间复杂度是o(n),我们需要执行n次操作,所以平均时间复杂度是o(nn)。

[算法]冒泡排序

概念

每次冒泡就是相邻的两个数进行比较,如果两个数据的关系不满足我们所要求的的,那我们就进行交换。一次冒泡操作至少会让一个元素移动到它所应该在的位置。

int[] t = new int[10] { 2, 3, 4, 2, 43, 54, 23, 43, 1, 67};
for(int i = 0; i < t.Length; i++)
    {
      for(int j = 0; j < t.Length-i-1; j++)
          {
              if (t[j+1] > t[j])
                 {
                   int temp = t[j+1];
                   t[j+1] = t[j];
                   t[j] = temp;
                  }
            }
     }
     for(int i = 0; i < t.Length; i++)
        {
          Console.WriteLine("{0}={1}", i, t[i]);
        }
  1. 因为只涉及到相邻的两个数的操作,所以它的空间复杂度是o(1)是原地算法排序。

  2. 因为只有当相邻的两个元素不一致的时候我们才会去交换两个元素的位置,所以这个排序也是一个稳定排序算法。

  3. 在最好的情况下就是说他们已经是有序的情况下他们的时间复杂度是o(n),在最坏的情况下就是说它们是反倒序的情况下,它的时间复杂度是o(nn);还有一种叫平均时间复杂度n(n-1)/4,一般来说我们就看做o(n*n)。

【算法】如何分析一个排序算法

排序算法的执行效率

  1. 最好情况,最坏情况,平均情况时间复杂度
    第一,有些算法会区分,为了好对比,我们最好都要做一下区分。
    第二,对于要排序的数据,有的接近有序,有的是完全无序的。有序度的不同,对于排序的执行时间肯定有影响的。

  2. 时间复杂度的系数,常数,低阶
    时间复杂度一般都是数据规模n很大的时候的一个增长趋势,所以在这个时候它就会忽略系数,常数,低阶。但是我们一般排序的数据规模10,100,1000,这样很小的规模的数,所以我们要考虑系数,常数,低阶。

  3. 比较次数和交换(或移动)次数
    基本比较的排序算法,会设计两种操作。一种是比较两个数的大小,一种是交换两个数的位置。所以我们在分析排序算法的时候这两个都要考虑。

排序算法的内存消耗

算法的内存消耗可以通过空间复杂度来衡量,但是针对排序算法的时候,我们新加了一个概念,原地排序。原地排序就是特指空间复杂度为o(1)的排序算法

排序稳定性

针对排序算法还有一个很重要的指标,就是稳定性。这个概念就是说待排序的元素中有值相等的元素,在排序之后相等元素的先后顺序不会发生改变
发生改变的排序算法叫不稳定排序算法
不发生改变的排序算法叫稳定排序算法
为什么排序的稳定性比较重要呢?一般来说我们实际使用的时候,很有可能对一组数据做两次排序。如果排序算法不稳定的话,那我们第二次排序就会覆盖掉第一次排序所产生我们需要的结果。

[lua]关于lua中函数点和冒号的区别

前言

在lua中创建一个可以被外部访问的函数有两种方式,一种是table.function一种是table:function这两种函数的调用也是可以使用点和冒号两种方式调用的。

第一种使用table.function创建函数

local f = {}
function f.test(self,x,y) 
    print(self)
    print(x)
    print(y)
end
return f

使用点去访问

local f = require(f)
f.test(f,3,4)

打印出来的值是,self为f这个对象,x为3,y为4

使用冒号去访问

local f = require(f)
f:test(3,4)

打印出来的self还是这个对象,x为3,y为4

结论

当我们在创建一个方法的时候,我们时候点创建然后用冒号访问的时候,会默认把当前所被使用的对象给传进来。

第二种使用table:function创建函数

local f = {}
function f:test(x,y)
    print(self)
    print(x)
    print(y)
end
return f

使用点去访问

local f = require("test")
t.test(3,4)

打印出来的值为:3,4,nil;

local f = require("test")
t.test(f,x,y)

这个时候打印出来值为f,3,4

使用冒号去访问

local f = require("test")
t:test(3,4)

打印出来的是:f,3,4

结论

在这个测试中可以表明,使用冒号创建一个函数的时候,在使用点去调用的时候,会默认在第一个参数前加一个self的参数。

[算法]快排

原理

中心思想是用了二分法,首先取一个标致值,用这个标致值来分割两部分,一部分比它大,一部分比它小,然后就这样递归下去,直到全部的值都已经变成有序的。

上图


用图的方式更方便理解这个概念。

代码详情

第一种

 void Quicksort(int[]numbers,int left,int right)
            {
                if (left < right)
                {
                    int middle = numbers[(left + right) / 2];
                    int i = left -1 ;
                    int j = right + 1;
                    while (true)
                    {
                        while (numbers[++i] < middle) ;
                        while (numbers[--j] > middle) ;
                        if (i >= j)
                            break;
                        swap(numbers, i, j);
                    }
                    Quicksort(numbers, left, i - 1);
                    Quicksort(numbers, j+1, right);
                }
            }
void swap (int[]numbers,int i,int j)
            {
                int number = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = number;
            }

代码解释

首先我们找到一个标识值middle这个值是我们要分隔数组的中间值,然后我们定义两个下标,分别代表,比middle大的值,和比middle小的值,分别用i,j来表示,然后我们在while循环中,首先找到比middlle大的i下标的值,和比middle小的j下标的值,来调换位置,当找到i已经比j大等的时候,说明我们这次已经找完了,已经查到了middle这个值的下标,然后我们就开始分左右两个部分继续来大小划分。这时候的开始左边的区间就左边是这次的左区间加上这次找到的标识左边下标,右边区间就是标识右边的值加上当前区间的右边下标。

第二种

void Quicksort1(int [] numbers,int left,int right)
            {
                int i, last;
                if (left >= right)
                    return;
                swap(numbers, left, (left + right) / 2);
                last = left;
                for(i = left + 1; i < right; i++)
                {
                    if (numbers[i] < numbers[left])
                    {
                        swap(numbers, ++last, i);
                    }
                }
                swap(numbers, left, last);
                Quicksort1(numbers, left, last);
                Quicksort1(numbers, last+1, right);
            }
 void swap (int[]numbers,int i,int j)
            {
                int number = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = number;
            }

代码解释

首先我们把最左边的值和中间的值交换,然后我们记录下来交换的值的位置设置为last,因为我们已经把中间值和最左边的值交换了,所以我们标志值在最左边,我们就用这个值来和当前数组中的所有值来进行比较,如果比较的值比标识值小的话,就累加last然后交换,就把这个值换到了左边去了,如果大的话就不动,所以换过的左边都是比标识小的,最后当前的标识符换到最后一个最小的值的位置,那个位置之后就没有比标识值更小的了。然后再递归分割。

[c#基础]浅析c#Dictionary实现原理

引用原文 https://www.cnblogs.com/InCerry/p/10325290.html

前言

对于c#中的Dictionary类相信大家都不陌生,这是一个Collection(集合)类型,可以通过Key/Value(键值对)的形式来存放数据;改类最大的优点就是它查询元素的时间复杂度接近o(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。
那个是什么样的设计能让Dictionary类实现o(1)的时间复杂度呢?这就是这篇文章想和大家讨论的东西;这些都是个人的一些理解和观点。

理论知识

对于Dictionary的实现原理,有两个关键的算法,一个是hash算法,一个是用于对hash碰撞冲突解决算法。

1,hash算法

Hash算法是一种数字摘要算法,它能将不定长度的二进制数据集给映射到一个较短的二进制长度数据集,常见的MD5算法就是一种Hash算法,通过MD5算法可对任何数据生成数字摘要。而实现了Hash算法的函数我们叫它Hash函数。Hash函数又以下几点特征:

  1. 相同的数据进行Hash运算,得到的结果一定相同。
    HashFunc(key1) == HashFunc(key1);
  1. 不同的数据进行Hash运算,其结果可能会相同(Hash会产生碰撞).key1 != key2 => HashFunc(key1) == HashFunc(key2)

  2. Hash运算时不可逆,不能由key获取原始的数据.key => HashFunc(key)但是HashFunc(key)=\=> key

散列函数还需要满足以下性质:

  1. 从数据到整数值(0~N-1)的映射

  2. 足够分散

  3. 不易冲突

“足够分散”是指,即使数据只有很小的差异,散列函数的计算也需要很大的差异。
“不易冲突”是指,不易发生不同的数据得到相同的散列值的情况。()
由于散列值的计算和指定索引访问数组元素所需要的时间都和数据的个数无关,所以散列表的访问计算量为o(1)。
下图就是Hash函数的一个简单的说明,任意长度的数据通过HashFunc映射到一个较短的数据集中。

关于Hash碰撞,下图很清晰的就解释了,可从图中得知Sandra DeeJohn Smith通过Hashfunc运算后都落在了02的位置,产生了碰撞和冲突。

常见的构造Hash函数的算法有以下几种。

  1. 直接寻址法:取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key)= a*key+b,当中a和b是常数(这样的散列函数叫做自身函数)

  2. 数字分析法:分析一组数据,比方一组员工的出生年月日,这时候我们发现出生年月日的前几位数字大体相同,这样话,出现冲突的几率就会非常大,可是我们发现年月日的后几位和详细日期的数字区分非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

  3. 平方取中法:去keyword平方后的中间几位作为散列地址。

  4. 折叠法:将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加值和(去除进位)作为散列地址。

  5. 随机数法:选择一随机函数,取keyword的随机值作为散列地址,通常使用于keyword长度不同的场合。

  6. 除留余数法:取keyword被某个不大于散列列表表长m的数p除后所得的余数为散列地址。即H(key) = key MOD p,p<=m,不仅能够对keyword直接取模,也可在折叠,平方取中等运算之后取模。对p的选择非常重要,一般取素数或m,若p选的不好,容易产生碰撞。

2,解决冲突算法

对于一个hash算法,不可避免会产生冲突,那么产生冲突以后如果处理,这是一个很关键的地方,目前常见的冲突解决算法有链地址法(chain-ing),开放地址法(open addressing),再Hash法,公共溢出分区法,本文主要介绍链地址法(也就是c#的Dictionary所采用的),和开放地址法。

1. 链地址法

是将拥有相同散列值的元素存放在链表中,因此随着元素个数的增加,散列冲突和查询链表的时间也跟着增加,也就造成了性能的损失。优点:元素的删除可以用比较简单且搞性能的方式来实现。

2. 开放地址法

是在遇到冲突的时候,寻找一个新的数据存放空间(一般称为槽)。寻找空闲槽最简单的方法,就是循序遍历,直到找到空闲槽为止。
开放地址法中,在访问数据时候,如果散列值所代表的位置(槽)中不存在所希望的数据,则要么代表数据不存在,要么代表由于散列冲突而被转存到别的槽中了。于是通过以下算法来寻找目标槽位:

  1. 计算数据(key)的散列值。
  2. 从散列值找到相应的槽位(如果散列值比槽的数量大则取余数)
  3. 如果槽位与数据一致,则使用该槽->查找结束。
  4. 如果槽位为空闲,则散列表中不存在改数据->查找结束
  5. 计算下一个槽的位置。
  6. 返回第三步进行循环。

由于开放地址在数据存放上使用的是相对简单的数组方式,和链表相比较所需要的内存空间更小,因此在性能上存在有利的一面。
但是它的缺点也很明显,相比于原本的散列冲突发生的概率来说,它会让散列冲突发生的更加频繁。因此在这个方法里,会把有冲突的数据存放在下一个槽里,这就意味着下一个槽无法用来存放原本和散列值直接对应的数据。
当存放数据的数组逐渐被填满时,冲突的发生会十分频繁。当发生冲突以后,就必须通过计算来求得下一个槽的位置,用于槽查找的处理时间就会逐渐增加。因此,在开放地址法的设计中,所使用的数组大小必须是留有一定余量。
还有删除也是比较麻烦的,一般的元素和因冲突而不在原位的元素是混在一起的。因此无法简单地删除某个数据,要删除数据,仅仅将删除对象的数据所在槽置为空闲是不够的。
这样可能会把开放地址法中的连锁切断,从而导致本来存在的数据无法被找到。因此要删除数据,必须要将存放改元素的槽设置为一种特殊的状态,即“空闲(允许存放新数据)但不中断对下一个槽的计算”。

结论

随着散列表中存放的数据越来越多,发生冲突的危险性也随之增加。假设真有一种理想的散列函数,对于任何数据都能求出完全不同的散列值,那么当元素个数超过散列列表中槽的个数时,就不可避免地会产生冲突。尤其是开放地址法中当槽快要被填满时,所引发的冲突问题更加显著。
无论采用哪种方法,一旦发生冲突,就必须沿着某种连锁来寻找数据,因此无法实现o(1)的查找效率。
因此,在实用的散列表实现中,当冲突对查找效率产生的不利影响超过某一程度,就会对表的大小进行修改,从而努力在平均水平上保持o(1)的超早效率。
即便在最好的情况下,重组操作所需要的计算量也至少和元素的个数相关(即o(n)),不过,只要将重组的频率尽量控制在一个很小的值,就可以将散列表的平均访问消耗水平维持在o(1)。

三,Dictionary实现

Dictionary实现我们主要对着源码来解析,目前对照的版本为.NET Farmework 4.8,源码地址链接Dictionary
这一章主要介绍Dictionary中几个比较关键的类和对象,然后跟着代码来走一遍插入,删除和扩容的流程,相信大家就能理解它的设计原理。

1.Entry结构体

首先我们引入Entry这样一个结构体,它的定义如下代码所示。这是Dictionary类存放数据的最小单位,调用Add(Key,Value)方法添加的元素都会被封装在这样的一个结构体里。

private struct Entry{
    public int hashCode;        //哈希码的低31位,如果没有被使用则为-1
    public int next;            //下一个Entry元素的下标索引,如果没有下一个则为-1
    public TKey key;            //存放元素的key
    public TValue value;        //存放元素的value
}

2.其它关键私有变量

除了Entry结构体以外,还有几个关键的私有变量,其定义和解释如下代码所示:

private int[] buckets;                          //Hash桶
private Entry[] entries;                        //Entry数组,存放元素
private int count;                              //当前entries的index位置,当freeCount为0时启用
private int version;                            //当前版本,防止迭代过程中集合被更改
private int freeList;                           //被删除Entry在entries中的下标index,这个位置是空闲的
private int freeCount;                          //有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer;     //比较器,并且为object生成Hashcode
private KeyCollection keys;                     // 存放key的集合
private ValueCollection values;                 //存放value的集合

上面代码中,需要注意的是buckets,entries这两个数组,这是实现Dictionary的关键。

3.Dictionary-Add操作

正常的Add操作

经过上面的分析,相信大家还是不是特别明白为啥需要这样的设计。现在让我们先走一遍Dictionary的Add流程,来体会一下。
首先我们用图的形容来描述一个Dictionary的数据结构,其中只画出了关键的地方。桶大小为4以及Entry大小也是4的一个数据结构。

然后我们假设需要一个Add操作,dictionary.Add("a","b"),其中key = "a",value = "b".
大致的流程如下:

  1. 根据key的值,计算出它的hashCode。我们假设“a”的hash值为6(comparer.GetHashCode(key)).计算出来的hashCode和0x7FFFFFFF按位与(&)取底31位值,计算出的值为当前key的hashcode,假设为6。

  2. 通过hashcode和buckets的长度去余,计算出改hashCode落在哪一个buckets桶中。现在桶的长度(buckets.Length)为4,那么假设取到的值为2(6%4)所以index的值就是2,也就是落在buckets[2]这个桶中。

  3. 避开一种情况不谈,接下来它会将hashCode,key,value等信息存入entries[count]中,因为count位置是空闲的,所以值是可以放进去的;继续count++指向下一个空闲位置。上图中第一个位置,index = 0就是空闲,所以就存放在entries[0]的位置。

  4. Entry的下标entryIndex赋值给buckets中对应下标bucket。步骤3中是存放在entries[0]的位置,所以buckets[2]=0

  5. 最后version++,集合发生了变化,所以版本需要+1.只有增加,替换和删除元素才会更新版本

实际步骤和上文的步骤会有些出入,现在这样区分只是为了能方便大家理解,后续将更详细的补充
完成上面Add操作后,数据结构更新成了下图这样的形式。

这样是理想情况下的操作,一个bucket中只有一个hashCode没有碰撞产生,但是实际上是会经常发生碰撞的;下面说下Dictionary类中是如果解决碰撞的。

碰撞的Add操作

我们继续执行一个Add操作,dictionary.Add("c","d"),假设根据上面的步骤1所得到的值还是6,那么求余数还是2,最后桶的index也还是2,按照以前的步骤执行完以后数据结构如下图所示

如果继续执行步骤4那么buckets[2] = 1,然后原来的buckets[2]=>entries[0]的关系就会丢失,这是我们不能接受的,所以在这个时候Entry中的next就开始发挥作用。
如果对应的buckets[index]有其它的元素已经存在,那么会执行以下两条语句,让新的entry.next指向之前的元素,让buckets[index]指向现在新的元素,就构成了一个单链表。

entries[index].next = buckets[targetBucket];
...
buckets[targetBucket] = index;

实际上步骤4也就是做一个这样的操作,并不会去判断是不是有其它元素,因为buckets中捅的初始值就是-1,所以直接赋值不会出现问题。
经过上面的步骤以后,数据结构就更新成了下面这个样子。

4.Dictionary-Find操作

为了方便演示如果查找,我们继续Add一个元素dictionary.Add("e","f"),comparer.GetHashCode("e")=7;7% buckets.Length=3,数据结构如下所示。

假设我们现在执行这样一条语句dictionary.GetValueOrDefault("a"),就会执行以下步骤

  1. 获取key的hashCode,计算出所在的桶的位置。我们之前提到过,“a“的hashcode = 6,所以计算出来targetBucket=2。

  2. 通过buckets[2]=1找到entries[1],比较key的值是否相等,相等就返回entryindex,不相等就继续entries[next]查找,直到找到key相等元素或者next==-1的时候,这里我们找到了key=="a"的元素,返回entryindex = 0.

  3. 如果entryindex >= 0 那么返回对应的entries[entryindex]元素,否则返回default(TValue)。这里我们直接返回entries[0].value.
    整个查找过程如下图

    代码如下

    //寻找Entry元素的位置
    private int FindEntry(TKey key){
    if(key == null){
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if(buckets != null){
        int hashCode = comparer.GetHashCode(key)  & 0x7FFFFFFF; // 获取HashCode,忽略符号位
        // int i = buckets[hashCode % buckets.Length] 找到对应桶,然后获取entry在entries中位置
        // i >= 0; i = entries[i].next 遍历单链表
        for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
            // 找到就返回了
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
        }
    }
    return -1;
    }
    ...
    internal TValue GetValueOrDefault(TKey key){
    int i = FindEntry(key);
    // 大于等于0代表找到了元素位置,直接返回value
    // 否则返回该类型的默认值
    if (i >= 0) {
        return entries[i].value;
    }
    return default(TValue);
    }

    5.Dictionary-Remove操作

    前面介绍了增加,查找,接下来向大家介绍Dictionary是如何执行删除操作。我们沿用之前的Dictionary数据结构。

    删除前面的步骤和查找类似,也是需要找到元素的位置,然后再进行删除的操作。
    我们现在执行这样的一条语句dictionary.Remove("a"),hashFunc运算结果和上文中一致,步骤大部分与查找类似,我们直接看摘录的代码:

    public bool Remove(TKey key){
    if(key == null){
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if(buckets != null){
         // 1. 通过key获取hashCode
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        // 2. 取余获取bucket位置
        int bucket = hashCode % buckets.Length;
        // last用于确定是否当前bucket的单链表中最后一个元素
        int last = -1;
          // 3. 遍历bucket对应的单链表
        for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
                // 4. 找到元素后,如果last< 0,代表当前是bucket中最后一个元素,那么直接让bucket内下标赋值为 entries[i].next即可
                if (last < 0) {
                    buckets[bucket] = entries[i].next;
                }
                else {
                    // 4.1 last不小于0,代表当前元素处于bucket单链表中间位置,需要将该元素的头结点和尾节点相连起来,防止链表中断
                    entries[last].next = entries[i].next;
                }
                // 5. 将Entry结构体内数据初始化
                entries[i].hashCode = -1;
                // 5.1 建立freeList单链表
                entries[i].next = freeList;
                entries[i].key = default(TKey);
                entries[i].value = default(TValue);
                // *6. 关键的代码,freeList等于当前的entry位置,下一次Add元素会优先Add到该位置
                freeList = i;
                freeCount++;
                // 7. 版本号+1
                version++;
                return true;
            }
        }
    }
    return false;
    }

    执行完上面代码后,数据结构就更新成了下图所示,需要注意的是version,freelist,freecount的值都被更新了。

    6.Dictionary-Resize操作(扩容)

    有细心的小伙伴可能看过了Add的操作以后就会想了,buckets,entries不就是两个数组嘛,万一数组被放满了那怎么办?接下来就是我要说的Resize(扩容)这种操作了,对我们的buckets,entries进行扩容。

    6.1扩容操作的触发条件

    首先我们需要知道在什么情况下,会发生扩容操作:第一种情况自然就是数组已经满了,没有办法继续存放新的元素。如下所示

    从上文中大家也知道了,Hash运算会不可避免的产生冲突,Dictionary中使用链地址法来解决冲突的问题,但是如果一直都在冲突那就会如下所示:

    所有的元素都刚好落在了buckets[3]上面,结果就是导致了时间复杂度0(n),查找性能就会下降;所以第二种,Dictionary中发生碰撞的次数太多,导致严重的性能问题,也会触发扩容操作。
    **目前.Net Framwork 4.8中设置的碰撞次数阈值为100.

    public const int HashCollisionThreshold = 100;

    6.2扩容操作如何进行

    为了给大家演示清楚,模拟了以下的这种数据结构,大小为2的Dictionary,假设碰撞的阈值为2;现在触发Hash碰撞扩容

    开始扩容操作

  4. 申请两倍于现在大小的buckets,entries

  5. 将现有的元素拷贝到新的entries

完成上面的两步操作后,新的数据结构如下图所示。

  1. 如果是Hash碰撞扩容,使用新的HashCode函数来重新计算hash值

上文提到了,这是发生了Hash碰撞扩容,所以需要使用新的Hash函数计算Hash值,新的Hash函数并不一定能解决碰撞的问题,可能还会更糟糕,像下图中一样还是会落在同一个bucket
上。

  1. 对entries每个元素bucket=newEntries[i].hashCode%newSize确定新的buckets位置

  2. 重建hash链,newEntries[i].next=buckets[bucket];buckets[bucket]=i;

因为buckets也扩容了两倍大小,所以需要重新确定hashCode在哪个bucket中;最后重新建立hash单链表。

这就完成了扩容的操作,如果是达到Hash碰撞阈值触发的扩容可能扩容后结果会更差。
在JDK中,HashMap如果碰撞的次数太多了,那么会将单链表转换为红黑树提升查找性能,目前在.Net Framwork中还没有这样的优化,.Net Core中已经有了类似的优化。
每次扩容操作都需要遍历所有元素,会影响性能。所以创建Dictionary实例时最好设置一个预估的初始大小。

private void Resize(int newSize,bool forceNewHashCodes){
    Contract.Assert(newSize >= entries.Length);
    //1.申请新的Buckets和entries
    int[] newBuckets = new int[newSize];
    for(int i = 0;i<newBuckets.Length;i++) newBuckets[i] = -1;
    Entry[] newEntries = new Entry[newSize];
    //2.将entries内元素拷贝到新的entries内
    Array.Copy(entries,0,newEntries,0,count);
    //3.如果是Hash碰撞扩容,使用新的HashCode函数重新计算Hash值
    if(forceNewHashCodes){
        for(int i = 0; i<count; i++){
            if(newEntries[i].hashCode != -1){
                newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
            }
        }
    }
    //4.确定新的bucket位置
    //5.重建Hash单链表
    for(int i = 0;i < count; i++){
        if(newEntries[i].hashCode >= 0){
            int bucket = newEntries[i].hashCode % newSize;
            newEntries[i].next = newBuckets[bucket];
            newBuckets[bucket] = i;
        }
    }
    buckets = newBuckets;
    entries = newEntries;
}

Dictionary-再谈Add操作

在我们之前的Add操作步骤中,提到了这样的一句话,这里提到会有一种其它的情况,那就是有元素被删除的情况。

  1. 避开一种其它情况不谈,接下来它会将hashCode,key,value等信息存入entries[count]中,因为count位置是空闲的;继续count++指向下一个空闲位置。上图中第一个位置,index=0就是空闲位置,所以就存在entries[0]的位置。

因为count是通过自增的方式来指向entries[]下一个空闲的entry,如果有元素被删除了,那么在count之前的位置就会出现一个空闲的entry位置,代码如下:

private void Insert(TKey key,TValue value,bool add){
    if(key == null){
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if(buckets == null)Initialize(0);
    //通过key获得hashCode
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    //计算出目标bucket下标
    int targetBucket = hashCode%buckets.Length;
#if FEATURE_RANDOMIZED_STRING_HASHING
    //碰撞次数
    int collisionCount = 0;
#endif
    for(int i = buckets[targetBucket];i>= 0;i = entries[i].next){
        if(entries[i].hashCode == hashCode&&comparer.Equals(entries[i].key,key)){
            //如果增加操作,遍历到相同的元素,那么抛出异常
            if(add){
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            //如果不是增加操作,那可能是索引赋值操作
            //赋值成功版本加一后退出
            entries[i].value = value;
            version++;
            return;
        }
#if FEATURE_RANDOMIZED_STRING_HASHING
        //遍历一次元素,都是一次碰撞
        collisionCount++;
#endif  
    }
    int index;
    //如果有被删除的元素,那么将元素放在被删除元素的空闲位置
    if(freeCount > 0){
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }else{
        //如果当前的entries已满,那么出发扩容
        if(count == entries.Length){
            Resize();
            targetBucket = hashCode%buckets.Length;
        }
        index = count;
        count++;
    }
    //给entry赋值
    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;
    //版本号加一
    version++;
    #if FEATURE_RANDOMIZED_STRING_HASHING
#if FEATURE_CORECLR
            // In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
            // in this case will be EqualityComparer<string>.Default.
            // Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
            // be using randomized string hashing
            //如果碰撞次数大于设置的最大碰撞次数,那么出发hash碰撞扩容
            if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default)
            {
                comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
                Resize(entries.Length, true);
            }
#else
            if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
            {
                comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
                Resize(entries.Length, true);
            }
#endif // FEATURE_CORECLR

#endif
}

现在在看完整的Add代码,是不是很简单了。

8.Collection版本控制

在上文中提到过一个version这个变量,在每一次增加,修改,删除的时候,都会对这个变量做一次递增;那么这个变量存在的意义是什么呢?
首先来看一段代码的运行,先实例化一个Dictionary的实例,然后通过foreach来遍历该实例,在foreach的代码块中使用remove(key)来删除元素

结果就是抛出来System.InvalidOperationException:"Collection was modified...这样的异常,迭代过程中不允许集合出现变化。这个就是用version来实现版本控制。
我们开看下源码是怎么实现版本控制的

[Serializable]
public struct Enumerator: IEnumerator<KeyValuePair<TKey,TValue>>,IDictionaryEnumerator{
    private Dictionary<TKey,TValue> dictionary;
    private int version;
    private int index;
    internal Enumerator(Dictionary<TKey,TValue> dictionary, int getEnumeratorRetType) {
        this.dictionary = dictionary;
        //这个给版本号赋值
        version = dictionary.version;
        index = 0;
        this.getEnumeratorRetType = getEnumeratorRetType;
        current = new KeyValuePair<TKey, TValue>();
    }
    public bool MoveNext() {
        //这里看版本是否有修改
        if (version != dictionary.version) {
            ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
        }
    }
}

所以在迭代开始的时候记录一下version,后续在每一次迭代过程中都会去检查。