前言
CLR(Common Language Runtime)支持两种类型:值类型和引用类型。
关于这两兄弟,很多程序员都不会去深究。大部分也就是在面试前草草看两眼。其实非常多的BUG和性能优化都需要从这两兄弟开始下手
本篇仅记录一些经常被忽视的特性,有兴趣的朋友欢迎在文章下方留言讨论.
值类型
1.所有的值类型都是隐式密封。
其目的是防止将值类型作为其它值类型或者引用类型的基类。
具体表现为:
2.值类型不受垃圾回收器的控制
原因是绝大部分值类型储存在栈里(值类型可以作为引用类型的字段存在堆里),而垃圾回收器只能管道堆。
因此值类型的大量使用可以缓解托管堆的压力,并减少引用程序生存周期内的垃圾回收次数!
总而言之,在开发时应优先考虑值类型
3.值类型的定义方式
int i=5;//标准的定义方式
bool b=new bool();//忽悠‘小弟’专用的定义方式
所以:永远不要用是否可以new的的思维来区分值类型和引用类型!
有的人可能会觉得int i创建的是变量,而int i = new int()创建的是对象实例
,所以int i = new int()其实比int i更消耗内存。 这种说法常按理说是没问题的,可我们的宇宙第一编译器具有无可匹敌的兼容性。所以在C#里这两种写法其实几乎没有区别,因为他们最终生成的IL代码是一样的。但是第二种写法比较繁杂且具有一定迷惑性,所以几乎没什么人用。
4.结构体
结构体也是值类型.你可以简单地认为当一个“类”被struct修改而不是class的时候,那么这个“类”就是结构体。
//定义一个结构体
public struct Val
{
public int MyInt;
}
a) 结构体的使用方式。
Val yourInt;
Val myInt = new Val();
与其它值类型不同的是,上面两种方式其实有一点小区别:第二种方式编译器会认为实例已初始化,而第一种编译器会在编译的时候为其初始化(生成的IL是一致的)。以下代码可以更清楚地进行说明:
b) 结构体不可以自己写默认的构造器。如果要写构造器,则必须把所有字段初始化一次。效果如下:
引用类型
使用引用类型时,必须注意性能问题。
a) 引用类型的内存必须从托管堆分配,而其地址存在栈里。
b) 托管堆里分配的每个对象都会有一些额外成员,这些成员必须初始化
c) 从托管堆分配对象时,可能会强制执行一次垃圾回收(现在应该不存在这个顾虑了,没深究过)
值类型和引用类型的区别
1.值类型分配在栈上,引用类型分配在堆上。
在拷贝值类型的时候其实是把值类型的地址和内存拷贝给了新的对象,
在拷贝引用类型的时候是把引用类型的地址拷贝给了新对象。也就是说引用类型的新老对象其实是两个地址一个内存,当你修改新对象的值的时候其实吧老对象的值也该了。以下代码可以更直观地表现:
2.几乎所有的引用类型都直接从System.Object继承,而值类型则直接继承System.Object的子类System.ValueType
3.值类型转换为引用类型称为装箱,引用类型转换为值类型称为拆箱
因为C#所有的类型都继承自Object所以值类型和引用类型可以互转.而这一行为就被称为装箱或者拆箱.
装箱:值类型=>Object=>引用类型 : 将值类型字段拷贝到托管堆上发生的内存分配
拆箱:引用类型=>Object=>值类型 : 把托管堆上值类型数据传递到栈
为什么需要装箱或者拆箱?
在实际开发的时候经常会遇到不确定入参类型或者返回类型的情况.这个时候就可以直接把不确定的类型设置为Object,然后在使用时候再使用其具体类型.
但是这种操作是极其消耗内存的.所以从C#0.2开始我们有了泛型.
理论上一切你需要拆装箱的操作都可以用泛型取代.
4.new对象操作不一定会在堆上开辟内存
在new一个引用类型时,C#会在堆上为对象开辟一块内存,并返回该内存的地址储存在栈上。 而在new一个结构体时,编译器会识别到它,并在栈上为其开辟内存(宇宙第一编译器的基本操作)。