之前在网上看过许多关于拆箱和装箱的说法,看多了更糊涂了。今天看了《CLR VIA C#》第三版,突然感觉豁然开朗。
这篇博客之所以起名为新解,只是我看到的关于装箱拆箱操作的嘴让人透彻明白的解释。
废话就不说了,我们还是来看看吧!先来看例子:
下面是一段代码的三中不同写法,还有他们的反编译后的il,请问每种方法中装箱的次数???
-----------------------------------程序一------------------------------------ static void Main(string[] args) { Int32 v = 5; object o = v; v = 123; Console.WriteLine(v.ToString()+","+(int32)o); Console.ReadKey(); } .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 53 (0x35) .maxstack 3 .locals init ([0] int32 v, [1] object o) IL_0000: nop IL_0001: ldc.i4.5 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: box [mscorlib]System.Int32 IL_0009: stloc.1 IL_000a: ldc.i4.s 123 IL_000c: stloc.0 IL_000d: ldloc.0 IL_000e: box [mscorlib]System.Int32 IL_0013: ldstr "," IL_0018: ldloc.1 IL_0019: unbox.any [mscorlib]System.Int32 IL_001e: box [mscorlib]System.Int32 IL_0023: call string [mscorlib]System.String::Concat(object, object, object) IL_0028: call void [mscorlib]System.Console::WriteLine(string) IL_002d: nop IL_002e: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0033: pop IL_0034: ret } // end of method Program::Main -----------------------------------------程序二-------------------------------- static void Main(string[] args) { Int32 v = 5; object o = v; v = 123; Console.WriteLine(v+","+o); Console.ReadKey(); } .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 43 (0x2b) .maxstack 3 .locals init ([0] int32 v, [1] object o) IL_0000: nop IL_0001: ldc.i4.5 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: box [mscorlib]System.Int32 IL_0009: stloc.1 IL_000a: ldc.i4.s 123 IL_000c: stloc.0 IL_000d: ldloc.0 IL_000e: box [mscorlib]System.Int32 IL_0013: ldstr "," IL_0018: ldloc.1 IL_0019: call string [mscorlib]System.String::Concat(object, object, object) IL_001e: call void [mscorlib]System.Console::WriteLine(string) IL_0023: nop IL_0024: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_0029: pop IL_002a: ret } // end of method Program::Main ---------------------------程序三------------------------------------------- static void Main(string[] args) { Int32 v = 5; object o = v; v = 123; Console.WriteLine(v.ToString()+","+o); Console.ReadKey(); } .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 44 (0x2c) .maxstack 3 .locals init ([0] int32 v, [1] object o) IL_0000: nop IL_0001: ldc.i4.5 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: box [mscorlib]System.Int32 IL_0009: stloc.1 IL_000a: ldc.i4.s 123 IL_000c: stloc.0 IL_000d: ldloca.s v IL_000f: call instance string [mscorlib]System.Int32::ToString() IL_0014: ldstr "," IL_0019: ldloc.1 IL_001a: call string [mscorlib]System.String::Concat(object, object, object) IL_001f: call void [mscorlib]System.Console::WriteLine(string) IL_0024: nop IL_0025: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey() IL_002a: pop IL_002b: ret } // end of method Program::Main
程序1中:3次 程序2:2次 程序3:1次
why??当然我们可以根据il代码中box出现的次数就可以判断出为什么是3,2,1,但是当我们没有il代码,或者在决断我们改怎样编写代码避免不比较的装箱拆箱操作呢??
《CLR Via C#》给了我们很详尽的解答:
我们先来认识一下装箱和拆箱的背景:
一.值类型和引用类型
CLR 支持两种类型,值类型和引用类型。
1.引用类型总是从托管堆上非配,C#的new关键词或放回对象的内存地址--也就是指向对象数据的内存地址;
2.引用类型对象是存放在栈上,而对象的引用实在堆上。
3.值类型一般就是在堆栈上非配的
4.值类型的复制是在堆栈中再开辟一个空间,将字节一个一个的复制过来,前后两个变量没有任何的联系
5.引用的类型复制,是在堆栈中声明一个对象,有别于待复制的对象,但其指向的引用都是堆中相同的一块地址。所以该变一个对象,另一个对象也会改变(如下图)
对应程序:
static void Main(string[] args) {
pointStrcut a1=new pointStrcut (); a1.x = "a1"; pointStrcut a2=a1; a2.x = "a2"; Console.WriteLine("a1 is " + a1.x + " a2 is "+a2.x); pointClass o1= new pointClass(); o1.x ="nihao"; pointClass o2= o1; o2.x = "buhao"; Console.WriteLine("a1.x is "+o1.x+" a2.x is "+o2.x)
}
internal class pointClass { public string x; public string y; } internal struct pointStrcut { public string x; public string y; }
结果:
strcutBox1 is strcutBox1 strcutBox2 is st
pclass1.x is buhao pclass2.x is buhao
在上例子中,class pointClass是引用类型,而struct PointStruct 是值类型
对已值类型,只在栈中开辟内存,复制是是整个copy。所以a1和a2 是完全没有联系的只是值相同的两个变量。a2的变化不会导致a1的变化
对引用类型,栈中保存对象,堆中保存对象的引用。copy是,栈中的对象不一样,但是所对应的引用地址是一样的,所以,o2的改变实际上市堆中内存块发生了改变,所以o1也随着改变。
那么值类型和引用类型有哪些?
值类型:直接派生于System.Valuetype 包括结构,枚举。枚举继承自system.enum,但是system.enum也继承自system.valueType 。
引用类型:类
一.何为装箱,拆箱
简单点装箱就是将值类型转化为引用类型,拆箱反之;
我们再看一段代码
class Program { static void Main(string[] args) { Point p = new Point(1, 1); Console.WriteLine(p); p.Change(2, 2); Console.WriteLine(p); //1,1 object o = p; Console.WriteLine(o); //2,2 ((Point)o).Change(3, 3); Console.WriteLine(o); //只改变了栈中的临时Point值(下式中显示写出临时变量o2) 2,2 Point o2 = (Point)o; o2.Change(3, 3); Console.WriteLine(o2); //2,2 Console.ReadKey(); } } internal struct Point { private Int32 x, y; public Point(Int32 x, Int32 y) { this.x = x; this.y = y; } public void Change(Int32 x, Int32 y) { this.x = x; this.y = y; } public override string ToString() { return "(" + x + "," + y + ")"; } } internal class pointClass { public string x; public string y; }
Console.WriteLine(o);的执行结果为什么是(2,2)??
仔细研究一下这一句:((Point)o).Change(3, 3);
o是一个object类型,时刻记住引用类型:栈上面保存对象,堆上面保存对象的引用。
那上面一句话干了什么?
将一个引用类型转化为值类型(拆箱),再调用change方法。 注意拆箱过程:拆箱会在栈上新建一个临时变量,并将堆中的所有数据全部复制到这个临时变量中!
也就是说o还是o,没有变化!我们用下面的方法显示拆箱过程
Point o2 = (Point)o;
o2.Change(3, 3);
Console.WriteLine(o2); //2,2
那如果我们要改变引用类型的变量改怎么办?看下面代码:(代码中有注释,一看便知)
class Program { static void Main(string[] args) { Point p = new Point(1, 1); Console.WriteLine(p); p.Change(2, 2); Console.WriteLine(p); //1,1 object o = p; Console.WriteLine(o); //2,2 ((Point)o).Change(3, 3); Console.WriteLine(o); //只改变了栈中的临时Point值(下式中显示写出临时变量o2) 2,2 Point o2 = (Point)o; o2.Change(3, 3); Console.WriteLine(o2); //2,2 ((IChangeBoxedPoint)p).Change(4, 4); //同样 p先装箱生成临时变量,在堆中改变值, Console.WriteLine(p); //依旧显示的是栈中的值 2,2 // IChangeBoxedPoint ipoint = (IChangeBoxedPoint)p; // ipoint.Change(4,4); // Console.WriteLine(ipoint); ((IChangeBoxedPoint)o).Change(5, 5); //o已经是引用类型,无需装箱 Console.WriteLine(o); //显示的是堆中的数据 5,5 Console.ReadKey(); } } internal interface IChangeBoxedPoint { void Change(Int32 x,Int32 y); } internal struct Point : IChangeBoxedPoint { private Int32 x, y; public Point(Int32 x, Int32 y) { this.x = x; this.y = y; } public void Change(Int32 x, Int32 y) { this.x = x; this.y = y; } public override string ToString() { return "(" + x + "," + y + ")"; } } internal class pointClass { public string x; public string y; } }
总结:在装箱和拆箱的过程中,无时无刻记住
1.装箱是值类型转化为引用类型
2.转化都有新的变量生成,装箱生成在栈中的对象和堆中的引用,拆箱生成栈中的数据变量