装箱和拆箱-值类型和引用类型的区别
一、概述
在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的会改变呢?下面我们详细分析下:
首先,我们先要理解ref是什么,对于Class类型使用 ref,是为了保持引用的地址是一致的。所以在使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。
如果还不清楚,就跟着代码走一遍吧!
1.在控制器中先打一个断点,我们可以在局部变量中看到他们的初始值都为0.
2.转到方法Test2中,在还没有开始修改引用类型的值的时候,str的值还是0.
3.在走过方法Test2后,我们看到引用类型str的值变为了"666".
4.走出这个方法,我们看看,str1的会因此改变吗?
!!!str1居然没有改变,他居然还是0!!
4.同样操作,我们在RefStringTest2方法后看看会怎么样。
NEXT》》》进入方法,先不执行赋值操作
NEXT》》》走过赋值操作
NEXT》》》走出方法,去看看str2的值改变了吗
可以看到,str2的值已经变成了“666”。
同样的操作,我们走过剩下两个方法,可以看到
可以看出,只有str2和int2的值改变了,也就是只有使用了Ref的值会被修改。但这是为什么呢?我们再一探究竟,Come!!
传递引用参数的时候传递的是一个地址的值,在RefStringTest2方法内,传入str1的地址会被str给修改(所以我们要用ref来保证a和str的地址一致,这样当str的值改变时,str1也会改变,因为他们指向的是同一个地址,也就是同一个值!!),所以输出了666. 如果这里没有ref,那么,传入的参数地址,(也就是str1的地址)会被str修改成其他地址,在StringTest2方法内部,修改的是str的地址指向的值,出了方法,str1地址指向的值并没有改变!!
而传递值类型参数的时候传递的是一个真实的值,他没有地址,在IntTest方法内,由于没有使用ref,形参int1的值把这个值“0”拷贝一份,然后把拷贝后的值传递到了方法内部,所以,在方法内改变的只是拷贝的值,方法结束后int1的值还是0. 而使用ref的int2,保证参数value和传入的int2的内存位置一致,这样,同一内存位置的值才会被修改!!!
从堆栈地址可以更直观得看出:
来,,,对比着看一下
发现了什么,看得出来,str和str1的地址并不一样!!既然地址都不一样,那你给“str = "666";和我str1有啥关系??我str1的值还是0啊大哥!!
来,再去RefStringTest2方法看看,看看ref的作用
进入方法前,看好地址!!Look!!
走过方法后,Look!!
看出了什么???是不是str2和str的地址居然一样!!!地址一样,指向的值自然是同一个。
OK,解释完毕了。(上面地址其实我也不清楚是堆还是栈地址,😓汗,但相同不相同还是看得出来的,等我请教大佬再补上!!)