之前看到一道关于值类型在装箱和拆箱上的题目,觉得很有意思,所以就拿出来分享一下
大家可以先用几分钟的时间在心里做个答案再往下看 这样或许帮助更大
class Program { static void Main(string[] args) { Point p = new Point(1,1); Console.WriteLine(p); p.Change(2,2); Console.WriteLine(p); object o = p; Console.WriteLine(o); ((Point)o).Change(3,3); Console.WriteLine(o); ((IChangeBoxedPoint)p).Change(4,4); Console.WriteLine(p); ((IChangeBoxedPoint)o).Change(5,5); Console.WriteLine(o); Console.ReadKey(); } } struct Point:IChangeBoxedPoint { private int x, y; public Point(int x,int y) { this.x = x; this.y = y; } public void Change(int x, int y) { this.x = x; this.y = y; } public override string ToString() { return string.Format("{0},{1}",x,y); } } interface IChangeBoxedPoint { void Change(int x,int y); }
下面是运行结果
logs_code_hide('e210018a-8af6-483d-905f-9f497b9328f2',event)" src="/Upload/Images/2013081819/2B1B950FA3DF188F.gif" alt="" />1,1 2,2 2,2 2,2 2,2 5,5结果
看到结果后,是不是和你所预期的一样呢?下面我们来详细解释一下:
代码本身不是很复杂,其中包含一个值类型Point,一个接口类型IChangeBoxedPoint以及一个Main函数,我们就从Main函数开始从堆栈开始分析每行代码:Main在栈上创建Point值类型的一个实例(p),并将它的x和y字段设为1。然后,在第一次调用WriteLine之前,p要进行装箱。WriteLine会在已装箱的Point上调用ToString,并像预期那样显示(1,1)。然后P用于调用Change方法,该方法将p在栈上的x,y字段的值都改成2。第二次调用Writeline时,要求再次对p进行装箱,并像预期的那样显示(2,2)。
现在,p要进行第三次装箱,o将引用已装箱的Point对象。第三次调用WriteLine会再次显示(2,2),这同样是我们预期的。最后,我们希望调用Change方法来更新已装箱的Point对象中的字段。然而,object(变量o的类型)对Change方法一无所知,所以首先必须将o转型为一个Point。将o转型为Point要求对o进行拆箱,并将已装箱Point中的字段复制到线程栈上的一个临时Point中!这个临时的x,y字段会变成3和3,但已装箱的Point不受这个Change调用的影响。第四次调用WriteLine方法,会再次显示(2,2)。很多朋友或许没有考虑到。
之前我们尝试修改已装箱的p但没有成功,因为C#不允许这样操作,但可以通过接口来欺骗C#,因此在最后又加了2个例子。在第一个例子中,未装箱的Point p转型为一个IChangeBoxedPoint。这个转型造成p中的值进行装箱。然后,我们在已装箱的值上调用Change,这确实会将其x,y字段分别变成4和4.但是,在Change返回之后,已装箱的对象立即准备好进行垃圾回收(因为没有被任何对象引用,所以已经无法访问了)。所以,对WriteLine的第五个调用会显示(2,2),很多朋友可能没有预期到这个结果。
在最后一个例子中,o所引用的已装箱Point要转型为一个IChangeBoxedPoint。这里不需要进行装箱,因为o当前已经是一个装箱的Point。然后调用Change,它能正确修改已装箱的Point的x,y字段。接口方法Change允许我更改一个已装箱Point对象中的字段!现在,当调用Writeline时,它会像预期那样显示(5,5)。
本文的宗旨在于解释值类型在装箱和拆箱时内存中的运行原理,具体什么时候用值类型和如何正确使用值类型以及使用值类型的优点不是本文讨论的重点。第一次写博客,觉得好的就把知识消化了,觉得不好的轻喷。