欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET Core CSharp 中级篇 2-1 装箱与拆箱

程序员文章站 2022-06-21 19:57:49
.NET Core CSharp 中级篇 2 1 本节内容为装箱与拆箱 简介 装箱和拆箱是一个相对抽象的概念。你可以想象一下一堆满载货物的大卡车,他是由许多工人将货物集中堆放装入的,对于我们而言在没有打开货箱的时候,我们可以知道这是一辆运货的卡车,里面有着许多货物,但是具体货物是什么,我们只有打开后 ......

.net core csharp 中级篇 2-1

本节内容为装箱与拆箱

简介

装箱和拆箱是一个相对抽象的概念。你可以想象一下一堆满载货物的大卡车,他是由许多工人将货物集中堆放装入的,对于我们而言在没有打开货箱的时候,我们可以知道这是一辆运货的卡车,里面有着许多货物,但是具体货物是什么,我们只有打开后才能知道,并且对于货箱而言,它可以存放任意体积小于自身的货物,也就是说货箱具有通配性。事实上在c#中也是这样,装箱就是将具有实际数据的变量(值类型)打包成一个引用类型(object),而我们货物到货箱的变化,就是我们本节所需要谈论的装箱与拆箱。利用装箱和拆箱功能,可通过允许值类型的任何值与object 类型的值相互转换,将值类型与引用类型链接起来。

装箱

装箱是将值类型转换为引用类型,在此前对于基础类型的讲述中,我曾经提到过值类型是在栈中进行分配的,而引用类型是在堆中进行分配,并且需要注意的是,这个堆,是托管堆。托管堆对应于垃圾回收,也就是说用垃圾回收堆中存储值类型。装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。这里的运用一种最普通的场景是,调用一个含类型为object的参数的方法,该object可支持任意类型,因为所有类型都隐式的继承于object类,以便通用。当你需要将一个值类型(如int32)传入时,需要装箱。另一种用法是,一个非泛型的容器,同样是为了保证通用,而将元素类型定义为object。于是,要将值类型数据加入容器时,需要装箱。

这是一个非常简单的装箱操作:

double price = 13.53;
object temp = price;

这段代码看似异常的和谐和简单,但是你是否想过这个过程发生了什么呢?

还记得我们在类的生命周期中讲到的类的创建过程吗?装箱事实上是一样的,装箱对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。按三步进行。

  • 新分配托管堆内存,值得注意的是,这里内存需要加上方法表指针和syncblockindex指针
  • 将值类型的实例字段拷贝到新分配的内存中。
  • 返回托管堆中新分配对象的地址。这个地址就是一个指向对象的引用了。

显然,从装箱的过程上可以看出,装箱时,生成了一个全新的引用类型,创建类型必定伴随着相对较大的时间损耗。所以应该尽量避免装箱。通常对于装箱的情形,我们可以通过重载函数或者通过泛型来避免。但是假设你想改造的代码为第三方程序集,你无法更改,那你只能是装箱了。对于装箱的过程,在c#中都是隐式的,如果你想要观察这个过程,我建议你使用dnspy或者ilspy进行反编译分析il代码。

不过装箱看似只是一个损耗性能的操作,偶尔也是有作用的一种最普通的场景是,调用一个含类型为object的参数的方法,该object可支持任意为型,以便通用。当你需要将一个值类型(如int32)传入时,需要装箱。另一种用法是,一个非泛型的容器,同样是为了保证通用,而将元素类型定义为object。于是,要将值类型数据加入容器时,需要装箱。

并且特别的,对于已装箱的对象,因为无法直接调用其指定方法,所以必须先拆箱,再调用方法,但再次拆箱,会生成新的栈实例,而无法修改装箱对象。这句话我此前学习c#的时候也纠结了一段时间,后来恍然大悟。直白的意思有点类似于你克隆了你自己,和你一模一样,但是你两是同一个人吗?显然不是,你操作克隆人并不会对你有任何的影响。

下面这段代码你可以尝试一下

struct test
{
    public int x;
    public void test(int x)
    {
        this.x = x;
    }
}


test t = new test();
t.x = 100;
object a = t;//装箱
((test)a).test(300);//x还是100不变,为什么

拆箱

相对于装箱,将一个引用类型(object)类型转换成值类型的过程就是拆箱,说明确一点就是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。拆箱会检查对象实例,确保它是给定值类型的一个装箱值。将该值从实例复制到值类型变量中。不过我查阅了很多资料,对于拆箱操作,讲的少之又少,我猜测,拆箱过程中,会调用gettype这种方法进行严格的匹配。

double price = 13.53;
object obj = price;
double temp = (double) obj;

这是一个拆箱的过程,是将值类型转换为引用类型,再由引用类型转换为值类型的过程。首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。可以认为和装箱是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同装箱操作中一样影响性能。

如果我的文章帮到了你,请为我点一个推荐关注,在github项目页点一颗star,感谢支持

后续我会补上习题以及图片

github

bilibili主页

warrenryan's blog