Notifications
Article
推陈出新之 --- 拆装箱
Updated 3 months ago
770
4
日常的开发中,我们会注意避免拆装箱的发生来规避 GC ,但是 总在那不经意的API调用时就发生了隐式的装箱,本文将 深入浅出 的介绍 box\unbox 的 常见坑 以及 闭坑指南。

什么是拆装箱

  • 装箱 box 在值类型向引用类型转换时发生,在堆中分配
  • 拆箱 unbox 拆箱在引用类型向值类型转换时发生。
首先呢,我们需要知道 值类型 与 引用类型 的内存分配规则, 值类型是需要放在栈上的,而引用类型是在堆上分配的。
那么,我们在将 值类型 赋值给 引用类型的时候 会就发生 装箱操作了。可以想象一下,我们在栈上 申请了一个 Int,在堆上申请了一个 Object 类型。然后将 Int 赋值给 Object ,而由于 值类型 与 引用类型的 在内存中的形式不同。我们需要在 堆上建立个 小盒子(盒子大小取决于被丢进来的值类型大小) ,然后将 值类型 丢进盒子,那么就完成这个赋值操作了。至于拆箱就可以理解为 装箱的反向操作了。

举个栗子

说完了概念,可能对部分同学会比较抽象。
我们来看一个装箱行为的示例。
public void BoxIn() { object tempObj = 4; }
我们使用 Ildasm 工具查看改函数被编译成 IL指令后 的形式如下
method public hidebysig instance void BoxIn() cil managed { // Code size 8 (0x8) .maxstack 2 .locals init (object V_0)/*声明object类型的名称为objValue的局部变量 */ IL_0000: ldc.i4.4 /*小于8大于0的情况下用的是ldc.i4 num ,而大于8 和小于0又是针对32位(在64 位下还有ldc.i8 num )的机器编程就用 ldc.i4.s num */; IL_0001: box [mscorlib]System.Int32 /*执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间*/ IL_0006: stloc.0 /*弹出堆栈上的变量,将它存储到索引为0的局部变量中*/ IL_0007: ret } // end of method Test::BoxIn
接下来看看拆箱回发生什么样的事情
public void BoxOut() { //首先进行一次装箱操作将整形数字装箱成引用类型 object tempObj = 9; //执行一次拆箱操作,将存储到堆上的引用变量tempObj存储到局部整形值类型变量tempInt中 int tempInt = (int)tempObj; }
同样的,我们来看看这段代码会生成哪些操作指令
method public hidebysig instance void BoxOut() cil managed { // Code size 16 (0x10) .maxstack 2 .locals init (object V_0, int32 V_1) IL_0000: ldc.i4.s 9 IL_0002: box [mscorlib]System.Int32 IL_0007: stloc.0 /*运行原理同上*/ IL_0008: ldloc.0 /*将索引为0的局部变量(即tempObj变量)压入栈*/ IL_0009: unbox.any [mscorlib]System.Int32 /*执行拆箱指令将引用类型object转换成System.Int32类型*/ IL_000e: stloc.1/*将栈上的数据存储到索引为1的局部变量即tempInt*/ IL_000f: ret } // end of method Test::BoxOut
至此,我们知道 执行装箱操作时不可避免的要在堆上申请内存空间,并将堆栈上的值类型数据复制到申请的堆内存空间上,因此消耗内存和cpu资源

有哪些常见的坑,以及如何避免?

1. 以 string.Format 为代表的 Object入参 API

  • public static String Format(String format, object arg0, object arg1, object arg2);
  • public static String Format(String format, object arg0, object arg1);
  • public static String Format(String format, object arg0);
错误示例: string.Format("Box {0}, {1}", 1, 2);
正确操作: string.Format("without Box {0}, {1}", 1.ToString(), 2.ToString());

2. MethodInfo.Invoke

  • public abstract object Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture)
部分项目中会用到 反射的机制来实现方法的调用,可以看到 parameters 参数设计是一个Object的数组,这将会有两个问题
  1. 需要构造一个数组
  2. 入参有值类型将发生装箱
解决方案是我们需要 利用 MethodInfo 来创建一个 Delegate ,例如:
var tempMethodInfo = typeof(Material).GetMethod("SetVectorArrayImpl", BindingFlags.NonPublic | BindingFlags.Instance); Action<Material, int, Vector4[], int> _SetVectorArrayImp = tempMethodInfo.CreateDelegate(typeof(Action<Material, int, Vector4[], int>)) as Action<Material, int, Vector4[], int>; _SetVectorArrayImp (123,new Vector4[]{Vector4.one},1);

3. 其他代码编程中的接口设计可以使用 泛型 来规避这个问题

4. <del>...暂时想不起来了...</del>

郡墙
Programmer - Programmer
19
Comments
白开水
3 months ago
👍👍👍
0
郡墙
3 months ago
Programmer
zhang zhidong写的不错👌👍
谢谢支持,欢迎关注收藏
0
冬雪
3 months ago
喜欢游戏并热衷创作
👍👍
0
zhang zhidong
3 months ago
coder
写的不错👌👍
1