Effective C#:儘量減少裝箱和拆箱

iDotNetSpace發表於2010-01-14
    裝箱和拆箱存在的意義:值型別是資料的容器,它儲存在堆疊上,不具備多型性,而.NET框架在整個物件層次的設計中,使用System.Object作為所有型別的基類,但是Obejct是引用型別,而作為值型別的基類System.ValueType,是從System.Object派生出來的,這就產生了矛盾,裝箱和拆箱就是為了解決這兩種型別之間的差異。

    裝箱會將一個值型別放入一個未具名型別(untyped)的引用物件中,從而允許該值型別應用於那些只能使用引用型別的場合。拆箱則會從前面的裝箱物件中提取出一個值型別的副本。裝箱和拆箱都是比較耗時的操作。

    裝箱操作會將值型別轉換為一個引用型別,這個過程中會建立一個新的引用獨享,然後將其分配到堆上,同時值型別的副本會被儲存在該引用物件內部。當我們需要從裝箱物件中獲取任何資訊時,會建立值型別的一個副本,然後將其返回。其中的關鍵是:當我們需要引用型別時,會建立一個新的引用型別的物件並將其放入到堆中;當我們需要訪問已經裝箱的物件資訊時,就會建立對應值型別的一個副本,並將其返回。

    裝箱和拆箱最大的問題是它們會自動發生。當我們使用的是值型別,而期望的是引用型別,那麼編譯器就會自動產生裝箱和拆箱語句。

   我們來看下面的語句,居然也發生了裝箱和拆箱操作。

Console.WriteLine("A few numbers:{0}, {1}, {2}",
25, 32, 50);

    上述程式碼之所以發生了裝箱,是因為WriteLine方法需要的引數型別是System.Object,而25是一個int型別,屬於值型別,因此需要裝箱,而在WriteLine方法內部實現時,需要呼叫方法引數的ToString()方法,為了呼叫裝箱物件的方法,就會發生拆箱的操作。

    為了避免裝箱和拆箱,可以將上述程式碼進行如下修改。

Console.WriteLine("A few numbers:{0}, {1}, {2}",
25.ToString(), 32.ToString(), 50.ToString());

    另外,由於裝箱和拆箱都會產生新的例項,那麼有時會產生一些詭異的bug,我們來檢視下面的程式碼。

程式碼

1 public struct Person
2 {
3 private string _Name;
4
5 public string Name
6 {
7 get
8 {
9 return _Name;
10 }
11 set
12 {
13 _Name = value;
14 }
15 }
16
17 public override string ToString( )
18 {
19 Return _Name;
20 }
21 }
22
23  // Using the Person in a collection:
24 ArrayList attendees = new ArrayList( );
25 Person p = new Person( "Old Name" );
26 attendees.Add( p );
27
28 // Try to change the name:
29 // Would work if Person was a reference type.
30 Person p2 = (( Person )attendees[ 0 ] );
31 p2.Name = "New Name";
32
33 // Writes "Old Name":
34 Console.WriteLine(
35 attendees[ 0 ].ToString( ));

    上述程式碼中,Person是一個值型別,在將其放入ArrayList時,會進行裝箱操作,這時會有一次複製操作,當我們需要獲得ArrayList內 Person物件的資訊時,需要一次拆箱,又會有一次複製操作,因此,當我們並沒有對ArrayList內的物件進行修改,而是針對副本進行修改。

    我們可以通過以下的方式來修改上述程式碼存在的問題。

程式碼

1 public interface IPersonName
2 {
3 string Name
4 {
5 get; set;
6 }
7 }
8
9 struct Person : IPersonName
10 {
11 private string _Name;
12
13 public string Name
14 {
15 get
16 {
17 return _Name;
18 }
19 set
20 {
21 _Name = value;
22 }
23 }
24
25 public override string ToString( )
26 {
27 return _Name;
28 }
29 }
30
31 // Using the Person in a collection:
32 ArrayList attendees = new ArrayList( );
33 Person p = new Person( "Old Name" );
34 attendees.Add( p ); // box
35
36 // Try to change the name:
37 // Use the interface, not the type.
38 // No Unbox needed
39 (( IPersonName )attendees[ 0 ] ).Name = "New Name";
40
41 // Writes "New Name":
42 Console.WriteLine(
43 attendees[ 0 ].ToString( )); // unbox
44
45

    裝箱後的引用型別實現了原來值型別物件上所有的介面,這意味著不會再發生複製,但是當我們呼叫IPersonName.Name屬性時,它會將呼叫請求轉發給“箱子”內部的值型別,在值型別上實現介面使我們可以訪問”箱子“的內部,從而允許直接改變ArrayList中的資訊。

 

   總之,我們應該對任何將值型別轉換為System.Object或者介面型別的構造保持密切的關注,例如將值型別放入集合中,在值型別上呼叫 System.Object定義的方法等,這些操作都會將值型別轉換為System.Object,只要有可能,我們都應該避免這種轉換。
    
作者:李勝攀
    
出處:http://wing011203.cnblogs.com/

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-625129/,如需轉載,請註明出處,否則將追究法律責任。

相關文章