[No0000146]深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)理解堆与栈3/4

前言

 
虽然在.Net Framework 中我们不必考虑内在管理和垃圾回收(GC),但是为了优化应用程序性能我们始终需要了解内存管理和垃圾回收(GC)。另外,了解内存管理可以帮助我们理解在每一个程序中定义的每一个变量是怎样工作的。
 

简介

这一节我们将介绍引用类型变量在堆中存储时会产生的问题,同时介绍怎么样使用克隆接口ICloneable去修复这种问题。
 
 

复制不仅仅是复制

 
为了更清晰的阐述这个问题,让我们测试一下在堆中存储值类型变量和引用类型变量时会产生的不同情况。
 

值类型测试

 
首先,我们看一下值类型。下面是一个类和一个结构类型(值类型),Dude类包含一个Name元素和两个Shoe元素。我们有一个CopyDude()方法用来复制生成新Dude。
[csharp] view plain copy
 
  1. public struct Shoe{  
  2.     public string Color;  
  3. }  
  4.   
  5. public class Dude  
  6. {  
  7.      public string Name;  
  8.      public Shoe RightShoe;  
  9.      public Shoe LeftShoe;  
  10.   
  11.      public Dude CopyDude()  
  12.      {  
  13.          Dude newPerson = new Dude();  
  14.           newPerson.Name = Name;  
  15.           newPerson.LeftShoe = LeftShoe;  
  16.           newPerson.RightShoe = RightShoe;  
  17.   
  18.           return newPerson;  
  19.      }  
  20.   
  21.      public override string ToString()  
  22.      {  
  23.           return (Name + " : Dude!, I have a " + RightShoe.Color  +  
  24.               " shoe on my right foot, and a " +  
  25.                LeftShoe.Color + " on my left foot.");  
  26.      }  
  27.   
  28. }  

Dude类是一个复杂类型,因为值 类型结构Shoe是它的成员, 它们都将存储在堆中。
 
 
当我们执行下面的方法时:
[csharp] view plain copy
 
  1. public static void Main()  
  2.            {  
  3.                Class1 pgm = new Class1();  
  4.    
  5.                   Dude Bill = new Dude();  
  6.                   Bill.Name = "Bill";  
  7.                   Bill.LeftShoe = new Shoe();  
  8.                   Bill.RightShoe = new Shoe();  
  9.                   Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";  
  10.    
  11.                   Dude Ted =  Bill.CopyDude();  
  12.                   Ted.Name = "Ted";  
  13.                   Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";  
  14.    
  15.                   Console.WriteLine(Bill.ToString());  
  16.                   Console.WriteLine(Ted.ToString());              
  17.    
  18.            }  

我们得到了期望的结果:
[plain] view plain copy
 
  1. Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.  
  2. Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.  
如果我们把Shoe换成引用类型呢?
 

引用类型测试

 
当我们把Shoe改成引用类型时,问题就产生了。
[csharp] view plain copy
 
  1. public class Shoe{  
  2.                public string Color;  
  3.            }  

执行同样上面的Main()方法,结果改变了,如下:
[plain] view plain copy
 
  1. Bill : Dude!, I have a Red shoe on my right foot, and a Red on my left foot  
  2. Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot  

这并不是我们期望的结果。很明显,出错了!看下面的图解:
 
因为现在Shoe是引用类型而不是值类型,当我们进行复制时仅是复制了指针,我们并没有复制指针真正对应的对象。这就需要我们做一些额外的工作使引用类型Shoe像值类型一样工作。
很幸运,我们有一个接口可以帮我们实现:ICloneable。当Dude类实现它时,我们会声明一个Clone()方法用来产生新的Dude复制类。(译外话:复制类及其成员跟原始类不产生任何重叠,即我们所说的深复制)   看下面代码:
[csharp] view plain copy
 
  1. ICloneable consists of one method: Clone()  
  2.   
  3.                   public object Clone()  
  4.                   {  
  5.    
  6.                   }  
  7.   
  8. Here's how we'll implement it in the Shoe class:  
  9.   
  10.            public class Shoe : ICloneable  
  11.              {  
  12.                   public string Color;  
  13.                   #region ICloneable Members  
  14.    
  15.                   public object Clone()  
  16.                   {  
  17.                       Shoe newShoe = new Shoe();  
  18.                       newShoe.Color = Color.Clone() as string;  
  19.                       return newShoe;  
  20.                   }  
  21.   
  22.                   #endregion  
  23.              }  

