[c#]值类型内存分布

前言

最近公司对于gc考虑的很严格,希望堆上内存只在系统初始化的时候才分配,以减少因为gc导致的cpu卡顿。在这个基础上仔细看了一下值类型的内存分布,了解一下值类型在内存上究竟怎么分配的。

值类型

在定义值类型的变量的时候会直接算好占用多少内存。

public struct DataUnion
{
    public long _i64;
    public ulong _u64;
    public double _f64;
}

在定义的时候就已经开辟了24字节的内存保存,就算不存值内存也已经开辟了。
但是他有一个好处就是不会增加gc计算。

public struct DataUnion{
    public data _data
}

在struct中保存的是引用类型对象的引用,是固定大小的,根据操作系统的位数,32位的占4字节,64位的占8字节。每次传递的时候值类型里的数据都会被复制一份,所以引用也会被拷贝一份。

自定义内存布局方式

通过使用[StructLayout(LayoutKind.Explicit)]和[FieldOffset]特性,这个结构体可以在相同的内存位置上保存多种不同类型的数据,通过这个特性,我们能使用同一个结构保存不同类型的值。

[StructLayout(LayoutKind.Explicit)]
public struct SlateDataBase
{
    [FieldOffset(0)]
    public long _i64;
    [FieldOffset(0)]
    public bool _bool;
    [FieldOffset(0)]
    public ulong _u64;
    [fieldOffset(8)]
    public string _str;
    [FieldOffset(0)]
    public IntPtr  _next;
}

SlateDataBase _dataBase = new SlateDataBase(){_bool = false};
private SlateDataBase _dataBase1 = new SlateDataBase();

void main(){
    IntPtr ptr1 = Marshal.AllocHGlobal(Marshal.SizeOf<SlateDataBase>());
    Marshal.StructureToPtr(_dataBase,ptr1,false);
    _dataBase1._next = ptr1;
    SlateDataBase data = Marshal.PtrToStructure<SlateDataBase>(_dataBase1._next);
    data._bool;
}

上段代码使用了IntPtr来保存对象指针,这样避免了结构布局中的循环依赖问题。同时通过了StructLayout来控制内存布局。但是我知道有一个问题:

  1. 如果我分配的内存不是从0开始的,那保存的值会有问题

注意点

  1. 在使用StructLayout做内存布局的时候是在创建的时候就分配内存了,值类型可相互覆盖,所以都可以从统一位置做偏移,但是引用类型保存的是指针大小,它的大小和对齐方式跟值类型不一样,所以它不能覆盖值类型的内存,得重新取一块内存保存。
  2. 需要自己手动做InPtr所占内存的回收,而且使用的是指针很容易出内存问题。
  3. 内存大小是通过最大偏移加类型所占内存。上面所占内存为8字节+string所占(8字节)=16字节

值类型分配地方

一般来说,值类型所分配的地方有两个地方,一个是堆上,一个是栈上。那什么时候分配在堆上,什么时候分配在栈上呢?


public struct SData{
    public int _int;
}

public class Data{
    public int _int;
    public ulong _ulong;
}

public class Data1{
    public Data _data;
    public SData _sdata;
}

public Data1 _data1 = new Data1();

public void Main(){
    SData sdata = new SData();
    sdata._int = 10;
    _data1._sdata = sdata;
    sdata._int = 20;
    Data data = new Data();
    data._int = 20;
    _data1._data = data;
}

上面的示例代码值类型的struct分别在堆和栈上都分配了。
栈上分配:
在函数内创建的sdata是分配在栈上,它的什么周期是跟着函数体的结束而一起结束,内存也就被回收掉了。
堆上分配:
当我实例化了_data1这个引用类型对象的时候,它就在堆上分配了内存,分别是_data的指针大小+_sdata这个struct所占用的内存大小。所以这个时候_sdata是分配在堆上,当我把函数内的
sdata赋值给_sdata的时候,是把sdata的内容给复制过去。所以当我赋值完了之后把sdata的int改成20,实际上_sdata中的_int还是10。

引用类型

在定义引用类型的时候在没有实例化引用类型对象的时候是不会申请内存的。

public class Data{
    public long _i64; 
}

void main(){
    public Data data;
}

在使用_i64的时候需要创建一个Data的引用类型对象,相当于对_i64做了一次装箱操作。
当内部保存的类型是引用类型的时候,因为不会出现拷贝引用类型数据,所以引用不会被拷贝。

实例化

当我实例化的时候,就会在堆上分配引用类型中值类型所占用的内存,在堆上分配一个8字节的空间,然后把指针地址返回给data。data就通过这个地址找到这片空间找到里面的_i64所写入的内容。

gc回收

之前说过,不讲了。

泛型类型

根据上面的特性,在构造值类型的泛型时,使用struct来保存

public struct ValueVariable<T> where T : struct{
    private T _value;
}

public ValueVAriable<int> _int_value = 10;
public ValueVAriable<bool> _bool_value = false;

这样我在保存一个值类型的时候也是通过值类型保存,所以不会产生gc问题。
引用类型就可以直接使用object来保存。

不安全代码

暂无

【编程基础】值类型和引用类型

前言

值类型和引用类型,对于使用了托管内存的编程语言来说,他们的最大差距就是在分配和回收上的差距。

  • 值类型
    它是在存储在栈上的,当你在创建它的时候,它被压进栈内,在它出栈前,它都将可用,把它传进别的函数内使用的时候,这个时候复制了一份它的内容,然后把这个新拷贝内容给压进栈内。所以我们在函数内改变的值,只是改变了这个新压入栈的值,当然了我们也可以把这个值的引用传入进来。这里说将的值类型其实都是在函数中的局部变量。

  • 引用类型
    它是存在托管堆上的,我在这里所说的类型区分都是基于托管内存的编程语言来说的。当我们创建一个对象的时候,先告诉托管堆给我们分配一块内存,然后它把这个内存的地址引用返回给我们,然后我们就在这块内存内做一些操作,存一些数据,一些别的托管内存的一些指针。

引用类型中包含的值类型

如果引用类型中有值类型的变量实例,那它会在分配给它的内存中开辟值类型所需要的内存大小,给值类型用。那这个时候只值类型就被放在托管堆上了。

正文

看完上面的一些东西,是不是感觉很迷茫呢?有些迷糊值类型和引用类型究竟有哪些区别,究竟是放在堆上还是在栈上,它们之间啥关系呢?下面我就来讲讲。

一位仙气少年

在编程世界里,我们程序启动有一个虚拟机在跑。当我们把世界转向一个玄幻的世界里,那这个虚拟机我们让他叫做小m,是一个刚刚从师门出发,将要去入世,去闯荡一番。那现在我们来给他一些入世需要的装备吧。

绝世武功?

在给小m发放装备的时候 ,我们先去了解小m有哪些功法,他可以克隆一切物体,还会一种世界上绝无仅有的功法,那就是空间术。当他在学习这门功法的时候,他也继承了前人开辟的一大块空间(这个就是托管堆的空间大小),他可以自己去这块大的空间里切割一部分归自己所用。在了解了这些之后,我们来给小m发放装备了,俗话说得好的好,功夫再好也怕手枪,所以我们先给他一把枪,这把枪有个特殊的属性,就是能发射任何东西,但是,大家也知道,弹夹有个特性,就是最后压进来的子弹是最先发射出去的(这就是栈)。其实有了这把枪我们就可以不需要再去给他发放什么装备了。

空间戒子

小m在收拾东西的时候,发现自己想带的东西太多了。这个时候他想起来了自己的空间术。他在仔细研究之后,发现在自己切割下来的空间内,能放和自己相关的任何东西,那他就开始创建了一个空间指环,但是有个不同的情况就是,空间术创建的时候,它的空间大小是不能更改的,当你创建的时候是多大,那就是多大,当你创建的空间指环不够你使用的时候,你就需要重新创建一个更大的空间,然后把原来空间的东西给搬到新空间里,在这个操作是,需要很耗费小m的功力,需要花费他很多时间。现在小m已经创建了一个空间戒子了,他现在把这个戒子带到身上去了(获取引用)。

Hello Word

现在小m带齐了自己的所有装备开始下山了,他下山后看着这个未知的世界,他不知道怎么的就像对着这个世界说上一句“Hello Word”。那现在它就开始使用它的枪打出这几个字了,它开始一个字符一个字符的压入枪内d,r,o,w o,l,l,e,h。然后一口气打出了九发,但是它觉得太不爽了,连续扣动扳机太累的。转念一想,自己有个空间术吗?这把枪不是可以射出任何东西,是不是可以把我这个空间术内的这一整块空间射出去呢?它就开始使用空间术来创建了一个特殊的空间,就是为了能把字符全部装进,这个空间他为了保险,还做了一个限制,就是我只能放一次字符的集合,后面再放进去,那就只能重新在创建一个新的空间,把之前的字符集合给拷贝进去,然后再添加。好了,现在我们开始把这个字符串空间打出去了。一枪出去,天空上炸出了一整句“Hello Word”。小m现在太兴奋了,感觉自己创造了功法的一种新的使用方法。然后疯狂的往天上打枪。

子弹不够了?

突然小m发现他打出来的子弹是空弹,啥都看到。这个时候他也发现了不正常的情况,就是他不能再使用空间术了,这个时候小m慌了,他觉得他最大的一个依仗没有了,他懊恼自己的浪费。这个时候,他疯狂的翻自己的秘籍,就像知道,怎么让自己重新获得空间术。他查了一遍又一遍,终于发现自己的功法不能用是因为他没办法自己开辟空间,只能用继承下来前人的那一整块空间,但是在刚刚那一波疯狂输出的时候,这块继承的空间以及被消耗殆尽了。然后他再一次翻阅前人所遗留下来的文档,发现了,这个空间术在开辟空间的时候,只是暂用了前人的空间,前人空间有个特殊的属性,就是,当发现你没有再使用的时候,它就会把这被你借走的空间给拿回来,把里面的东西清空了。但是这个属性跑起来的这段时间内,小m就没有办法使用空间术了。小m发现了这样的一个属性的时候很高兴呀,感觉自己将再一次能掌握到空间术了,他开始研究起来,究竟什么情况下会被认识这块空间不被自己使用了。原来是自己把空间术所返回过来引用给遗弃掉,就是当自己从枪里发射出去,其实就是把引用给抛走了。现在小m就开始等待空间恢复了。

这颗子弹真奇怪

小m在探险的时候,发现了一种特殊的材质(接口),我们称这个材质为I。这个材质自带一种空间属性,小m用这个材质做了一枚子弹(struct),当这枚子弹在使用的时候被当成I类型使用的时候(装箱),它就突然变成了一个引用类型,它会把它所有的东西都包装到空间槽内,然后把自己变成一个引用类型的子弹。当把它当成一个struct类型的子弹使用的时候(拆箱),它会把它放置在空间槽内的东西都打包拿出来,变成一个真正的子弹,那部分被它用来放物体的空间槽就被遗弃了。等待回收。

后记

这篇太难产了,想换个写法,感觉自己还是写不出来,这种的写法不太适合我

【编程基础】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是一个好用但不高效的组件。

[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,后续在每一次迭代过程中都会去检查。

[编程基础].NET中的c#stack和heap(四)

引用https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iv/

本文中我们将研究垃圾收集(GC)以及保持应用程序有效运行的一些方法

标记

让我们从GC的角度来看这件事。如果我们负责“清除垃圾”,我们需要好好设计一个方案。首先,我们需要确定什么是垃圾,什么不是垃圾。
为了确定需要保留的内容,我们首先假设所有未使用的都是垃圾。想象一下,我们与两个好朋友住在一起, Joseph Ivan Thomas(JIT)和 Cindy Lorraine Richmond (CLR)。Joe和Cindy会跟踪他们在使用什么,并向我们提供他们需要保留的物品清单。我们将初始列表称为“root”列表,因为我们将其当成作用起点。我们将保留一个主列表,以图形化我们要保留的房间中所有物体的位置。使列表中的内容在正常工作时所需要的内容都添加到图表中。这也就是GC确定保留哪些内容的方式。它接受一个“Root”对象引用列表,以使其免受即时(JIT)编译器和公共语言运行时(CLR)的影响,然后递归搜索对象引用以构建应包含哪些内容的图形需要保留。
root包括:

  • 全局/静态指针。通过在静态变量中保留对对象的引用来确保它不会被垃圾回收。
  • 堆栈上的指针。我们不会想要去抛弃掉我们应用程序的线程仍然需要执行的东西。
  • CPU寄存器指针。CPU中内存地址指向的托管堆中的所有内容都应该要保留下来。

    从上图我们可以看到,从roots直接引用了我们托管堆中的object1和object5,而我们的object1引用了我们的object3,所以在递归搜索中,我们roots也引用到了object3,现在我们知道所使用到的物体后,下一步进行压缩。

    压缩

    现在我们已经标识了需要保留的物体对象,我们现在只需要移动下需要保留的对象,即可压缩空间。

    在我们的内存中,我们还不需要放置一个对象之前,我们就先清理空间,由于不需要object2,因此作为GC,我们将object3往下移动,固定在object1的附近,把object2给清理掉。

    接下来我们按照和上面一样的方式,把object5给复制下来。

    现在我们已经清理了所有的内容了,我们只需要写一个便签并将其放置在压缩堆的顶部即可让Claire知道新的对象放在哪里。

    了解GC的本质可以帮助您理解,移动对象是非常消耗的。所以我们可以减少所要移动的内容的大小,那么我们将改善下GC流程,因为复制的内容将减少。

    处理托管堆之外的东西

    作为负责垃圾收集的人,我们在打扫房间的时候遇见一个问题是如何清理掉汽车中的物体。清洁时,我们需要清洁的所有东西,如果笔记本电脑在家而电池在车里该怎么办呢?
    在某些情况下,GC需要执行代码以清理非托管资源,例如文件,数据库连接,网络连接等等。一种解决方法是通过终结器

    class Sample{
    ~Sample(){
        //
    }
    }

    在对象创建期间,所有带有终结器的对象都将添加到终结队列中,假设object1,4,5都具有终结器的话,并且位于终结队列中。让我们看看当应用程序不再引用object2和4并且做垃圾回收时候会发生什么。

    object2以通常方法处理掉,但是,当我们处理到object4的时候,GC看到它在终结队列中,然后就先不回收掉object4的所拥有的内存,而是移动object4并把它的终结器添加到名为freachable的特殊队列中。

    有一个专用线程用于执行freachable队列中的对象。一档终结器由对象4上的该线程执行了,它将从freachable队列中删除。然后只有这样,object4才能准备好被GC;

    然后,object4会在下一个GC循环中被回收掉。
    因此在我们的类中添加终结器会为GC增加工作量,这个消耗是非常昂贵的,并且会对垃圾回收的性能和程序产生不利的影响,仅在绝对确定需要终结器的时候才使用它们。
    更好的做法是确保清理非托管资源。可以想象,最好是显式关闭链接并且使用IDisposable接口进行清理,而不是尽可能的用终结器。

    IDisposable

    实现IDisposable的类再Dispose()方法(接口的唯一签名)中执行清除。因此如果我们有一个ResourceUser类而使用using来追踪finalizer,代码如下:

    public class ResourceUser{
    ~ResourceUser(){
        //
    }
    }

    我们可以用IDisposable作为实现相同功能的更好方法:

    public class ResourceUser:IDisposable{
    public void Dispose(){
        //
    }
    }

    IDisposable与using关键字集成在一起。在using块的末尾,对using()中声明的对象调用Dispose()。在using块之后不应引用该对象,因为该对象的本质上应被视为“已消失”并已准备好由GC清除。

    public static void DoSomething(){
    ResourceUser rec = new ResourceUser();
    using(rec){
        //
    }
    }

    还有一种写法是把对象放在using块中,这样从表面上看它更有意义,因为rec不在using块的范围以外可用。这种模式更符合IDisposible接口的意图

    public static void DoSomething(){
    using(ResourceUser rec = new ResourceUser()){
        //
    }
    }

    通过using()和实现IDisposible的类一起使用,我们可以执行清理操作,而不会通过强制终结对象来增加GC的额外开销。

    静态变量:当心

    class Counter{
    private static int s_Number = 0;
    public static int GetNextNumber(){
        int newNumber = s_Number;
        s_Number = newNumber + 1;
        return newNumber;
    }
    }

    如果两个线程同时调用GetNextNumber(),并且在s_Number之前都为newNumber分配了相同的值。
    则他们将返回相同的结果。word是确保一次仅一个线程可以访问代码块的一种方法。最佳的做法是:您应锁定尽可能少的代码,因为线程必须在队列中等待才能执行lock()块中的代码,并且效率低下。

    class Counter{
    private static int s_Number = 0;
    public static int GetNextNumber(){
        lock(typeof(Counter)){
            int newNumber = s_Number;
            newNumber += 1;
            s_Number = newNumber;
            return newNumber;
        }
    }
    }

    静态变量当心的第二种情况

    接下来,我们必须注意静态变量引用的对象。请记住,如何清理“roots”引用的任何内容。请看下面的例子:

    class Olympics{
    public static Collection<Runner> TryoutRunners;
    }
    class Runner{
    private string _fileName;
    private FileStream _fStream;
    public void GetStats(){
        FileInfo fInfo = new FileInfo(_fileName);
        _fStream = _fileName.OpenRead();
    }
    }

    因为Runner集合对于Olympics类是静态的,所以不仅将不释放集合中的对象让它们做垃圾回收(它们全部都是通过roots间接引用),并且您可能已经注意到,每次我们运行GetStats()时,Fileinfo都将打开文件。因为它没有关闭,也没有被GC释放,所以这段代码实际上的运行将是一场等待着发生的灾难。想象一下,我们有1000000个Runner在Olympics中。我们最终将会有许多不可收集的对象,每个对象都有一个开放的资源。

    单例模式

    保持现状不变的技巧是始终在内存中有一个使用程序类的实例。一种简单的方法就是使用GOF单例模式。应该谨慎的使用单例,因为它们确实是“全局变量”,并且在多线程应用程序中有很多让我们感到头疼和“奇怪”的行为,在这种情况下,不同的线程可能会改变对象的状态。如果我们使用单例模式(或任何全局变量),则我们应该能够证明其合理性。

    public class Earth{
    private static Earth _instance = new Earth();
    private Earth(){}
    public static Earth GetInstance(){return _instance;}
    }

    我们有一个私有的构造函数,因此只有Earth可以执行它的构造函数并制作一个Earth。我们有一个Earth的静态实例和一个获取实例的静态方法。该特定实现是线程安全的,因为CLR确保线程安全地创建静态变量。这是一种优雅的实现单例模式的方法。

    结论

    我们可以做以下的一些事情来提高GC性能:

    1. 清理。不要保留资源,确保关闭所有打开的链接,并尽快清理所有非托管对象。作为使用非托管对象的一般规则,请尽可能的延迟实例化并尽快清理。
    2. 不要过度引用。使用引用对象时候需要合理。请记住,我们的对象如果还活着,那它所引用的所有对象都不会被收集。当我们完成了类所引用的操作后,可以通过将引用设置为null来删除它。还有一个技巧是肩为使用的引用设置为自定义轻量级的NullObject,以避免获取空引用异常。启动GC时候引用到的文件越少,标记过程中的压力就越少。
    3. 使用finalizer很轻松,但是在GC时候代价却非常的大。我们只能在合理的情况下使用它们。如果我们使用IDisposible而不是finalizer,则效率是非常高的。因为可以通过一次GC就回收掉对象而不需要来两次GC。
    4. 把object和其子类放在一起。在GC上将大块内存复制到一起比较容易,而不是每次通过堆时都必须对堆进行碎片整理,因此我们声明一个由许多其他对象组成的对象时候,应该将他们尽可能的放在一起实例化。

[编程基础].NET中的c#stack和heap(三)

引用链接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iii/

前言

在使用.NET框架的时候,我们可以不必主动担心内存管理和垃圾回收(GC),但是仍然要牢记内存管理和垃圾回收的机制,以优化应用程序的性能。另外,对内存管理的工作原理有一个基本的了解将有助于我们理解所编写程序中使用变量的行为。在这个文章中,我们将讨论堆中具有引用变量以及如何使用ICloneable修复引用变量引起的问题。

副本不是副本

为了清楚定义这个问题,我们来检查一下当堆上有一个值类型而不是堆上有一个引用类型的时候,会发生什么。首先,我们来定义一个值类型,采取一下的类和结构。我们有一个Dude类,其中包含Name元素和两个Shoe结构。我们有一个CopyDude()方法,可以轻松的制作一个新的Dudes。

public struct Shoe{
public string Color;
}
public class Dude{
public string Name;
public Shoe RightShoe;
public Shoe LeftShoe;
public Dude CopyDude(){
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe;
newPerson.RightShoe = RightShoe;
return newPerson;
}
public override string ToString(){
return (Name + ": Dude!,I have a"+RightShoe.Color+" shoe on my right foot , and a" + LeftShoe.Color+"on my left foot");
}
}

我们的Dude类是一个引用类型,因此shoe结构是该类的成员元素,所以它们都最终出现在堆上。

当我们执行以下方法时候:

public static void Main(){
Dude bill = new Dude();
bill.Name = "Bill";
bill.LeftShoe = new Shoe();
bill.RightShoe = new Shoe();
bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
Dude Ted = Bill.CopyDude();
Ted.Name = "Ted";
Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
Console.WriteLine(Bill.ToString());
Console.WriteLine(Ted.ToString());
}

输出为:
Bill:Dude!I have a Blue shoe on my right foot, and a blue on my left foot.
Ted:Dude!I have a Red shoe on my right foot, and a Red on my left foot.

如果将Shoe设置为引用类型会怎样呢?

public class Shoe{
    public string Color;
}

并且在Main()中运行完全相同的代码,看看我们的输入如何变化。
Bill:Dude!I have a Red shoe on my right foot, and a Red on my left foot.
Ted:Dude!I have a Red shoe on my right foot, and a Red on my left foot.
这显然是一个错误,但是你知道为什么会出现这样的错误吗?这就是我们最终在堆中得到的结果

因为我们使用的Shoe作为引用类型而不是值类型,并且在复制引用类型的内容时仅仅复制了指针(而不是实际对象),所以我们必须做一些额外的工作来制作Shoe引用类型的行为像值类型。
刚好我们有一个可以帮我们实现这个方案的接口:ICloneable。这个接口基本是是所有Dudes都会用到的方法,并且定义了如何复制引用类型,以避免我们的“鞋子共享”错误。我们所有需要“clone”的引用类型都应该使用ICloneable接口,包括Shoe类。
ICloneable包含一种方法:Clone()

public object Clone(){
    //
}

这是我们在Shoe类中实现它的方式:

public class Shoe:ICloneable{
    public string Color;
    public object Clone(){
        Shoe newShoe = new Shoe();
        newShoe.Color = Color.Clone() as string;
        return newShoe;
    }
}

在clone()方法内部,我们仅制作了一个新的Shoe,克隆所有引用类型并复制所有值类型,然后返回新对象。string类已经实现了ICloneable,因此我们可以调用Color.Clone()方法,由于Clone()返回对象的引用,因此在设置鞋子颜色之前,我们必须“重新赋值”引用。
接下来我们的CooyDude()方法中,我们需要克隆鞋子而不是复制它们:

public Dude CopyDude(){
    Dude newPerson = new Dude();
    newPerson.Name = Name;
    newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
    newPerson.RightShoe = RightShoe.Clone() as Shoe;
    return newPerson;
}

现在,当我们运行Main时。

public static void Main(){
    Dude Bill = new Dude();
    Bill.Name = "Bill";
    Bill.LeftShoe = new Shoe();
    Bill.RightShoe = new Shoe();
    Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
    Dude Ted = Bill.CopyDude();
    Ted.Name = "Ted";
    Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
    Console.WriteLine(Bill.ToString());
    Console.WriteLine(Ted.ToString());
}

打印出来:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
这才是我们需要输出的内容。

总结

因此,作为常规方法,我们总是希望克隆引用类型并复制值类型。因此,本着减少额外影响的原则,我们来更进一步的清理Dude类以实现ICloneable。使用CopyDude()方法。

public class Dude:ICloneable{
    public string Name;
    public Shoe RightShoe;
    public Shoe LeftShoe;
    public override string ToString(){
        return(Name + ":Dude! I have a"+RightShoe.Color+" shoe on my right foot ,and a"+LeftShoe.Color+" on my left foot.");}
        public object Clone(){
            Dude newPerson = new Dude();
            newPerson.Name = Name.Clone() as string;
            newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
            newPerson.RightShoe = RightShoe.Clone() as Shoe;
            return newPerson;
        }
}

然后我们在将Main()中的方法改成Dude.Clone()

public static void Main(){
    Dude Bill = new Dude();
    Bill.Name = "Bill";
    Bill.LeftShoe = new Shoe();
    Bill.RightShoe = new Shoe();
    Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
    Dude Ted = Bill.Clone() as Dude;
    Ted.Name = "Ted";
    Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
    Console.WriteLine(Bill.ToString());
    Console.WriteLine(Ted.ToString());
}

输出如下:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.
输出的值如我们所预期的。
实际上,System.String类的赋值运算符(“=”符号)实际上就是克隆String,因此不必担心重复引用的问题,因此你必须主要到我们内存的膨胀。实际上由于字符串是引用类型,所以它实际上在图中应该指向堆中的另一个对象的指针,但是为了方便理解,我们把它显示成值类型

结论

通常我们计划复制对象,则应该实现(并使用)ICloneable这个接口,这使我们的引用类型可以在某种程度上模仿值类型的复制行为。由于值类型和引用类型分配内存的方式有所不同,因此跟踪我们要处理的变量类型非常重要。

[编程基础].NET中的c#stack和heap(二)

引用 https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-ii/

前言

使用.NET框架,我们不必主动考虑内存管理和垃圾回收(GC),但仍必须牢记内存管理和垃圾回收,以优化应用程序的性能。另外,对内存管理的工作原理有一个基本的了解将有助于解释我们在编写的每个程序中使用的变量的行为。在本文中,我将介绍将参数传递给方法时需要注意的一些行为。

参数,重点

这是代码执行过程中发生的情况的详细视图。在第一部分中,我们介绍了进行方法调用时发生的情况。现在让我们更详细的介绍一下。。。
当我们进行方法调用时,会发生以下的情况:

  1. 给堆栈上我们所执行的方法所需要的信息分配空间(称为堆栈框架)。这包括调用地址(指针),该地址基本上是GOTO指令,因此当线程完成运行我们的方法时,它知道要返回到哪里才能继续执行。
  2. 我们的方法参数被复制。这是我们需要更仔细研究的地方。
  3. 控制权传递给JIT'ted方法,线程开始执行代码,因此,我们有另外一种犯法,由“调用堆栈”上的堆栈帧表示。
public int AddFive(int pValue){
    int result;
    result = pValue + 5;
    return result;
}

使用的堆栈如下图所示

注意:这个方法不存在与堆栈中,在这里只作为参考来说明堆栈框架的开始。
和第一部分讨论的那样,根据参数的类型是引用类型还是值类型,对堆栈上的参数的处理方式将有所不同。值类型将被整体复制,引用类型将被复制引用。

传递值类型

这是传递值类型
首先,当我们传递值类型时,将分配空间,并将类型中的值复制到堆栈上的新空间。
示例代码:

class Class1
{
    public void Go(){
        int x = 5;
        AddFive(x);
        Console.WriteLine(x.ToString());
    }
    public int AddFive(int pValue){
        pValue += 5;
        return pValue;
    }
}

执行该方法时,将“x”的空间放置在堆栈上,其值为5。

接下来,将AddFive()放置在堆栈上,并为其参数留出空间,并从x逐位复制值。

当AddFive()完成执行后,线程将被传递回Go(),并且由于AddFive()已经完成,因此pValue本质上是已经被抛弃了的数据:

因此,我们的代码输出的值为“5”,其中的关键点是:传递给方法的任何值类型参数都是复本,我们依靠原始变量的值来拷贝。
要记住的一件事:如果我们传递一个非常大的值类型(列如一个非常大的结构)并将其传递给堆栈,则每次复制它的空间和处理周期都会非常消耗。堆栈没有无限的空间,就像往一个杯子里一直倒水,最后它就可能会溢出。struct是一个可以变得非常大的值类型,我们必须了解我们如何处理它。
这是一个很大的结构:

public struct MyStruct{
    long a,b,c,d,e,f,g,h,i,j,k,l,m;
}

看一下我们执行Go()并转到下面的DoSomething()方法时会发生什么:

public void Go(){
    MyStruct x = new MyStruct();
    DoSomething(x);
}
public void DoSomeThing(MyStruct pValue){
    //
}


这就能看到低效的地方了。想想一下,如果我们通过MyStruct数千次,你就可以知道它是怎么拖累你程序的效率了。
那我们改怎么改善上述问题呢?可以通过传递对原始值类型的引用,如下所示:

public void Go(){
    MyStruct x = new MyStruct();
    DoSomething(ref x);
}
public struct MyStruct{
    long a,b,c,d,e,f,g,h,i,j,k,l,m;
}
public void DoSomething(ref MyStruct pValue){
    //
}

这样,我们最终可以在内存中更加有效的分配对象。

通过引用传递值类型时,我们唯一需要注意的是,我们对值类型的值的访问。
pValue中的任何更改都将x中的值进行了修改。使用下面的示例代码

public void Go(){
    MyStruct x = new MyStruct();
    x.a = 5;
    DoSomething(ref x);
    Console.WriteLine(x.a.ToString());
}
public void DoSomething(ref MyStruct pValue){
    pValue.a = 12345;
}

上述代码的打印值为“12345”,因为pValue.a实际上使用的是原始数据x变量里的内存空间。

传递引用类型

作为引用类型的传递参数类似于在上一个示例中通过引用传递值类型。
如果我们需要使用值类型

public class MyInt{
    public int MyValue;
}

并且调用Go()方法,因为MyInt是引用类型,所以它会出现在堆上。

如果按照以下代码来执行Go()

public void Go(){
    MyInt x = new MyInt();
    x.MyValue = 2;
    DoSomething(x);
    Console.WriteLine(x.MyValue.ToString());
}
public void DoSomething(MyInt pValue){
    pValue.MyValue = 12345;
}

这个时候发生了以下的事情。

  1. 从对Go()的调用开始,变量x进入堆栈。
  2. 从对DoSomething()的调用开始,参数pValue进入堆栈。
  3. x的值(堆栈上MyInt的地址)被复制到pValue

因此,当我们使用pValue更改堆中MyInt对象的MyValue属性并稍后使用x引用堆上对象时,我们得到的值是“12345”。
所以有一个有趣的地方,当我们通过引用传递引用类型的时候会发生什么呢?
看看这个。如果我们有Thing类,Animal和Vegetable继承自它

public class Thing{

}
public class Animal:Thing{
    public int Weight;
}
public class Vegetable:Thing{
    public int Length;
}

然后再执行下面Go()方法:

public void Go(){
    Thing x = new Animal();
    Switcharoo(ref x);
    Console.WriteLine("x is Animal : "+(x is Animal).ToString());
    Console.WriteLine("x is Vegetable : "+(x is Vegetable).ToString());
}
public void Switcharoo(ref Thing pValue){
    pValue = new Vegetable();
}

我们的x就变成的Vegetable。
如果不加ref那x还是Animal
让我们看看发生了什么;

  1. 从Go()方法调用开始,x指针进入堆栈
  2. Animal对象在堆上
  3. 对Switcharoo方法调用开始,pValue进入堆栈并指向x

  1. Vegetable在堆上
  2. x的值通过pValue更改为Vegetable的地址。

如果不通过ref传递Thing,则保留对Animal引用的并从代码获得相反的结果。

[编程基础].NET中的c#stack和heap(一)

引用

https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/

前言

这段时间在巩固c#基础的时候,发现有些基础性的东西在以前是没有仔细去想过的,比如值类型,和引用类型的区分,究竟值类型保存在哪里,引用类型保存在哪里,两个究竟是啥区别。在说到这个问题的时候,我们需要巩固一下堆和堆栈的信息。这个知识点在前面讲GC的时候有带过一点,但是没有详细去了解过,所以现在开始做一次总结。

Stack 和 Heap 有什么区别

堆栈(stack)一般来负责跟踪代码中正在执行的内容(即所谓的“调用”)。而堆(Heap)一般来说负责跟踪我们的对象。
可以将堆栈想想成一系列盒子堆叠在一起。每次调用方法的时候,都是从顶部叠放一个盒子,从而跟踪应用程序中发生的事情。我们只能使用堆栈顶部盒子里面的内容。当我们完成顶部盒子里的后,我们就把这个盒子给丢掉并继续使用推上来的顶部盒子里的内容。堆是类似的,它的主要用途是保存信息,以便可以随时随地的访问堆中的任何内容。和堆栈相比堆没有任何访问限制
堆栈是自我维护的,这就意味着我们可以不用去管理它的内存情况,顶部的数据不再使用就丢弃掉。堆就必须去考虑垃圾回收的情况(GC)。

以上图片并不是内存中真实的表现形式,但是能够帮助我们区分堆栈和堆。

堆栈和堆上发生了什么

我们将在执行代码时将四种主要类型的东西放入堆栈和堆中:值类型,引用类型,指针和指令。

值类型

在c#中,使用以下类型声明列表表明的所有“事物”均为值类型(因为它们来自System.ValueType):

  • bool
  • byte
  • char
  • decimal
  • double
  • enum
  • float
  • int
  • long
  • sbyte
  • short
  • stuct
  • uint
  • ulong
  • ushort

    引用类型

    所有被声明为以下类型的事物都被称为引用类型:

  • class
  • interface
  • delegate
  • object
  • string

    指针

    放在内存管理方案“事件”的第三种类型是对类型的引用。引用通常被称为指针。在c#中尽量不要明确使用指针,他们是由公共语言运行时(CLR)管理的。指针(或引用)与引用类型的不同之处在于,当我们说某个对象是引用类型时,意味着我们能够通过指针访问它。指针是内存中的一大块空间,指向内存中的另一个空间。指针占用的空间与我们放入堆栈和堆中的其他任何东西一样。其值可以是内存地址或null。

    指针是可以在堆栈中也可以在堆中的。

    如何决定是放在哪里的

    这是我们的两个黄金法则:

    1. 引用类型总放在堆上。
    2. 值类型和指针总是放在声明它们的地方。
      正如我们前面提到的,堆栈(stack)负责跟踪代码执行过程中每个线程的位置。您可以将其视为线程“状态”,并且每个线程都有自己的堆栈。当我们的代码调用执行一个方法时,线程开始执行经过JIT编译并存放在于方法表中的指令,它还将方法的参数放在线程堆栈中。然后当我们遍历代码并在方法中遇到变量时,将它们放置在堆栈的顶部。

      public int AddFive(int pValue){
      int result;
      result = pValue + 5;
      return result;
      }

      这是堆栈顶部的情况。在查看的内容已经位于堆栈的的其他项目之上了。
      一旦执行executin ghte方法,该方法的参数将被放置在堆栈上。
      注意
      该方法不存在与堆栈中,仅作参考说明

      首先入栈的是方法体,然后入栈我们方法的参数。
      将控制(执行该方法的线程)传递给位于我们类型方法表中的AddFive()方法的指令,如果这是我们第一次点击该方法,将执行JIT编译

      该方法执行时,我们使用一些内存来存储“结果”变量,并将其分配在堆栈上。

      该方法完成执行并返回我们的结果。

      通过将指针移到AddFive()开始的可用内存地址来清理堆栈上分配的所有内存,然后我们转到堆栈上的上一个方法。

      在此实例中,我们的“结果”变量被放置在堆栈上。事实上,每次在方法主体中声明值类型时,它将被放置在堆栈中。
      现在,值类型有时也放置在堆上。在规则中:值类型总是去声明它们的地方。所以,如果在方法外部但是在引用类型内部声明了值类型,那么将其放置在堆的引用类型内。
      现在来看另一个例子:
      如果我们定义以下的MyInt类:

      public class MyInt{
      public int MyValue;
      }

      并且正在执行以下方法:

      public MyInt AddFive(int pValue){
      MyInt result = new MyInt();
      result.MyValue = pValue + 5;
      return result;
      }

      与之前一样,线程开始执行该方法,并且其参数被放置在该线程的堆栈中。

      现在开始变得有趣了。
      由于MyInt是引用类型,因此将其放置在堆上,并且由堆栈上的指针引用。

      在AddFive()完成执行之后,我们正在清理

      我们在堆中只剩下一个孤零零的MyInt(堆栈中不再有任何人指向MyInt)

      这个时候就是垃圾回收集(GC)发挥作用的地方。一单程序达到一定内存阈值并且需要更多的堆空间时候,便会启动GC。GC将停止所有正在运行的线程(FULL STOP),在堆中找到主程序未访问的所有对象并将其删除。然后,GC将压缩堆中剩余的所有对象以腾出空间,并调整指针指向堆栈和堆中的这些对象。可以想象,这在性能方面可能是非常昂贵的。因此现在您可以了解为什么在尝试编写高性能代码时,注意堆栈和堆中内容是很重要。
      那这是怎么影响我的呢?
      当使用引用类型时,我们要处理类型的指针,而不是该类型本身。当我们使用值类型时,我们使用的是值类型本身。这是不是十分明确呢?
      那我们看下实例代码
      如果我们执行以下方法:

      public int ReturnValue(){
      int x = new int();
      x = 3;
      int y = new int();
      y = x;
      y = 4;
      return x;
      }

      这上面返回的值是3,这个很简单对吧。
      但是我们如果使用的是MyInt类:

      public class MyInt{
      public int MyValue;
      }

      并且我们正在执行以下方法:

      public int ReturnValue2(){
      MyInt x = new MyInt();
      x.MyValue = 3;
      MyInt y = new MyInt();
      y = x;
      y.MyValue = 4;
      return x.MyValue;
      }

      现在我们得到的返回值是几呢:是4。
      可能有人想不通为啥这个值是4
      我们先来先第一个例子中的内存图

      后面我们看下第二个例子的内存图,因为x,y都是指向堆中的同一个对象,所以我们就没办法获得3

      希望以上内容能使您更好的了解c#中“值类型”和“引用类型”变量之间的基本区别,以及对什么是指针以及何时使用它的基本理解。

[c#]关于c#中委托,匿名函数,事件的浅要解析

委托

委托是一种引用类型,标识对具有特定参数列表和返回类型的方法的引用。在实例化委托时,可以将其实例与任何具有兼容签名和返回类型的方法相关联。通过委托实例调用方法。
委托用于将方法作为参数传递给其它方法。可将任何可访问类或结构中与委托类型匹配的任何方法分配给委托。该方法可以是静态方法,也可以是实例方法。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。
委托有以下属性:

  • 委托类似于c++的函数指针,但委托完全面向对象,不像c++指针会记住函数,委托会同时封装对象实例和方法。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起
  • 方法不必与委托类型完全匹配
  • 可使用匿名方法或lambda表达式内联代码块
  • 可以使用?.Invoke()来调用委托方法
    匿名函数和函数实例的问题
    在研究逆变和协变的时候发现一段官方代码

    public delegate R SampleGenericDelegate<A, R>(A a);
    public static Second AFristRSecond(First first)
    {
    return new Second();
    }
    SampleGenericDelegate<Second, First> fse = AFirstRSecond;

    这段代码里,返回值是定义委托的子类,参数是定义委托的父类,所以这个逆变,又是协变,但是我用匿名函数写的时候却没办法成功
    SampleGenericDelegate<Second, First> fse3 = delegate (First a)
    {
    return new Second();
    };
    没有搞明白这个原因,只能归结于是匿名函数和函数实例的不一致。

    .NET包含的委托类型

  • Func<> 通常用于现有转换的请款,也就是说需要将委托参数转换为其他结果时。必须且只有一个返回值的时候用这个,返回值有协变,如果传入参数的话是带有逆变。
  • Action<> 用于需要使用委托参数执行操作的情况。简而言之,无返回值的用这个,如果有传入参数的定义,这个是有逆变out关键词的。
  • Predicate<> 用于需要确定参数是否满足委托条件的情况,必须且只有一个bool值的返回值,必须且只有一个传入参数,传入参数带有逆变。

    委托中的变体

    
    public class First { }
    public class Second : First { }
    public delegate First SampleDelegate(Second a);
    public delegate Second SampleDelegateSecond(Second a);
    public First ASecondRFirst(Second first)
    { return new First(); }
    public Second AsecondRsecond(Second second)
    {return new Second();}
    public First AFirstRFirst(First first)
    {return new First();}
    public Second AFirstRSecond(First first)
    {return new Second();}
- 协变
定义:将返回派生程度较大的派生类型的方法分配给委托(就是把子类返回出来)
在上述代码中体现为:SampleDelegate sa = AsecondRsecond 这个就表现为协变
- 逆变
定义:方法所接受参数的派生类型所具有的派生程度小于委托类型指定的程度(就是传参传父类)
在上述代码中表现为:SampleDelegate sa = AFirstRFirst 这个就表现为逆变

SampleDelegate sa = AFirstRSecond 这个应该是既满足协变,又满足逆变。
- 泛型委托
public delegate R SampleGenericDelegate(A a);
SampleGenericDelegatedGeneric = ASecondRFirst
上述代码为正常的泛型委托
dGeneric = AFirstRSecond;
上述即是协变也是逆变的隐式转换。
- 泛型类型参数中的变体
泛型委托之间的隐式转换,要求类型继承自对方,才可以将泛型委托分配给对方。
泛型类型参数的变体仅支持引用类型
合并委托的时候要求类型完全相同,所以变体委托不应该合并。
泛型委托的协变需要使用关键词out,协变类型只能用作方法返回类型,不能用作方法参数类型;逆变使用关键词in,逆变类型只能用作方法参数类型,不能用作方法返回类型。
可以在同一个委托中支持变体和协变,单这只适用于不同类型的参数。
在做协变和逆变的时候,写不写关键词,协变是可以支持函数实例和匿名函数的委托,逆变可以支持函数实例的的委托,匿名函数的委托就不能支持;这块的文档官方只说了可能会发现可以使用,所以就当一种奇巧淫技使用了。
当不是用关键字来进行逆变和协变的时候,就不能将一个委托分配给另外一个委托了。
```csharp
public delegate R SampleGenericDelegate<out R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;
//错误
public delegate R SampleGenericDelegate<R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;</code></pre>
<p>上述代码就体现了这个问题。</p>
<ul>
<li>合并委托(多播委托)
可以通过使用+运算符将多个对象分配到一个委托实例上。多播委托包含已分配委托列表。此多播委托被调用时会依次调用列表中的委托。仅可合并类型相同的委托。对象必须是已经初始化过。当使用匿名函数合并委托的时候,取消是很麻烦的。
<pre><code class="language-csharp">delegate void CustomDel(string s);
void Hello(string s){Console.WriteLine("Hello" + s);}
//错误1
CustomDel c += Hello;
//错误2
CustomDel c = Hello;
CustomDel d;
c += d;</code></pre>
<p>这两个委托都是没有初始化的。所以在合并的时候就不对。</p>
<pre><code class="language-csharp">CustomDel c = Hello;
c += (string s)=> { Console.WriteLine("niming" + s); };
c -= Hello;
c("aaaaa");</code></pre>
<p>上述代码你如果想在多播委托中删除掉匿名函数的委托,你只能再定义一个委托实例,把匿名函数赋值给给委托实例,然后再添加到多播委托上。</p>
<h3>闭包</h3>
<p>委托还能增加一种闭包的概念,这个概念在前面有分析过。</p>
<h2>匿名函数</h2>
<p>在说委托的时候,已经用到了匿名函数,现在具体说下匿名函数的内容
匿名函数是一个“内联”语句或表达式,可在需要委托类型的任何地方使用。可以使用匿名函数来初始化命名委托,或传递命名委托作为方法参数。
可以使用lambda表达式或匿名方法来创建匿名函数。某些类型的lambda表达式可以转换为表达式树类型</p>
<h3>匿名方法</h3>
<p>delegate 运算符创建一个可以转换为委托类型的匿名方法。</p>
<h3>lambda表达式</h3>
<p>只是使用委托的更方便的语法,将声明签名和方法正文,但是在分配到委托之前没有自己的正式标识。</p></li>
<li>表达式lambda,表达式为其主体:
<pre><code class="language-csharp">(input-parameters)=>expression</code></pre>
<p>使用=>从其主体分离lambda参数列表。在左侧指定输入参数,在另一侧输入表达式或语句块。
lambda广泛用于表达式树的构造。
只有当lambda只有一个输入参数的时候,小括号才是可选的;否则括号是必须的。
输入参数类型必须全部为显式或者全部为隐式;否者会编译出错。
表达式lambda的主体可以包含方法调用。不过若要创建在.NET公共语言运行时的上下文之外计算的表达式树,不得在lambda表达式中使用方法调用。在.NET公共语言运行时上下文之外,方法将没有任何意义。</p></li>
<li>语句lambda,语句块作为其主体:
<pre><code class="language-csharp">(inpput-parameters) => {<sequence-of-statements>}</code></pre>
<p>语句lambda与表达式lambda表达式类似,只是语句括在大括号中。
语句lambda的主体可以包含任意数量的语句;但是实际上通常不会多于两个或者三个。
语句 lambda 也不能用于创建表达式目录树。</p>
<h3>异步lambda</h3>
<p>使用关键词async和await。</p>
<h3>lambda 表达式和元组</h3>
<p>c# 7.0起,c#语言提供了对元组的内置支持。可以提供一个元组作为Lambda表达式的参数,同时Lambda表达式也可以返回元组。可通过用括号括住用逗号分隔的组件列表来定义元组。</p>
<pre><code class="language-csharp">Func<Tuple<int, int, int>, Tuple<int, int, int> > doubleThem = ns => (new Tuple<int, int, int> (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3));
var numbers = new Tuple<int, int, int>(2, 3, 4);
var doublednumbers = doubleThem(numbers);</code></pre>
<p>元组类型关键词是Tuple,通常是item1,item2这样的字段命名。但是也可以使用命名组件定义元组。</p>
<pre><code class="language-csharp">Func<(int n1, int n2, int n3), (int, int, int)> doublethem = ns => (2 * ns.n1, 2 * ns.n2, 3 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doublethem(numbers);</code></pre>
<p>上述是使用命名组件定义元组的示例代码。</p>
<h3>捕获lambda表达式中的外部变量和变量范围</h3>
<p>lambda可以引用外部变量。这些变量在定义lambda表达式的方法中或包含lambda表达式的类型中的范围内变量。以这种方式捕获的变量将进行存储以备在lambda表达式中使用,即使在其他情况下,这些变量将超出范围并进行垃圾回收。必须明确地分配外部变量,然后才能将lambda表达式中使用该变量。</p></li>
<li>捕获的变量将不会被作为垃圾回收,直至引用变量的委托符合垃圾回收的条件。</li>
<li>在封闭方法中看不到lambda表达式内引入的变量。</li>
<li>lambda表达式无法从封闭方法中直接捕获in,ref或out参数。</li>
<li>lambda表达式中的return语句不会导致封闭方法返回。</li>
<li>如果相应跳转语句的目标位于lambda表达式块之外,lambda表达式不得包含goto,break或continue语句。同样,如果目标在块内部,在lambda表达式块外部使用跳转语句也是错误的。
<h2>事件</h2>
<p>要定义一个事件,可以在事件类的签名中使用event关键字,并制定事件的委托类型。
还能显式创建包含添加或删除处理程序的属性,创建方法和属性的语法类似,只是替换关键词为add,remove。</p>
<h3>使用委托添加事件</h3>
<pre><code class="language-csharp">
delegate void CustomDel(string s);
event CustomDel cusevnet;
void funcust(string s){</code></pre></li>
</ul>
<p>}
cusevnet += funcust;</p>
<pre><code>### .NET支持事件的委托
```csharp
private EventHandler<T> directoryChanged;
event EventHandler<T> DirectoryChanged
{
    add{ directoryChanged += value;}
    remove{directoryChanged -= value;}
}

EventHandler

是一个预定义的委托,当不生成数据的时候用EventHandler,但是当有数据产生的时候用泛型EventHandler&lt TEventArgs &gtTEventArgs是继承EventArgs的类型。

事件委托的签名

void OnEventRaised(object sender,EventArgs args)

上述代码为.NET事件委托的标准签名。返回类型为void,参数列表包含两个参数:发件人和事件参数。sender的编译类型为system.object,第二个参数通常派生自system.EventArgs的类型,即使事件类型不需要任何参数,你仍要提供者两个参数。应使用特殊值EventArgs.Empty来表示事件不包含任何附加信息。在.NET Core版本中对TEventArgs的定义不再要求必须派生自System.EventArgs的类。

两种事件对比

EventHandler事件是不带返回值的,传入参数只能为固定的两个,而委托事件是可以自定义参数和返回值的。

[编程基础]c#托管资源和非托管资源

前言

GC(垃圾回收)实现的时候,对引用资源管理都是对托管资源的管理,而在c#中还有一些非托管资源,现在简单讲下,两者的区别,和对非托管资源是怎么释放内存的。

托管资源

.NET中的类型都是从System.Object类型派生出来的。
分为两大类:
引用类型(reference type),分配在内存堆上。
值类型(value type),分配在堆栈上。

分配内存

值类型在栈里,先进后出,值类型变量的生命有先有后,这个确保了值类型变量在退出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。
引用类型分配在托管堆上,声明一个变量在栈上保存,当实例化一个对象的时候,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存。
初始化新进程的时候,CLR会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。一开始该指针指向托管堆的基址。托管堆上包含了所有的引用类型。从托管堆中分配内存要比非托管内存分配速度快。由于CLR通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。另外由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

释放内存

这个就是GC所做的事情了,参考c#GC。

非托管资源

应用使用完非托管资源后,必须要显式释放这些资源。最常见的非托管资源类型是包装操作系统资源的对象。虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但是无法了解如何发布并清理这些非托管资源。所以清理需要做以下操作:

  • 实现清理模式:需要提供IDisposable.Dispose实现以启用非托管资源的确定性释放。当不再需要此对象时,类型使用者可以调用Dispose。Dispose方法立即释放非托管资源。

  • 在类型使用者忘记调用Dispose的情况下,准备释放非托管资源。有两种方法可实现此目的:

    • 使用安全句柄包装非托管资源。安全句柄派生自System.Runtime.InteropServices.SafeHandle类并包含可靠的Finalize方法。在使用安全句柄时,只需要实现IDisposable接口并在Dispose实现中调用安全句柄的IDisposable.Dispose方法。如果未调用安全句柄Dispose方法,则垃圾回收器将自动调用安全句柄的终结器。
    • 重写Object.Finalize方法。当类型使用者无法调用IDisposable.Dispose以确定性地释放非托管资源时,终止会启动对非托管资源的非确定性释放。

然后,类型使用者可直接调用IDisposable.Dispose实现以释放非托管资源使用的内存。在正确实现Dispose方法时,安全句柄的Finalize方法或Object.Finalize方法的重写会在未调用Dispose方法的情况下阻止清理资源。