值類型是數據的容器,它們不具備多太性。另一方面就是說,.Net框架被設 計成單一繼承的引用類型,System.Object,在整個繼承關系中做為根對象存在 。設計這兩種類型的目的是截然不同的,.Net框架使用了裝箱與拆箱來鏈接兩種 不同類型的數據。裝箱是把一個值類型數據放置在一個無類型的引用對象上,從 而使一個值類型在須要時可以當成引用類型來使用。拆箱則是額外的從“ 箱”上拷貝一份值類型數據。裝箱和拆箱可以讓你在須要使用 System.Object對象的地方使用值類型數據。但裝箱與拆箱操作卻是性能的強盜 ,在些時候裝箱與拆箱會產生一些臨時對象,它會導致程序存在一些隱藏的BUG 。應該盡可能的避免使用裝箱與拆箱。
裝箱可以把一個值類型數據轉化 也一個引用類型,一個新的引用對象在堆上創建,它就是這個“箱子 ”,值類型的數據就在這個引用類型中存儲了一份拷貝。參見圖2.3,演示 了裝箱的對象是如何訪問和存儲的。箱子中包含一份這個值類型對象的拷貝,並 且復制實現了已經裝箱對象的接口。當你想從這個箱子中取回任何內容時,一個 值類型數據的拷貝會被創建並返回。這就是裝箱與拆箱的關鍵性概念:對象的一 個拷貝存放到箱子中,而不管何時你再訪問這個箱子時,另一個拷貝又會被創建 。
圖2.3,值類型數據在箱子中。把一個值類型數據轉化成一個 System.Object的引用,一個無名的引用類型會被創建。值類型的數據就存儲在 這個無名的引用對象中,所有的訪問方法都要通過這個箱子才能到達值類型數據 存儲的地方。
最陰險的地方是這個裝箱與拆箱很多時候是自動完成的!當 你在任何一個期望類型是System.Object的地方使用值類型數據時,編譯器會生 成裝箱與拆箱的語句。另外,當你通過一個接口指針來訪問值類型數據時,裝箱 與拆箱也會發生。當你裝箱時不會得到任何警告,即使是最簡單的語句也一樣。 例如下面這個:
Console.WriteLine("A few numbers:{0}, {1}, {2}",
25, 32, 50);
使用重載的 Console.WriteLine函數須要一個System.Object類型的數組引用,整型是值類型 ,所以必須裝箱後才能傳給重載的WriteLine方法。唯一可以強制這三個整數成 為System.Object對象的方法就是把它們裝箱。另外,在WriteLine內部,通過調 用箱子對象上的ToString()方法來到達箱子內部。某種意義上講,你生成了這樣 的結構:
int i =25;
object o = i; // box
Console.WriteLine(o.ToString());
在WriteLine內部,下面 的執行了下面的代碼:
object o;
int i = ( int )o; // unbox
string output = i.ToString( );
你可能自己從來 不會寫這樣的代碼,但是,卻讓編譯器自動從一個指定的類型轉化為 System.Object,這確實是你做的。編譯器只是想試著幫助你,它想讓你成功(調 用函數),它也很樂意在必要時候為你生成裝箱和拆箱語句,從而把一個值類型 數據轉化成System.Object的實例。為了避免這麼挑剔的懲罰,在使用它們來調 用WriteLine之前,你自己應該把你的類型轉化成字符串的實例。
Console.WriteLine("A few numbers:{0}, {1}, {2} ",
25.ToString(), 32.ToString(), 50.ToString ());
(譯注:注意,在自己調用ToString方法時,還是會在堆上 創建一個引用實例,但它的好處是不用拆箱,因為對象已經是一個引用類型了。 )
這段代碼使用已知的整數類型,而且值類型再也不會隱式的轉化為 System.Object類型。這個常見的例子展示了避免裝箱的第一個規則:注意隱式 的轉化為System.Object,如果可以避免,值類型不應該被System.Object代替。
另一個常見情況就是,在使用.Net 1.x的集合時,你可能無意的把一個 值類型轉化成System.Object類型。任何時候,當你添加一個值類型數據到集合 時中,你就創建了一個箱子。任何時候從集合中移出一個對象時,你得到的是箱 子裡的一個拷貝。從箱子裡取一個對象時,你總是要創建一個拷貝。這會在應用 程序中產生一些隱藏的BUG。編譯器是不會幫你查找這些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之前它被裝箱 。這會產生一個拷貝。而在移出的Persone對象上通過訪問屬性做一些修改時, 另一個拷貝被創建。而你所做的修改只是針對的拷貝,而實際上還有第三個拷貝 通過ToString()方法來訪問attendees[0]中的對象。
正因為這以及其它 一些原因,你應該創建一些恆定的值類型(參見原則7)。如果你非要在集合中使 用可變的值類型,那就使用System.Array類,它是類型安全的。
如果一 個數組不是一個合理的集合,以C#1.x中你可以通過使用接口來修正這個錯誤。 盡量選擇一些接口而不是公共的方法,來訪問箱子的內部去修改數據:
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
裝箱後的引用類型會實現原數據類型上所有已經實現的接 口。這就是說,不用做拷貝,你可以通過調用箱子上的IPersonaName.Name方法 來直接訪問請求到箱子內部的值類型數據。在值類型上創建的接口可以讓你訪問 集合裡的箱子的內部,從而直接修改它的值。在值類型上實現的接口並沒有讓值 類型成為多態的,這又會引入裝箱的懲罰(參見原則20)。
在C#2.0中對泛 型簡介中,很多限制已經做了修改(參見原則49)。泛型接口和泛型集合會時同處 理好集合與接口的困境。在那之前,我們還是要避免裝箱。是的,值類型可以轉 化為System.Object或者其它任何的接口引用。這些轉化是隱式的,使得發現它 們成為繁雜的工作。這些也就是環境和語言的規則,裝箱與拆箱操作會在你不經 意時做一些對象的拷貝,這會產生一些BUG。同樣,把值類型多樣化處理會對性 能有所損失。時刻注意那些把值類型轉化成System.Object或者接口類型的地方 :把值類型放到集合裡,調用定義參數為System.Object類型的方法,或者強制 轉化為System.Object。能夠避免就盡量避免!
返回教程目錄