一、概述
在C#中,数据根据变量的类型以两种方式中的一种存储在一个变量中。变量的类型分为两种:引用类型和值类型,这也是CLR支持的两种类型。
二、定义
1.引用类型:
分配在堆上的类型称为引用类型。
解析:一个可以称为”类“的类型都是引用类型。 引用类型总是从托管堆上分配的,常用的语法就是New XX(). C#的new 操作符会返回对象的指针 - 也就是指向对象数据的内存地址的一个引用。引用类型的传递其实传递的是对象的指针(string类型比较特殊),所以在特定的场景下性能是高于值类型的。一个引用类型在创建时默认为null,也就是说当前变量不指向一个有效的对象,也就是我们常遇到的异常“未将对象引用设置到对象的实例”。
2.值类型:
值类型一般在线程栈上分配。
三、区别
我们总图然后详细分析。
1.值类型的数据存储在内存的栈中,内存分配是自动释放,在GC的控制之外,不会对GC造成压力,所以值类型存取速度快;引用类型的数据存储在内存的堆中,在.NET中会有GC来释放,而内存单元中只存放堆中对象的地址,在.NET中会有GC来释放所以存取速度慢。我们可以这么理解,值类型就是现金,要用直接用;引用类型是存折,要用还得先去银行取现。
当然,值类型虽然存取速度快,但也不能卵用,举个例子:我自定义一个struct 类型作为一个方法的参数会发生什么呢?每次调用都会发生全字段的赋值,这是不可接受的,这也是典型的值类型勿用场景。
2.值类型表示实际数据,引用类型表示指向存储在内存堆中的数据的指针或引用。
3.值类型继承自System.ValueType,引用类型继承自System.Object。
4.值类型总是包含一个值,而引用类型可以是null。
四、封箱和拆箱
封箱(boxing)是把值类型转换为引用类型(System.Object)。拆箱(unboxing)是相反的转换过程。
封箱的过程:
1.在托管堆中分配好内存,分配的内存量是值类型的各个字段需要的内存量加上托管堆上所以对象的两个额外成员(类型对象指针,同步块索引)需要的内存量。
2.值类型的字段复制到新分配的堆内存中。
3.返回对象的地址,这个地址就是这个对象的引用。
从图可知,对象 o 存的是地址引用,指向的是堆上的值,这个值的类型和变量 i 一样,也是 int 类型,值(123)也就是从栈上变量 i复制过来的一个副本值而已。(所以装箱就是在堆上分配好内存,再复制栈上的值,再将堆的地址引用返回到栈上)
拆箱的过程:
1.获取已经装箱的值类型实例的指针。
2.把获取到的值复制到栈。
所以装箱是比较耗费性能的,还有可能引发一次GC操作,而拆箱只是一个获取指针的过程耗费资源要比装箱小的多。注意:一个对象拆箱之后只能还原为原先未装箱之前的类型,例如:你不能把int32类型装箱后还原为int16类型。
引用类型和值类型区别实例:
1.我们首先定义一个类,用来处理数据。
2.在控制器中先定义两个数据类型,赋值都为0。调用上面的类,看看会输出什么。
前台页面(简单实例,就直接用session了,平时可不要这么写)
输出页面:
这究竟是怎么回事呢,引用类型的值为什么会改变呢?下面我们详细分析下:
首先,我们先要理解ref是什么,对于Class类型使用 ref,是为了保持引用的地址是一致的。所以在使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。
如果还不清楚,就跟着代码走一遍吧!
1.在控制器中先打一个断点,我们可以在局部变量中看到他们的初始值都为0.
2.转到方法Test2中,在还没有开始修改引用类型的值的时候,a的值还是0.
3.在走过方法Test2后,我们看到引用类型a的值变为了5454.
4.同样操作,我们在Test方法后得到的值类型数据还是0。
同样是修改变量的值,为什么值类型的却并没有改变呢?这就是他们各自的特性造成的了。传递引用参数的时候传递的是一个地址的值,在Test2方法内,形参a的地址被实参str给修改了,所以返回时,a的地址变了,由原先的指向0的地址变为了指向5454的地址,所以输出了5454.而传递值类型参数的时候传递的是一个真实的值,他没有地址,在Test方法内,形参b的值把这个值“0”拷贝一份,然后把拷贝后的值传递到了方法内部,所以,在方法内改变的只是拷贝的值,方法结束后b的值还是0.
在下面的窗口可以更加直观的看出,变量a的地址在变化。