引用链接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iii/
前言
在使用.NET框架的时候,我们可以不必主动担心内存管理和垃圾回收(GC),但是仍然要牢记内存管理和垃圾回收的机制,以优化应用程序的性能。另外,对内存管理的工作原理有一个基本的了解将有助于我们理解所编写程序中使用变量的行为。在这个文章中,我们将讨论堆中具有引用变量以及如何使用ICloneable修复引用变量引起的问题。
副本不是副本
为了清楚定义这个问题,我们来检查一下当堆上有一个值类型而不是堆上有一个引用类型的时候,会发生什么。首先,我们来定义一个值类型,采取一下的类和结构。我们有一个Dude类,其中包含Name元素和两个Shoe结构。我们有一个CopyDude()方法,可以轻松的制作一个新的Dudes。
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(){ 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;
}
并且在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.
这显然是一个错误,但是你知道为什么会出现这样的错误吗?这就是我们最终在堆中得到的结果
因为我们使用的Shoe作为引用类型而不是值类型,并且在复制引用类型的内容时仅仅复制了指针(而不是实际对象),所以我们必须做一些额外的工作来制作Shoe引用类型的行为像值类型。
刚好我们有一个可以帮我们实现这个方案的接口:ICloneable。这个接口基本是是所有Dudes都会用到的方法,并且定义了如何复制引用类型,以避免我们的“鞋子共享”错误。我们所有需要“clone”的引用类型都应该使用ICloneable接口,包括Shoe类。
ICloneable包含一种方法:Clone()
public object Clone(){
//
}
这是我们在Shoe类中实现它的方式:
public class Shoe:ICloneable{
public string Color;
public object Clone(){
Shoe newShoe = new Shoe();
newShoe.Color = Color.Clone() as string;
return newShoe;
}
}
在clone()方法内部,我们仅制作了一个新的Shoe,克隆所有引用类型并复制所有值类型,然后返回新对象。string类已经实现了ICloneable,因此我们可以调用Color.Clone()方法,由于Clone()返回对象的引用,因此在设置鞋子颜色之前,我们必须“重新赋值”引用。
接下来我们的CooyDude()方法中,我们需要克隆鞋子而不是复制它们:
public Dude CopyDude(){
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
newPerson.RightShoe = RightShoe.Clone() as Shoe;
return newPerson;
}
现在,当我们运行Main时。
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。使用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.");}
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;
}
}
然后我们在将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,因此不必担心重复引用的问题,因此你必须主要到我们内存的膨胀。实际上由于字符串是引用类型,所以它实际上在图中应该指向堆中的另一个对象的指针,但是为了方便理解,我们把它显示成值类型
结论
通常我们计划复制对象,则应该实现(并使用)ICloneable这个接口,这使我们的引用类型可以在某种程度上模仿值类型的复制行为。由于值类型和引用类型分配内存的方式有所不同,因此跟踪我们要处理的变量类型非常重要。