裝箱(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,之前所有的問題都不見了!
修改實值型別的欄位,尤其是已經裝箱的實值型別,是一種很不安全的行為,所以,我們可以看到,修改的方法十分彆扭,而且也只有結構這種類型才能修改,像是其他實值型別已經預設是不可變的,這樣就不會給我們帶來那麼多的麻煩。
裝箱機制的內容就到這裡吧,再研究下去我可能就真的沒有時間做事了,等到以後有新的資料或想法時再補充吧