裝箱和拆箱存在的意義:值類型是數據的容器,它存儲在堆棧上,不具備多態性,而.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,我們來查看下面的代碼。
public struct Person
{
private string _Name;
public string Name
{
get
{
return _Name;
}
set
{
_Name = value;
}
}
public override string ToString( )
{
Return _Name;
}
}
// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p );
// Try to change the name:
// Would work if Person was a reference type.
Person p2 = (( Person )attendees[0 ] );
p2.Name = "New Name";
// Writes "Old Name":
Console.WriteLine(attendees[ 0 ].ToString( ));
上述代碼中,Person是一個值類型,在將其放入ArrayList時,會進行裝箱操作,這時會有一次複製操作,當我們需要獲得ArrayList內Person對象的信息時,需要一次拆箱,又會有一次複製操作,因此,當我們並沒有對ArrayList內的對象進行修改,而是針對副本進行修改。
我們可以通過以下的方式來修改上述代碼存在的問題。
代碼
public interface IPersonName
{
string Name
{
get; set;
}
}
struct Person : IPersonName
{
private string _Name;
public string Name
{
get
{
return _Name;
}
set
{
_Name = value;
}
}
public override string ToString( )
{
return _Name;
}
}
// Using the Person in a collection:
ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p ); // box
// Try to change the name:
// Use the interface, not the type.
// No Unbox needed
(( IPersonName )attendees[ 0 ] ).Name = "New Name";
// Writes "New Name":
Console.WriteLine(attendees[ 0 ].ToString( )); // unbox
裝箱後的引用類型實現了原來值類型對象上所有的接口,這意味着不會再發生複製,但是當我們調用IPersonName.Name屬性時,它會將調用請求轉發給“箱子”內部的值類型,在值類型上實現接口使我們可以訪問”箱子“的內部,從而允許直接改變ArrayList中的信息。
總之,我們應該對任何將值類型轉換爲System.Object或者接口類型的構造保持密切的關注,例如將值類型放入集合中,在值類型上調用System.Object定義的方法等,這些操作都會將值類型轉換爲System.Object,只要有可能,我們都應該避免這種轉換。