NET中栈和堆的区别(比较)(3)

英文原著:

Even though with the .NET framework we don't have to actively worry about memory management and garbage collection (GC), we still have to keep memory management and GC in mind in order to optimize the performance of our applications. Also, having a basic understanding of how memory management works will help explain the behavior of the variables we work with in every program we write. In this article we'll cover an issue that arises from having reference variables in the heap and how to fix it using ICloneable.

A Copy Is Not A Copy.

To clearly define the problem, let's examine what happens when there is a value type on the heap versus having a reference type on the heap. First we'll look at the value type. Take the following class and struct. We have a Dude class which contains a Name element and two Shoe(s). We have a CopyDude() method to make it easier to make new 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.");

                }

 

           }

Our Dude class is a variable type and because the Shoe struct is a member element of the class they both end up on the heap.

 

When we run the following method:

           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());            

 

           }

We get the expected output:

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.

What happens if we make the Shoe a reference type?  Herein lies the problem. If we change the Shoe to a reference type as follows:

           public class Shoe{

               public string Color;

           }

and run the exact same code in Main(), look how our input changes:

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

The Red shoe is on the other foot. This is clearly an error. Do you see why it's happening? Here's what we end up with in the heap.

 

Because we now are using Shoe as a reference type instead of a value type and when the contents of a reference type are copied only the pointer is copied (not the actual object being pointed to), we have to do some extra work to make our Shoe reference type behave more like a value type.

Luckily, we have an interface that will help us out: ICloneable. This interface is basically a contract that all Dudes will agree to and defines how a reference type is duplicated in order to avoid our "shoe sharing" error. All of our classes that need to be "cloned" should use the ICloneable interface, including the Shoe class.

ICloneable consists of one method: Clone()

                  public object Clone()

                  {

 

                  }

Here's how we'll implement it in the Shoe class:

           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

             }

Inside the Cone() method, we just make a new Shoe, clone all the reference types and copy all the value types and return the new object. You probably noticed that the string class already implements ICloneable so we can call Color.Clone(). Because Clone() returns a reference to an object, we have to "retype" the reference before we can set the Color of the shoe.

Next, in our CopyDude() method we need to clone the shoes instead of copying them

                public Dude CopyDude()

                {

                    Dude newPerson = new Dude();

                     newPerson.Name = Name;

                     newPerson.LeftShoe = LeftShoe.Clone() as Shoe;

                     newPerson.RightShoe = RightShoe.Clone() as Shoe;

 

                     return newPerson;

                }

Now, when we run 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.CopyDude();

                  Ted.Name = "Ted";

                  Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";

 

                  Console.WriteLine(Bill.ToString());

                  Console.WriteLine(Ted.ToString());            

 

           }

We get:

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

Which is what we want.

 

Wrapping Things Up.

So as a general practice, we want to always clone reference types and copy value types. (It will reduce the amount of aspirin you will have to purchase to manage the headaches you get debugging these kinds of errors.)

So in the spirit of headache reduction, let's take it one step further and clean up the Dude class to implement ICloneable instead of using the CopyDude() method.

           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

             }

And we'll change the Main() method to use Dude.Clone()

           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());            

 

           }

And our final output is:

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.

So all is well.

Something interesting to note is that the assignment operator (the "=" sign) for the System.String class actually clones the string so you don't have to worry about duplicate references. However you do have to watch our for memory bloating. If you look back at the diagrams, because the string is a reference type it really should be a pointer to another object in the heap, but for simplicity's sake, it's shown as a value type.

In Conclusion.

As a general practice, if we plan on ever copying of our objects, we should implement (and use) ICloneable.  This enables our reference types to somewhat mimic the behavior of a value type. As you can see, it is very important to keep track of what type of variable we are dealing with because of differences in how the memory is allocated for value types and reference types.

In the next article, we'll look at a way to reduce our code "footprint" in memory.
中文翻译:

尽管在.NET framework下我们并不需要担心内存管理和垃圾回收(Garbage Collection),但是我们还是应该了解它们,以优化我们的应用程序。同时,还需要具备一些基础的内存管理工作机制的知识,这样能够有助于解释我们日常程序编写中的变量的行为。在本文中我们将涉及到堆中引用变量引起的问题,以及如何使用ICloneable接口来解决该问题。

需要回顾堆栈基础,值类型和引用类型,请转到第一部分和第二部分
第一部分
http://agassi001.cnblogs.com/archive/2006/05/10/396574.html

第二部分
http://agassi001.cnblogs.com/archive/2006/05/13/399080.html


* 副本并不是真的副本

为了清楚的阐明问题,让我们来比较一下当堆中存在值类型和引用类型时都发生了些什么。首先来看看值类型,如下面的类和结构。这里有一个类Dude,它的成员中有一个string型的Name字段及两个Shoe类型的字段--RightShoe、LeftShoe,还有一个CopyDude()方法可以很容易地生成新的Dude实例。
复制C#代码保存代码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类的成员,所以它们都被放在了堆上。


当我们执行以下的方法时:
复制C#代码保存代码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改为引用类型:
复制C#代码保存代码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
可以看到红鞋子被穿到别人(Bill)脚上了,很明显出错了。你想知道这是为什么吗?我们再来看看堆就明白了。


由于我们现在使用的Shoe是引用类型而非值类型,当引用类型的内容被拷贝时实际上只拷贝了该类型的指针(并没有拷贝实际的对象),我们需要作一些额外的工作来使我们的引用类型能够像值类型一样使用。

幸运的是.NET Framework中已经有了一个IClonealbe接口(System.ICloneable)来帮助我们解决问题。使用这个接口可以规定所有的Dude类必须遵守和定义引用类型应如何被复制,以避免出现"共享鞋子"的问题。所有需要被克隆的类都需要使用ICloneable接口,包括Shoe类。

System.IClonealbe只有一个方法定义:Clone()
复制C#代码保存代码public object Clone()
{
}
我们应该在Shoe类中这样实现:
复制C#代码保存代码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()方法中我们需要克隆鞋子而非拷贝它们:
复制C#代码保存代码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()函数时:
复制C#代码保存代码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类来实现IClonealbe,而不是使用CopyDude()方法。
复制C#代码保存代码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.CopyDude()方法改为Dude.Clone():
复制C#代码保存代码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型应该是引用类型,所以它应该是一个指针(这个指针指向堆中的另一个对象),但是为了方便起见,我在图中将string型表示为值类型(实际上应该是一个指针),因为通过"="号重新被赋值的string型对象实际上是被自动克隆过后的。


总结一下:

通常,如果我们打算将我们的对象用于拷贝,那么我们的类应该实现IClonealbe借口,这样能够使引用类型仿效值类型的行为。从中可以看到,搞清楚我们所使用的变量的类型是非常重要的,因为在值类型和引用类型的对象在内存中的分配是有区别的。

在下一部分内容中,会看到我们是怎样来减少代码在内存中的"脚印"的,将会谈到期待已久的垃圾回收器(Garbage Collection)。

posted on 2007-11-09 13:21  hzwang  阅读(258)  评论(0)    收藏  举报

导航