在本系列的第一篇文章《C#堆棧對比(Part Two)》中,介紹了值類型和引用類型在參數(shù)傳遞時(shí)的不同,本文將討論如何應(yīng)用ICloneable接口實(shí)現(xiàn)去修復(fù)引在堆上的用變量所帶來的問題。
本文是系列文章的第三部分。
注:限于本人英文理解能力,以及技術(shù)經(jīng)驗(yàn),文中如有錯誤之處,還請各位不吝指出。
C#堆棧對比(Part One)
C#堆棧對比(Part Two)
C#堆棧對比(Part Three)
C#堆棧對比(Part Four)
為了更清楚的表達(dá)這個(gè)問題,我們來考察一下堆上的值類型與堆上的引用類型。首先,我們來看看值類型。跟隨如下的類和結(jié)構(gòu)體,我們有一個(gè)包含Name和兩個(gè)Shoe字段的Dude類。我們有一個(gè)CopyDude方法方便我們產(chǎn)生一個(gè)新的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類是一個(gè)引用類型(原本中此處為變量類型,作者已更正)并且Shoe結(jié)構(gòu)體是類的一個(gè)成員,他們都在堆上。
注:這里體現(xiàn)了值類型是在棧上還是在堆上,完全取決于其聲明時(shí)的地點(diǎn)。

當(dāng)我們運(yùn)行如下的方法時(shí):
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()); }我們得到的結(jié)果如下:
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改為引用類型呢?那將就是個(gè)問題,更改如下:
public class Shoe{ public string Color;}更改之后再次運(yùn)行代碼,得到的結(jié)果如下:
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
紅色的鞋子在另一個(gè)人身上,這明顯是錯的,你能看出這是怎么發(fā)生的嗎?下圖就是內(nèi)存引用示例:

因?yàn)楝F(xiàn)在我們用Shoe的引用類型代替值類型,并且拷貝引用類型內(nèi)容時(shí)僅僅是拷貝了指針(不是指針真正指向的對象),我們必須做一些額外工作使我們的引用類型的Shoe更符合值類型的行為。
注:上面這個(gè)例子中當(dāng)Shoe為值類型時(shí),已經(jīng)伴隨Dude的構(gòu)造方法生成了一個(gè)完全獨(dú)立的結(jié)構(gòu)體Shoe對象,所以Bill為藍(lán)色的鞋,Ted為紅色的鞋;當(dāng)Shoe為引用類型時(shí),Shoe僅僅初始化了一次,所以Ted在使用Shoe時(shí),其實(shí)更改的還是唯一初始化一次時(shí)的Shoe的內(nèi)容,所以導(dǎo)致了最后大家都為紅鞋。下文會應(yīng)用深拷貝解決引用類型復(fù)制指針的問題。
幸運(yùn)的是,我們有一個(gè)ICloneable接口來幫我們解決問題。這個(gè)接口是一個(gè)基本的契約,所有Dudes遵守這個(gè)契約并且規(guī)定如何按順序的復(fù)制避免Shoe Sharing問題。我們所有將被復(fù)制的類應(yīng)該使用ICloneable接口,包括Shoe類。
ICloneable包括一個(gè)方法:Clone()
下面我們將實(shí)現(xiàn)這個(gè)接口: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內(nèi)部,我們僅僅是New了一個(gè)新的Shoe對象,復(fù)制所有引用類型并且拷貝值類型,然后返回一個(gè)新對象。你可能注意到了String類已經(jīng)實(shí)現(xiàn)了ICloneable接口,所以我們能調(diào)用Color.Clone方法。因?yàn)镃lone返回一個(gè)對象的引用,我們必須在設(shè)置Shoe的顏色之前將類型顯示轉(zhuǎn)換成Shoe類型。
注:String類型是一種特殊的引用類型,其表現(xiàn)形式類似于值類型,因?yàn)樽址豢筛淖儯绻淖儎t產(chǎn)生一個(gè)新對象,請參考這里。
下一步,在我們的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;}現(xiàn)在我們運(yùn)行主方法:
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()); }我們得到如下結(jié)果:
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
這就是我們想要的。

作為一個(gè)練習(xí),我們希望總是克隆引用類型和復(fù)制值類型。(這將降低當(dāng)你調(diào)試程序錯誤時(shí)所購買治療頭疼的阿司匹林的數(shù)量)
所以,在頭疼降低的情況下,讓我們走的更遠(yuǎn)一些并且讓我們整理下Dude類實(shí)現(xiàn)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方法中的內(nèi)容。
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()); }最終的結(jié)果是:
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.
所以一切都很正常。
有一個(gè)很有意思的地方需要注意,System.String的操作符“=”真是的克隆了字符串,所以你不必?fù)?dān)心重復(fù)的引用。然而你必須注意內(nèi)存膨脹。如果你回頭看看圖示,字符串是引用類型,它真的本應(yīng)該是一個(gè)指向堆的指針,但是簡單起見,它的作用類似于值類型。
作為一個(gè)練習(xí),如果我們打算每次都拷貝對象,我們應(yīng)該實(shí)現(xiàn)ICloneable接口。這將確保我們的引用類型有點(diǎn)像模仿值類型的行為。正如你所見到的那樣,記錄我們正在處理的變量是重要的,因?yàn)橐妙愋秃椭殿愋驮趧?chuàng)建內(nèi)存上的區(qū)別。
在這下一篇文章中,我們將審視一種降低內(nèi)存印記的方式。
1. 引用類型的拷貝一定要注意是深拷貝,還是簡單的指針復(fù)制的淺拷貝。
2. System.String類型是特殊的引用類型,實(shí)際作用效果類似于值類型。
3. 引用類型應(yīng)該實(shí)現(xiàn)ICloneable接口,實(shí)現(xiàn)深拷貝,即對象拷貝而非指針拷貝。
新聞熱點(diǎn)
疑難解答
圖片精選