C#堆VS棧(Part Three)
在本系列的第一篇文章《C#堆棧對比(Part Two)》中,介紹了值類型和引用類型在參數傳遞時的不同,本文將討論如何應用ICloneable接口實現去修復引在堆上的用變量所帶來的問題。
本文是系列文章的第三部分。
注:限於本人英文理解能力,以及技術經驗,文中如有錯誤之處,還請各位不吝指出。
拷貝不是復制那麼簡單
為了更清楚的表達這個問題,我們來考察一下堆上的值類型與堆上的引用類型。首先,我們來看看值類型。跟隨如下的類和結構體,我們有一個包含Name和兩個Shoe字段的Dude類。我們有一個CopyDude方法方便我們產生一個新的Dude(花花公子)。
public struct Shoe
{
public string Color;
}
public class Dude
{
public string Name;
public Shoe RightShoe;
public Shoe LeftShoe;
public Dude CopyDude()
{
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe;
newPerson.RightShoe = RightShoe;
return newPerson;
}
public override string ToString()
{
return (Name + " : Dude!, I have a " + RightShoe.Color +
" shoe on my right foot, and a " +
LeftShoe.Color + " on my left foot.");
}
}
我們的Dude類是一個引用類型(原本中此處為變量類型,作者已更正)並且Shoe結構體是類的一個成員,他們都在堆上。
注:這裡體現了值類型是在棧上還是在堆上,完全取決於其生命時的地點。
當我們運行如下的方法時:
public static void Main()
{
Class1 pgm = new Class1();
Dude Bill = new Dude();
Bill.Name = "Bill";
Bill.LeftShoe = new Shoe();
Bill.RightShoe = new Shoe();
Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
Dude Ted = Bill.CopyDude();
Ted.Name = "Ted";
Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
Console.WriteLine(Bill.ToString());
Console.WriteLine(Ted.ToString());
}
我們得到的結果如下:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.
如果我們將Shoe改為引用類型呢?那將就是個問題,更改如下:
public class Shoe
{
public string Color;
}
更改之後再次運行代碼,得到的結果如下:
Bill : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
紅色的鞋子在另一個人身上,這明顯是錯的,你能看出這是怎麼發生的嗎?下圖就是內存引用示例:
因為現在我們用Shoe的引用類型代替值類型,並且拷貝引用類型內容時僅僅是拷貝了指針(不是指針真正指向的對象),我們必須做一些額外工作使我們的引用類型的Shoe更符合值類型的行為。
注:上面這個例子中當Shoe為值類型時,已經伴隨Dude的構造方法生成了一個完全獨立的結構體Shoe對象,所以Bill為藍色的鞋,Ted為紅色的鞋;當Shoe為引用類型時,Shoe僅僅初始化了一次,所以Ted在使用Shoe時,其實更改的還是唯一初始化一次時的Shoe的內容,所以導致了最後大家都為紅鞋。下文會應用深拷貝解決引用類型復制指針的問題。
幸運的是,我們有一個ICloneable接口來幫我們解決問題。這個接口是一個基本的契約,所有Dudes遵守這個契約並且規定如何按順序的復制避免Shoe Sharing問題。我們所有將被復制的類應該使用ICloneable接口,包括Shoe類。
ICloneable包括一個方法:Clone()
下面我們將實現這個接口:
public class Shoe : ICloneable
{
public string Color;
#region ICloneable Members
public object Clone()
{
Shoe newShoe = new Shoe();
newShoe.Color = Color.Clone() as string;
return newShoe;
}
#endregion
}
在Clone內部,我們僅僅是New了一個新的Shoe對象,復制所有引用類型並且拷貝值類型,然後返回一個新對象。你可能注意到了String類已經實現了ICloneable接口,所以我們能調用Color.Clone方法。因為Clone返回一個對象的引用,我們必須在設置Shoe的顏色之前將類型顯示轉換成Shoe類型。
注:String類型是一種特殊的引用類型,其表現形式類似於值類型,因為字符串不可改變,如果改變則產生一個新對象,請參考這裡。
下一步,在我們的CopyDude方法中我們需要克隆Shoes代替拷貝。
public Dude CopyDude()
{
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
newPerson.RightShoe = RightShoe.Clone() as Shoe;
return newPerson;
}
現在我們運行主方法:
public static void Main()
{
Class1 pgm = new Class1();
Dude Bill = new Dude();
Bill.Name = "Bill";
Bill.LeftShoe = new Shoe();
Bill.RightShoe = new Shoe();
Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
Dude Ted = Bill.CopyDude();
Ted.Name = "Ted";
Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
Console.WriteLine(Bill.ToString());
Console.WriteLine(Ted.ToString());
}
我們得到如下結果:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
這就是我們想要的。
將事物包裹起來
作為一個練習,我們希望總是克隆引用類型和復制值類型。(這將降低當你調試程序錯誤時所購買治療頭疼的阿司匹林的數量)
所以,在頭疼降低的情況下,讓我們走的更遠一些並且讓我們整理下Dude類實現ICloneable接口方法代替CopyDude方法。
public class Dude: ICloneable
{
public string Name;
public Shoe RightShoe;
public Shoe LeftShoe;
public override string ToString()
{
return (Name + " : Dude!, I have a " + RightShoe.Color +
" shoe on my right foot, and a " +
LeftShoe.Color + " on my left foot.");
}
#region ICloneable Members
public object Clone()
{
Dude newPerson = new Dude();
newPerson.Name = Name.Clone() as string;
newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
newPerson.RightShoe = RightShoe.Clone() as Shoe;
return newPerson;
}
#endregion
}
我們所要做的僅僅是通過使用Dude.Clone改變Main方法中的內容。
public static void Main()
{
Class1 pgm = new Class1();
Dude Bill = new Dude();
Bill.Name = "Bill";
Bill.LeftShoe = new Shoe();
Bill.RightShoe = new Shoe();
Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
Dude Ted = Bill.Clone() as Dude;
Ted.Name = "Ted";
Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
Console.WriteLine(Bill.ToString());
Console.WriteLine(Ted.ToString());
}
最終的結果是:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.
所以一切都很正常。
有一個很有意思的地方需要注意,System.String的操作符“=”真是的克隆了字符串,所以你不必擔心重復的引用。然而你必須注意內存膨脹。如果你回頭看看圖示,字符串是引用類型,它真的本應該是一個指向堆的指針,但是簡單起見,它的作用類似於值類型。
總結
作為一個練習,如果我們打算每次都拷貝對象,我們應該實現ICloneable接口。這將確保我們的引用類型有點像模仿值類型的行為。正如你所見到的那樣,記錄我們正在處理的變量是重要的,因為引用類型和值類型在創建內存上的區別。
在這下一篇文章中,我們將審視一種降低內存印記的方式。
1. 引用類型的拷貝一定要注意是深拷貝,還是簡單的指針復制的淺拷貝。
2. System.String類型是特殊的引用類型,實際作用效果類似於值類型。
3. 引用類型應該實現ICloneable接口,實現深拷貝,即對象拷貝而非指針拷貝。