问:这段代码里有多少次装箱操作?
这里首先创建了一个未装箱的int值类型(v),并将其初始化为4,然后,又创建了一个Object引用类型(o),并希望将其指向v,但是因为引用类型必须指向托管堆中的对象,所以会产生适当的IL代码将v进行装箱,并将v的装箱“拷贝”的地址存放在o中,随后123被放入未装箱的值类型v中,这对已经装箱的int值没有任何影响,它的值还是4。
接着,代码中调用了WriteLine方法,WriteLine方法期望的参数是一个String对象,但代码中没有,反而只有3个对象:一个未装箱的int值类型(v),一个String(引用类型),以及一个指向已经装箱的int值类型(o)的引用,并且这个已经装箱的值类型正被转型为一个未装箱的int类型,这3个对象必须被组合为一个String对象。
为了将3个对象组合为一个String对象,会调用String的静态Concat方法,public static String Concat(Object arg0, Object arg1, Object arg2);所以v必须被装箱,最后一个变量o首先被转型为一个int型,再被装箱,最后装箱后的内存地址才会被传递给arg2。
这里又发生了几次装箱?是1次。这是因为System.Console类中定义了一个接受int类型参数的WriteLine方法,public static void WriteLine(System.Int32 value);因此上面方法调用中的v(未装箱值类型实例)以传值的方式被传递。
如果我们知道自己写的代码会导致编译器反复地对一个值类型进行装箱,那么我们应该自己来做这样的装箱操作,这样就可以能够减少生成的代码量,并且提高代码运行速度。
若定义了INEFFICIENT符号,那么编译器将对v执行3次装箱,从而导致在托管堆上分配3个对象,这当然很浪费内存和时间。若没有INEFFICIENT符号,那么编译器将对v只执行1次装箱,只会在托管堆上分配1个对象,最后这个被装过箱的int 值分3次传递给Console.WriteLine()方法,显然,这要比前者分配的内存要少,执行速度更快。
未装箱类型相对装箱类型而言,有两点优势:1)不被分配在托管堆上,而是分配在线程所属的堆栈中。2)没有托管堆上的对象都有的额外的附加成员:一个方法表指针和一个SyncBlockIndex.但由于未装箱值类型没有SyncBlockIndex,所以不能利用System.Threading.Monitor类型来同步多个线程对它们的访问。又由于未装箱值类型没有方法表指针,所以也不能通过值类型的未装箱实例来调用其上继承而来的虚方法。另外,将一个未装箱的值类型实例转型为一个该类型实现的接口类型也需要对该实例进行装箱,因为接口类型总是引用类型。
在调用ToString时,p不会被装箱,因为ToString是一个继承自System.ValueType的方法,正常情况下,调用一个从基类继承而来的方法,我们需要一个指向其类型方法表的指针,而p是一个未装箱的值类型,它没有指向Point方法表的指针,但是,编译器会发现Point重写了ToString方法,它将产生直接调用ToString的指令,因为Point是一个值类型,而值类型不会被用作任何其他类型的基类型。所以编译器就知道这里不会出现多态行为。
在调用GetType()时,p必须被装箱。因为Point类型没有提供GetType方法的实现,而是直接继承了System.ValueType的GetType方法,所以在调用GetType()时,我们必须有一个指向Point方法表的指针,而这就只能够通过对p进行装箱来获得。
在第1次调用Clone时,p不必被装箱,因为Point实现了Clone方法,编译器可以直接调用它。Clone返回时的是一个Object,它是一个指向托管堆上已装箱的Point对象的引用。要将其内的字段拷贝到未装箱的值类型p2中,就必须执行拆箱操作。
而要把p2转型为ICloneable接口类型时,p2必须要被装箱,因为接口是一种引用类型,而装箱后的指针被存放在c中。
在第2次调用Clone方法时,不会出现装箱操作,Clone方法将直接在托管堆中已装箱的对象上被调用。Clone在托管堆上创建一个新对象,并且返回指向这个对象的引用,该引用被保存在变量o中。
在o转型为Point时,o所引用的对象会执行拆箱操作,其中的字段会被从托管堆拷贝到变量p中,p是一个驻留在堆栈上的Point值类型实例。