Effective C# Item17: Minimize Boxing and Unboxing
在.Net中有值和引用兩種類型,它們是不一致的。.Net Framework使用裝箱和拆箱來做為二者之間的橋樑。通過裝箱可以將一個實值型別封裝到一個無類型的引用型對象中。拆箱則是將參考型別從封裝中釋放出來。當我們需要將實值型別當作參考型別一樣來處理時,裝箱和拆箱是必要的。但是這樣的操作會浪費額外的資源,一般來說裝箱和拆箱時也會建立臨時的對象拷貝,這可能會為我們的程式造成難以查覺的隱患。我們應當盡量減少裝箱和拆箱操作。
裝箱可以將一個實值型別轉變為參考型別,是分配在堆上的。在這個box中包含了被裝箱的實值型別的拷貝和其實現的一些介面。當我們需要再將其複原的時候,就建立一個對應的實值型別,並賦以箱中的拷貝。
裝箱和拆箱是自動發生的,這正是它們危險的地方。當我們將一個實值型別當作參考型別來使用的時候,編譯器就會自動進行這些轉換。另外,當我們通過介面操作實值型別的時候也會引發裝箱和拆箱。它發生時沒有任何警告。下面這個例子中就發生了裝箱:
Console.WriteLine("A few numbers: {0}, {1}, {2}", 25, 32, 50);
重載的Console.WriteLine的參數是一組引用,對於int類型來說,必須首先進行裝箱才能被WriteLine方法使用。另外WriteLine會調用裝箱對象的ToString()方法。這樣我們的代碼其實是比較類似於這樣:
首先是裝箱:
int i = 25;
object o = i;
Console.WriteLine(o.ToString());
在拆箱時:
object o;
int i = (int)o;
string output = i.ToString();
我們不應當讓編譯器自動做這樣的轉換。編譯器的本意是好的,它自動產生了必要的裝箱和拆箱代碼來協助我們通過編譯。為了避免發生這種情況,我們可以在WriteLine之前先將我們的類型轉變為string:
Console.WriteLine("A few numbers: {0}, {1}, {2}", 25.ToString(), 32.ToString(), 50.ToString());
這樣做就可以避免轉換的發生。這樣一個簡單的例子告訴我們應當時刻留意這種轉換。如果可以的話,我們應當盡量避免自動轉換的發生。
另一個我們可能會遇到的將實值型別自動轉換為參考型別的地方是.Net 1.x中的集合。在.Net 1.x中集合儲存的是System.Object執行個體。當我們向其中添加實值型別的時候,boixng就會發生。當我們使用這些儲存的執行個體時,返回的是被裝箱對象中的實值型別的拷貝,而非是實值型別本身。這容易造成一些潛在的bug:
public struct Person
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public override string ToString()
{
return name;
}
}
//嘗試修改
ArrayList list = new ArrayList();
Person p = new Person();
p.Name = "a";
list.Add(p);
Person p2 = (Person)list[0];
p2.Name = "b";
Console.WriteLine(list[0].ToString());
修改是不會成功的。Person是一個實值型別,在儲存在ArrayList之前發生了裝箱操作。當我們後來在使用它時,返回的是一個拷貝。我們不能通過修改它來達到修改集合中對象的目的。
從這一點也可以看出我們建立實值型別時應當將其聲明為不可變的。如果我們確實需要可變的實值型別,那麼我們可以使用System.Array來達到目的。
如果數組不是一個合適的集合,那麼我們可以使用介面的方法達到目的。通過介面,我們可以修改box中的實值型別的值:
public interface IPersonName
{
string Name
{
get;
set;
}
}
public struct Person:IPersonName
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public override string ToString()
{
return name;
}
}
通過介面我們訪問到了原始的對象,而不是原對象的拷貝。這樣我們就可以修改集合中的實值型別的值。
ArrayList list = new ArrayList();
Person p = new Person();
p.Name = "a";
list.Add(p);
((IPersonName)list[0]).Name = "b";
Console.WriteLine(list[0].ToString());
在C# 2.0中上面的許多限制都得到了改進,但是儘管如果次我們還是要盡量避免的。頻繁的裝箱和拆箱不僅浪費系統資源,降低程式的執行效率,還可能造成意料之外的bug。在可能的情況下盡量不要使用。
譯自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
PS:在MSDN上有一篇關於裝箱和拆箱不錯的文章,介紹了使用裝箱類來達到減少這種轉換的目的
http://www.microsoft.com/china/msdn/Archives/voices/csharp03152001.asp
回到目錄