尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。
同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们日常程序编写中的变量的行为。
在本文中我们将涉及到堆中引用变量引起的问题,以及如何使用ICloneable接口来解决该问题。
需要回顾堆栈基础,值类型和引用类型,请转到第一部分和第二部分
* 副本并不是真的副本
为了清楚的阐明问题,让我们来比较一下当堆中存在值类型和引用类型时都发生了些什么。
首先来看看值类型,如下面的类和结构。
这里有一个类Dude,它的成员中有一个string型的Name字段及两个Shoe类型的字段--RightShoe、LeftShoe,
还有一个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(){ 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换成引用类型会发生什么?问题就在于此。
假如我们将Shoe改为引用类型:(前面例子是结构型,现在改成了类)
public class Shoe{ public string Color; }然后在与前面相同的Main()方法中运行,再来看看我们的结果:
Bill : Dude!,and a Red on my left foot
Ted : Dude!,and a Red on my left foot
(说明上面只是 引用在复制,引用所指向的对象并没有复制)
可以看到红鞋子被穿到别人(Bill)脚上了,很明显出错了。你想知道这是为什么吗?我们再来看看堆就明白了。
由于我们现在使用的Shoe是引用类型而非值类型,当引用类型的内容被拷贝时实际上只拷贝了该类型的指针(并没有拷贝实际的对象),
我们需要作一些额外的工作来使我们的引用类型能够像值类型一样使用。
幸运的是.NET Framework中已经有了一个IClonealbe接口(System.ICloneable)来帮助我们解决问题。
使用这个接口可以规定所有的Dude类必须遵守和定义引用类型应如何被复制,以避免出现"共享鞋子"的问题。
所有需要被克隆的类都需要使用ICloneable接口,包括Shoe类。
System.IClonealbe只有一个方法定义:Clone()
public 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()方法返回的是对象的引用,所以我们需要在设置鞋的颜色前重构这个引用。
接着,在我们的CopyDude()方法中我们需要克隆鞋子而非拷贝它们:
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!,and a Blue on my left foot
Ted : Dude!,and a Red on my left foot
这就是我们想要的。
在通常情况下,我们应该"克隆"引用类型,"拷贝"值类型。(这样,在你调试以上介绍的情况中的问题时,会减少你买来控制头痛的阿司匹林的药量)
在头痛减少的激烈下,我们可以更进一步地使用Dude类来实现IClonealbe,而不是使用CopyDude()方法。
public class Dude: ICloneable { public string Name; public Shoe RightShoe; public Shoe LeftShoe; public override string ToString() { return (Name + ":Dude!,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.CopyDude()方法改为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!,and a Red on my left foot.
非常好!
比较有意思的是请注意为System.String类分配的操作符("="号),它实际上是将string型对象进行克隆,所以你不必担心会发生引用拷贝。
尽管如此你还是得注意一下内存的膨胀。如果你重新看一下前面的那些图,会发现string型应该是引用类型,所以它应该是一个指针(这个指针指向堆中的另一个对象),
但是为了方便起见,我在图中将string型表示为值类型(实际上应该是一个指针),因为通过"="号重新被赋值的string型对象实际上是被自动克隆过后的。
总结一下:
通常,如果我们打算将我们的对象用于拷贝,那么我们的类应该实现IClonealbe借口,这样能够使引用类型仿效值类型的行为。
从中可以看到,搞清楚我们所使用的变量的类型是非常重要的,因为在值类型和引用类型的对象在内存中的分配是有区别的。
在下一部分内容中,会看到我们是怎样来减少代码在内存中的"脚印"的,将会谈到期待已久的垃圾回收器(Garbage Collection)。 To be continued...