C#装箱和拆箱(文字部分有点多,希望大家能耐心看下去)

C#中装箱和拆箱

定义

把值类型实例转换为引用类型实例就是装箱。反过来,把引用类型转换为值类型就是拆箱。

值类型:值类型的变量会直接存储数据,如byte、short、int、long、float、double、decimal、char、bool和struct,统称为值类型。

引用类型:引用类型的变量持有的是数据的引用,其真实数据存储在数据堆中,如所有的class实例的变量、string和class,统称为引用类型。当声明一个类时,只在堆栈(堆或栈)中分配一小片内存用于容纳一个地址,而此事并没有为其分配堆上的内存空间,因此它是空的,也就是为null,直到使用new关键字创建一个类的实例,分配了一个堆上的空间,并把堆上空间的地址保存给这个引用变量,这时这个引用变量才真正指向内存空间。

举例如下:

int a = 5;//声明一个引用类型变量a
object obj = a; //声明一个引用变量obj,并把a赋值给obj

这就是装箱,因为a是值类型,是直接有数据的变量,obj为引用类型,指针与内存拆分开来,把a赋值给obj,实际上就是obj为自己创建了一个指针,并指向了a的数据空间。

int a = 5;//声明一个引用类型变量a
object obj = a; //声明一个引用变量obj,并把a赋值给obj
a = (int)obj; //将obj赋值给a

这就是拆箱,相当于把obj指向的内存空间复制一份交给了a,因为a是值类型,所以它不允许指向某个内存空间,只能依靠复制数据来传递数据。

作用

了解了装箱拆箱的定义,我们要进一步思考,为什么需要装箱拆箱?他们到底有啥作用?

装箱作用

简单来说,有时候需要将值类型当作对象来处理。

例如,当你需要将值类型存储在集合类(如ArrayList、List)中,或者传递给参数为object类型的方法时。

下面举几个具体的例子:

1. 将值类型存储在集合中:

List<object> myList = new List<object>();
int intValue = 42;
myList.Add(intValue); // 需要装箱

在这个例子中,List<object> 是一个集合,它可以存储各种类型的对象。由于集合存储的是对象,而不是值类型,所以需要将 int 值类型装箱成 object

2. 将值类型传递给参数为object的方法:

void MyMethod(object parameter) {
    // ...
}

int intValue = 42;
MyMethod(intValue); // 需要装箱

在这里,MyMethod 方法接受一个 object 类型的参数。当我们将 int 类型的值传递给这个方法时,会发生装箱。

3. 使用非泛型集合类:

ArrayList myList = new ArrayList();
int intValue = 42;
myList.Add(intValue); // 需要装箱

ArrayList 是一个非泛型集合,它可以存储任意类型的对象。当我们向其中添加值类型时,会发生装箱。

拆箱作用

当需要从引用类型中取出值类型的数据时,就会发生拆箱操作。

下面举几个具体的例子:

1. 从集合中取出值类型:

List<object> myList = new List<object>();
int intValue = 42;
myList.Add(intValue); // 装箱
object boxedValue = myList[0];
int unboxedValue = (int)boxedValue; // 拆箱

在这个例子中,myList[0] 返回一个 object 类型,我们需要将其拆箱为 int 类型。

2. 从数组中取出值类型:

object[] objectArray = new object[] { 42, "Hello" }; // 装箱
int unboxedValue = (int)objectArray[0]; // 拆箱

在这个例子中,数组中的元素被装箱成 object,我们需要将其拆箱为 int 类型。

3. 从方法返回值类型:

object GetObjectValue() {
    int intValue = 42;
    return intValue; // 装箱
}

object boxedValue = GetObjectValue();
int unboxedValue = (int)boxedValue; // 拆箱

在这个例子中,GetObjectValue 方法返回一个 object 类型的值,我们需要将其拆箱为 int 类型。

更深一层的思考

​ 因为装箱和拆箱的需求通常涉及到值类型与引用类型之间的转换,值类型和引用类型又涉及到堆栈内存,所以这里再深入介绍下它们几个。

值类型

​ 值类型包括所有整数、浮点数、bool和Struct声明的结构体。这里需要注意Struct部分,这是经常犯错的地方,很多人会直接把结构体当做类来用,这是错误的,因为他是值类型,在复制操作时是通过直接复制数据完成操作的,所以常常会有a、b同是结构的实例,a赋值给了b,在b更改了数据之后,发现a的数据却没有同步的疑问出现,事实上,他们根本就是两个数据空间,在a赋值给b时,其实并不是引用复制,而是整个数据空间复制,相当于a、b为两个不同的东西,只是长得差不多而已。

引用类型