在Clone()方法里,我们创建了一个新的Shoe,克隆所有引用类型变量,复制所有值类型变量,最后返回新的对象Shoe。有些既有类已经实现了ICloneable,我们直接使用即可,如String。因此,我们直接使用Color.Clone()。因为Clone()返回object对象,我们需要进行一下类型转换。
下一步,我们在CopyDude()方法里,用克隆Clone()代替复制:
[csharp] view plain copy
 
  1. public Dude CopyDude()  
  2.                 {  
  3.                     Dude newPerson = new Dude();  
  4.                      newPerson.Name = Name;  
  5.                      newPerson.LeftShoe = LeftShoe.Clone() as Shoe;  
  6.                      newPerson.RightShoe = RightShoe.Clone() as Shoe;  
  7.    
  8.                      return newPerson;  
  9.                 }  

再次执行主方法Main():
[csharp] view plain copy
 
  1. public static void Main()  
  2.            {  
  3.                Class1 pgm = new Class1();  
  4.    
  5.                   Dude Bill = new Dude();  
  6.                   Bill.Name = "Bill";  
  7.                   Bill.LeftShoe = new Shoe();  
  8.                   Bill.RightShoe = new Shoe();  
  9.                   Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";  
  10.    
  11.                   Dude Ted =  Bill.CopyDude();  
  12.                   Ted.Name = "Ted";  
  13.                   Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";  
  14.    
  15.                   Console.WriteLine(Bill.ToString());  
  16.                   Console.WriteLine(Ted.ToString());              
  17.    
  18.            }  

我们得到了期望的结果:
 
[plain] view plain copy
 
  1. Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot  
  2. Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot  

下面是图解:
 

整理我们的代码

 
在实践中,我们是希望克隆引用类型并复制值类型的。这会让你回避很多不易察觉的错误,就像上面演示的一样。这种错误有时不易被调试出来,会让你很头疼。
 
因此,为了减轻头疼,让我们更进一步清理上面的代码。我们让Dude类实现IConeable代替使用CopyDude()方法:
[csharp] view plain copy
 
  1. public class Dude: ICloneable  
  2.            {  
  3.                 public string Name;  
  4.                 public Shoe RightShoe;  
  5.                 public Shoe LeftShoe;  
  6.    
  7.                 public override string ToString()  
  8.                 {  
  9.                      return (Name + " : Dude!, I have a " + RightShoe.Color  +  
  10.                          " shoe on my right foot, and a " +  
  11.                           LeftShoe.Color + " on my left foot.");  
  12.                     }  
  13.                   #region ICloneable Members  
  14.    
  15.                   public object Clone()  
  16.                   {  
  17.                        Dude newPerson = new Dude();  
  18.                        newPerson.Name = Name.Clone() as string;  
  19.                        newPerson.LeftShoe = LeftShoe.Clone() as Shoe;  
  20.                        newPerson.RightShoe = RightShoe.Clone() as Shoe;  
  21.    
  22.                        return newPerson;  
  23.                   }  
  24.   
  25.                   #endregion  
  26.              }  

在主方法Main()使用Dude.Clone():
[csharp] view plain copy
 
  1. public static void Main()  
  2.            {  
  3.                Class1 pgm = new Class1();  
  4.    
  5.                   Dude Bill = new Dude();  
  6.                   Bill.Name = "Bill";  
  7.                   Bill.LeftShoe = new Shoe();  
  8.                   Bill.RightShoe = new Shoe();  
  9.                   Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";  
  10.    
  11.                   Dude Ted =  Bill.Clone() as Dude;  
  12.                   Ted.Name = "Ted";  
  13.                   Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";  
  14.    
  15.                   Console.WriteLine(Bill.ToString());  
  16.                   Console.WriteLine(Ted.ToString());              
  17.    
  18.            }  

最后得到期望的结果:
[plain] view plain copy
 
  1. Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.  
  2. Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.  
 

特殊引用类型String

 
在C#中有趣的是,当System.String 使用操作符“=”时,实际上是进行了克隆(深复制)。你不必担心你只是在操作一个指针,它会在内存中创建一个新的对象。但是,你一定要注意内存的占用问题(译外话:比如为什么在一定情况下我们使用StringBuilder代替String+String+String+String...前者速度稍慢初始化耗多点内存但在大字符串操作上节省内存,后者速度稍快初始化简单但在大字符串操作上耗内存)。如果我们回头去看上面的图解中,你会发现Stirng类型在图中并不是一个针指向另一个内存对象,而是为了尽可能的简单,把它当成值类型来演示了。
 

总结

 
在实际工作中,当我们需要复制引用类型变量时,我们最好让它实现ICloneable接口。这样可以让引用类型模仿值类型的使用,从而防止意外的错误产生。你可以看到,慎重得理不同的类型非常重要,因为值类型和引用类型在内存中的分配是不同的。

 

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.

Until then,

Happy coding.

posted @ 2018-05-29 11:29  CharyGao  阅读(209)  评论(0编辑  收藏  举报