原文鏈接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iii/
導言
雖然在.NET Framework下編程,我們不需要主動的關注內存管理和垃圾回收(GC),但是爲了更優的程序性能,我們還是應該關注內存管理和GC。當然從根本上理解了內存管理是如何工作的也會幫助我們瞭解程序中的每一個變量的工作方式。在本節中,我們將討論由於在堆上的引用變量所引發的問題,並且使用ICloneable來修復它。
拷貝不是拷貝
爲了把問題說的更清晰,我們來分析一下,有一個值類型被分配到堆上,與之相對的還有一個引用類型也在堆上,這會發生什麼呢。首先來看值類型。看一下下面栗子中的類和結構。我們擁有一個Dude類,包含一個Name字段,和兩個Shoe類型的字段。我們擁有一個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是Dude類的成員變量,所以他們都在堆上分配內存。
當我們在執行下面的方法時:
public static void Main()
{
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改成引用類型的會發生什麼呢?這就是問題的根本所在了,讓我們把Shoe編程引用類型:
public class Shoe{
public string Color;
}
Main()中的代碼完全一致,輸出結果變了:
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
Red shoe也穿到了Bill的腳上。現在錯誤就很清除了。你知道這是怎麼發生的嗎?下圖是堆上的內存分配情況:
因爲我們現在把Shoe的類型從值類型替換爲引用類型,當引用類型的內容被複制的時候,只有指針的內容被複制了,而不是那個被指向的對象,我們必須再做點什麼以使得我們引用類型的Shoe看起來更像是值類型的。
幸運的是有一個接口可以幫助我們:ICloneable。這個接口主要規定了應用類型的變量是如何被複制的,所有的Dude都遵守這個規則的時候,就避免了鞋子被分享(shoe sharing)的錯誤。所有需要被拷貝的對象都繼承接口ICloneable,包括Shoe類。
ICloneable只包含一個方法:Clone()
//--譯者注:這裏和原文的代碼不一樣
public interface ICloneable
{
object Clone();
}
接下來看怎樣修改Shoe類:
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()裏面,我們只是新生成了一個Shoe對象,複製了所有的引用類型和值類型的變量,並返回新的對象。可能你會注意到string類早就實現了ICloneable接口,所以我們可以直接調用Color.Clone()。因爲Clone()方法返回的是一個object類型的引用,所以在我們將它的值賦值給shoe時,必須對類型進行轉換。
然後,在方法CopyDude()中我們通過拷貝來克隆shoe的實例。
public Dude CopyDude()
{
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
newPerson.RightShoe = RightShoe.Clone() as Shoe;
return newPerson;
}
現在,再來運行Mian()方法:
public static void Main()
{
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接口,並且用Clone()方法替換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
}
並且要修改Main()方法以使用Dude.Clone()方法。
public static void Main()
{
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類實際上是複製了它的string,所以你不必擔心它會複製一個引用。然鵝,你也必須要注意到,這使得我們的內存佔用變多了。如果你回過頭去看那些圖示,因爲string是引用類型,它確實應該是一個指向堆內存中的另一個對象的指針,但是在簡易的棧內存裏,它被當成了值類型。
總結
在一般情況下,如果打算通過拷貝創建對象,我們就需要實現和使用ICloneable接口。這使得我們的引用類型在一定程度上模仿了值類型。正如你所看到的,這對於我們追蹤需要處理的變量的類型是非常重要的,因爲值類型和引用類型在分配內存的時候是不一樣的。
在下一篇中,我們將尋找一條優化內存佔用的方式。
然後,祝你開心。