​ 引用类型包括类、接口、委托(委托也是类)、数组以及内置的object与string。前面说了delegate也是类,类都是引用类型(注意,说类都是引用类型只是为了方便记,实际上int等值类型也是类,所以只要记住这几个特殊的值类型的类、其他都是引用类型的类就可以了)

栈内存与堆内存:

​ 栈是用来存放对象的一种特殊的容器,是最基本的数据结构之一,遵循先进后出的原则。它是一段连续的内存,所以对栈数据的定位比较快速;而堆则是随机分配的空间,处理的数据比较多,无论情况如何,都至少需要两次才能定位。堆内存的创建和删除节点的时间复杂度是O(lgn)。栈创建和删除的时间复杂度则是O(1),栈速度更快。

​ 既然栈速度快,全部用栈不就好了?这又涉及到生命周期问题,由于栈中的生命周期必须确定,销毁时必须按照次序销毁,即从最后分配的块部分开始销毁,创建后什么时候销毁必须是一个定量,所以在分配和销毁上不灵活,它基本都用于函数调用递归调用这些生命周期比较确定的地方。相反,堆内存可以存放生命周期不确定的内存块,满足当需要删除时再删除的需求,所以堆内存相对于全局类型的内存块更合适,分配和销毁更灵活。

​ 很多人把值类型与引用类型归为栈内存和堆内存分配的区别,这是错误的,栈内存主要为确定性生命周期的内存服务,堆内存则更多的是无序的随时可以释放的内存。因此值类型可以在堆内也可以在栈内,引用类型的指针部分也一样,可以在栈内和堆内,区别在于引用类型指向的内存块都在堆内,一般这些内存块都在托管堆内,这样便于内存回收和控制,我们平时所说的GC机制就会做些回收和整理的事。也有非托管堆内存、不归托管堆管理的部分,他们是需要自行管理的,比如C++编写一个接口生成一个内存块,将指针返回给了C#程序,这个非托管堆内存就需要我们自行管理,C#也可以自己生成非托管堆内存块。

装箱内部操作

根据相应的值类型在堆中分配一个值类型内存块,再将数据复制给他,这要按三步进行。

第一步:在堆内存中新分配一个内存块(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex类)。

第二部:将值类型的实例字段复制到新分配的内存块中。

第三步:返回内存堆中新分配对象的地址。这个地址就是一个指向对象的引用。

拆箱内部操作

​ 拆箱比较简单,先检查对象实例,确保它是给定值类型的一个装箱值,再将该值从实例复制到值类型变量的内存块中。

效率及优化

​ 由于装箱、拆箱时生成的是全新的对象,不断的分配和销毁内存不但会大量消耗CPU,同时也会增加内存碎片,降低性能。那该如何做呢?

​ 我们需要做的就是减少装箱、拆箱的操作。在编程规范中要牢记减少这种浪费CPU内存的操作,在平时编程时要特别注意。

​ 整数、浮点数、布尔值等值类型变量的变化手段很少,主要靠加强规范、减少装拆箱的情况来提高性能。但Struct不一样,它既是值类型,又可以像类一样继承,用途多,转换的途径也多,但稍不留神,花样就变成了麻烦,所以这里再讲讲Struct变化后的优化方法。

Struct优化方法

1.Struct通过重载函数来避免拆箱、装箱。

​ 比如常用的ToString()、GetType()方法,如果Struct没有写重载ToString()和GetType()的方法,就会在Struct实例调用它们时先装箱再调用,导致内存块重新分配,性能损耗,所以对于那些需要调用的引用方法,必须重载。

2.通过泛型来避免拆箱、装箱。

​ 不要忘了Struct也是可以继承的,在不同的、相似的、父子关系的Struct之间可以使用泛型来传递参数,这样就不用在装箱后再传递了。

​ 比如B、C继承A,就有这个泛型方法void Test(T t) where T : A,以避免使用object引用类型形式来传递参数。

3.通过继承统一的接口提前装箱、拆箱,避免多次重复装箱、拆箱

​ 很多时候装箱、拆箱不可避免,这时可以让多种Struct继承某个统一的接口,不同的Struct可以有相同的接口。把Struct传递到其他方法里,就相当于提前进行了装箱操作,在方法中得到的是引用类型的值,并且有它需要的接口,避免了在方法中完成重复多次的装箱、拆箱操作。

​ 比如Struct AStruct B都继承了接口I,我们调用的方法是void Test(I i)。当调用Test方法时,传进去的Struct AStruct B的实例相当于提前执行了装箱操作,Test方法里拿到参数后就不用再担心内部再次出现装箱、拆箱的问题了。

​ 最后依然要提醒一下,如果没有深入理解Struct值类型数据结构的原理,用起来可能会存在很多麻烦,不要盲目的认为使用结构体会上性能提升,在没有完全彻底理解之前就贸然大量使用结构体,可能会对程序性能带来重创。

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值