C#装箱和拆箱

装箱与拆箱(又叫取消装箱)就是值类型与引用类型的转换,是值类型和引用类型之间的桥梁。

之所以可以这样转换是因为 CTS 允许这样做。只有值类型才存在装箱和拆箱。

装箱是隐式的,拆箱是显式的,因为你需要告诉 CLR 你要给拆出来的值赋予什么类型。

通过深入了解装箱与拆箱的过程,我们可以知道其中包含了对堆上内存的操作,故会消耗性能,这是完全不必要的。

另外值得注意的是,装箱需要比原数据更多的空间,因为它需要两个引用类型的标准配置:类型对象指针和同步块索引。

装箱的过程

装箱就是把值类型转换为 object 类型或由此值类型实现的任何接口类型,如下图所示:

int i = 1;
object o = i;

具体过程:

  • 在堆中申请内存,内存大小为值类型的大小,再加上额外固定空间(类型对象指针和同步块索引)。
  • 将值类型的字段值拷贝到新分配的内存中。
  • 返回新引用对象的内存地址(给栈上的引用)。

装箱

我们可以从图中看到,装箱就是生成图中除了一开始 i=1 的变量之外另外两块变量的过程。

实际上,仅仅通过观察 C# 代码,是无法意识到装箱的,只有访问对应的 IL 代码才能真正观察到装箱。

IL 代码的装箱指令为 box。上面两行代码对应的 IL 代码为:

IL_0040: ldc.i4.1
IL_0041: stloc.2
IL_0042: Idloc.2
IL_0043 : box [mscorlib] System.Int32
IL_0048: stloc.3

其中前两行对应int i=1这句代码,后三行对应object o=i这句代码。

拆箱的过程

简单地说,就是把装箱后的引用类型转换为值类型。由于并非一定成功,所以存在抛出异常的可能。

IL 代码的拆箱指令为 unbox。具体过程:

int b = (int) o;

1) 检查是否为 Null,否则抛出 NullReferenceException 异常。检查实例是否为给定值类型的装箱值。否则抛出 InvalidCastException 异常,最后获得对象各个成员的地址

2) 创建一个新的对象 b,并将第一步获得的值复制到 b 中。

在 CLR via C# 中,拆箱被定义为第一步。下面的代码就是拆箱:

(int) o;

但这句代码是无法通过编译的。通常来说,我们拆箱的目的都是为了将值拷贝到一个值类型中,所以拆箱之后,往往伴随着一次值的复制动作。上面的例子中,就将值复制到了变量 b。

拆箱对应的 IL 代码为:

IL_000a: Idloc.1
IL_000b: unbox.any [mscorlib]System.Int32
IL_0010: stloc.2

与拆箱比较,装箱的性能消耗更大,因为引用对象的分配更加复杂,成本也更高,值类型分配在栈上,分配和释放的效率都很高。

装箱过程需要创建一个新的引用类型对象实例。

如何避免拆箱和装箱

在 C#1 的时代,没有泛型,我们要定义一组自定义类型的数组只能使用 ArrayList。

由于 ArrayList 支持任何类型,所以其方法的参数全都是 object,这意味着即使我们的类型是结构体,也会被隐式地装箱,然后在使用时再拆箱。

C# 2 的泛型解决了这个问题。我们可以通过使用泛型集合避免不必要的装箱和拆箱。

很多地方会出现隐蔽的装箱,例如,对结构体的判等。当我们要实现自定义结构体的判等时:

struct Rectangle
{
    public override bool Equals(object obj)
    {

    }
}

我们发现,默认的签名为将两个比较对象转换为 object,这当然会引起装箱了。解决的办法是令结构体实现 IEquatable<T>接口 :

struct Rectangle : IEquatable<Rectangle>
{
    public bool Equals(Rectangle r)
    {
        //…
    }
}

这样一来,即使类型存在两个 Equals,CLR 也会优先选择类型较小的那个,即参数为具体类型的 Equals,而不是参数为 Object 的 Equals。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/22364.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论