装箱(boxing)机制是一个值得单独拿出来讨论的话题,因为忽略它,我们会在不知不觉间犯下很大的错误。
先说说装箱的过程:会先在堆中分配好内存,该内存大小为值类型所有字段和添加的类型对象指针以及同步块索引所需的字节,然后将值类型字段复制到这块新分配的内存中,接着返回对象的地址值,即该对象的引用。
拆箱并不是装箱的逆操作:拆箱只是获取一个引用,该引用指向值类型的字段,它并不要求复制字段,复制字段实际上拆箱之后的动作,但这个动作是一定会发生。
装箱设计到字段的复制,所以需要特别小心。但C#中有一个隐式装箱机制,使得我们很多时候防不胜防。所以,最好的做法就是显式的进行转换,而不是交给编译器。
如果大家还不信,看下面的例子:
for(int i = 0; i < 1000...000; i++){ int number = 5; Object obj = number; }
如果每次循环都要进行装箱,更糟糕的是该值类型是一个大值,那么,我们的程序就会在这里卡死! 也许这样的问题很简单,很多人都不会在一个循环中创建一个对象,但隐式装箱机制会让我们在不经意的情况下就陷入装箱的恶梦:
int number = 5; Object obj = number; number = 10; Console.WriteLine(number + "," + (int)obj);
这样的代码到底一共发生多少次装箱呢?答案是三次。Console.WriteLine()输出的是一个String,它里面会用String的静态方法Concat(),该方法的参数是Object,需要number和被强制转型为int的obj都需要装箱。 好吧,我们在修改一下:
int number = 5; Object = number; number = 10; Console.WriteLine(number + "," + obj);
现在是两次。但我只想要一次,该怎么办?也很简单:
int number = 5; Object obj = number; number = 10; Console.WriteLine(number.ToString() + "," + obj);
调用number的ToString()会返回一个String,但是number本身不会被装箱。 只是小小的修改,但这段代码的速度已经提升了很多(好吧,这样短小的代码根本看不出来)。
频繁的装箱不仅影响程序的运行速度,对内存消耗方面的压力也是非常大的。这个问题在CLR那里得到高度重视,以至于有许多方法重载都是为了消除了装箱带来的影响,像是Console.WriteLine()就有对应各个值类型的重载版本。
前面的例子还不能说明隐式装箱的隐患,因为它最可怕的是我们对此竟然毫无察觉:
int number = 5; Console.WriteLine("{0}, {1}, {2}", number, number, number);
我们很容易写下这样的代码,然而这里竟然发生了三次装箱! 如果是显示的装箱的话,就能一定程度上消除这样的影响:
int number = 5; Object obj = number; Console.WriteLine("{0}, {1}, {2}", obj, obj, obj);
这样就只有一次了。 关于装箱还有一个疑问:对于已装箱值类型,原先值类型的改变会不会对其造成影响?答案是不会,因为它们的存储方式完全不同。
但如果我们想要值类型一修改,装箱中对应的值类型也要跟着修改呢?很好的问题,实现起来也是很复杂的,甚至是别扭的:
public interface IChangeAble { void SetNumber(int number); } struct Pratice : IChangeAble { private int _Number; public Pratice(int number) { _Number = number; } public void SetNumber(int number) { this._Number = number; } public override String ToString() { return "" + _Number; } } class Program { public static void Main(String[] args) { Pratice pratice = new Pratice(5); Object obj = pratice; IChangeAble changeAblePratice = (IChangeAble)pratice; changeAblePratice.SetNumber(6); Console.WriteLine("第一次的值为:" + patice + "," + obj); IChangeAble changeAbleObject = (IChangeAble)obj; changeAbleObject.SetNumber(6); Console.WriteLine("第二次的值为:" + pratice + "," + obj); } }
通常我们需要更改的是结构这种值类型中的字段,所以我们需要让这个结构实现一个接口(值类型的确可以实现接口类型,但没说过所有的值类型都可以啊,像是int这种,那就匪夷所思了,至少我没见过,也不想见到这样的东西)。然后我们见到,我们第一次想要将pratice强制转换为IChangeAble,但是发现,值根本没有变!原因很简单,我们的确是在已装箱的pratice上进行修改,值也的确是改变了,但是,在SetNumber()方法返回后,该已装箱的对象竟然会被回收!所以仍然是之前未被装箱的pratice。后面的情况则是因为obj已经是一个引用类型,转换为IChangeAble不需要装箱,而IChangeAble允许我们对一个已装箱的pratice的字段进行修改。 要理解好这样的过程是很费劲的,因为很别扭!但这也是C#中唯一有可能对已装箱的值类型进行修改的方法。但别灰心,只要把struct改为class,之前所有的问题都不见了!
修改值类型的字段,尤其是已经装箱的值类型,是一种很不安全的行为,所以,我们可以看到,修改的方法十分别扭,而且也只有结构这种类型才能修改,像是其他值类型已经默认是不可变的,这样就不会给我们带来那么多的麻烦。
装箱机制的内容就到这里吧,再研究下去我可能就真的没有时间做事了,等到以后有新的资料或想法时再补充吧