内存栈和内存堆 Stack vs Heap
栈Stack :
1)是一个内存数组,类似子弹弹夹一样先进后出的结构体,意味着只能从顶部添加或从顶部删除;
2)内存分配是静态的,变量无法调整大小;
3)访问速度相对快;
3)同时只能由一个执行线程使用;
5)一旦脱离作用域,立刻被清除;
堆Heap :
1)是一块内存,在其中分配了块以存储某些类型的数据对象;
2)内存分配是动态的,变量可以调整大小;
3)访问速度相对慢;
4)同时能被多个线程使用;
5)托管堆由GC来清除,非托管堆由个人自行清除;
值类型
值类型被创建后,会在栈Stack上分配一个固定的空间,存储创建的实际数据;
当值类型超出它所存在的作用域时,会在栈上被立刻清除;
引用类型
引用类型被创建后,会在堆Heap上分配一个非固定的空间,存储创建的实际数据;同时在栈Stack上分配一个固定的空间,存储的数据为一个地址,而这个地址代表了刚刚在堆Heap上创建的实际数据的位置;
当引用类型的实例,超出它所存在的作用域时,会在栈上被立刻清除;而在堆上的数据会等待下一次GC回收来清除(如果它可被回收,具体见上一篇博客);
C# 数据类型(值类型,基类为System.ValueType)
- bool :布尔
- char :字符
- decimal :高精度浮点数
- double :双精度浮点数
- float :单精度浮点数
- int :整形
- long :长整形
- enum :枚举
- struct :结构体
没有 == 操作符,需要自己重写 - keyvaluepair :键值对
Dictionary的子元素,通常foreach Dictionary时候的item,因为它继承自结构体struct,所以没有 == 操作符,需要自己重写,虽然Dictionary是引用类型,但是keyvaluepair却是值类型
ps: 这里只列了常用部分的数据类型,其他相近类似的也是属于值类型;
C# 数据类型(引用类型,基类为System.object)
- class :类
- interface : 接口
- delegate : 委托
- object :基类
- string :字符串
被当做值类型来使用的引用类型,string的几乎所有操作符和方法都是通过堆中实际值来计算,而不是栈中的地址; - string[] :
字符串数据,需在初始化的时候就定义好长度; - ArrayList :
类型集合,长度可以伸缩,可以添加任何类型的元素,所以没有类型安全,实际是以object来实现,这就意味着添加元素的时候,会把元素放进object中俗称装箱,拿出时再拆箱成具体类型; - List<T>:
ArrayList的泛型版本,节省了装箱拆箱的性能,对类型有约束,所以是类型安全; - HashTable :
key value集合,长度可以伸缩,可以添加任何类型的元素,所以没有类型安全,实际是以object来实现,同样需要装箱拆箱(也可以说是ArrayList的key value版本)(据说有提供线程安全的方法Synchronized,但还是建议用专门提供线程安全的Concurrent集合) - Dictionary :
HashTable 的泛型版本,节省了装箱拆箱的性能,对类型有约束,所以是类型安全;(也可以说是List<T>的key value版本) - ConcurrentBag :
线程安全版本的List<T> - ConcurrentDictionary :
线程安全版本的Dictionary <T>
ps: 这里只列了常用部分的数据类型,其他相近类似的也是属于引用类型;
Q&A
- Q:引用类型是分配在堆里的吗?
A:引用类型是在堆里存储实际数据,然后在栈中存储指向堆里实际数据的地址;引用类型的Operator(==, ! =, =等)和 方法(add,remove等)均是操作的地址,重写过该方法的除外; - Q:Null是什么数据类型?Nullable是什么数据类型?
- A:Null没有类型,对于引用类型来说,赋值null指的是空引用,对于值类型来说,一般是不能赋值null的,除非是Nullable比如int?;Nullable实际是Nullable<T>的结构体struct,一种实现可赋值空的把戏;
- Q: 所有的类型都继承自System.object,难道System.ValueType不是吗?如果是的话,继承自System.ValueType的类型为什么是值类型而不是引用类型?
A:System.ValueType是继承自System.object,但是CLR对继承自System.ValueType的子类做了特殊处理,使其拥有值类型的特性。 - Q: == vs equals
A:对于引用类型来说,==比较是存在栈中的引用地址,equals则比较的是存在堆中的实际对象值;
对于值类型来说,==和equals都是比较的是存在栈中的实际值,但是 == 会尝试把左右两边的比较值做类型转换再比较,而equals不会;
但是有一些例外,比如string是引用类型,==是被重写过的,实际用的是equals,所以string永远比较是实际值,而class使用equals时,即使属性值都一样也是false,因为class并没有重写equals,所有调的是父类的equals方法, 但是父类并不知道class中有哪些属性,所以也是false。
鉴于==和equals都是可以被覆盖,如果只是单纯想比较引用地址是否一样,可以使用ReferenceEquals,该方法不会被覆盖;
string s1 = "test"; string s2 = "test"; string s3 = "test1".Substring(0, 4); object s4 = s3; Console.WriteLine("{0} {1} {2}", object.ReferenceEquals(s1, s2), s1 == s2, s1.Equals(s2)); Console.WriteLine("{0} {1} {2}", object.ReferenceEquals(s1, s3), s1 == s3, s1.Equals(s3)); Console.WriteLine("{0} {1} {2}", object.ReferenceEquals(s1, s4), s1 == s4, s1.Equals(s4));
The output is:
True True True False True True False False True
- Q:值类型与引用类型在代码上最大的区别?
A:当引用类型实例都是同一地址时,改变被引用对象的值时,所有实例都会改变,除非是通过深拷贝的方式来改变;当引用类型和值类型被当做参数传入另一个方法返回时,引用类型实例会自动改变,而值类型要通过ref、out的方式来返回才可以,这是因为引用类型传递是地址,而值类型传递的是值的复制;