深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第五节 引用类型复制问题及用克隆接口ICloneable修复

转载 2013年12月05日 11:09:14

前言


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

简介

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


复制不仅仅是复制


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

值类型测试


首先,我们看一下值类型。下面是一个类和一个结构类型(值类型),Dude类包含一个Name元素和两个Shoe元素。我们有一个CopyDude()方法用来复制生成新Dude。
[csharp] view plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  1. public class Shoe{  
  2.                public string Color;  
  3.            }  

执行同样上面的Main()方法,结果改变了,如下:
[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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 plaincopy在CODE上查看代码片派生到我的代码片
  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接口。这样可以让引用类型模仿值类型的使用,从而防止意外的错误产生。你可以看到,慎重得理不同的类型非常重要,因为值类型和引用类型在内存中的分配是不同的。


翻译:http://www.c-sharpcorner.com/UploadFile/rmcochran/chsarp_memory401152006094206AM/chsarp_memory4.aspx

相关文章推荐

深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第五节 引用类型复制问题及用克隆接口ICloneable修复

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

深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第六节 理解垃圾回收GC,提搞程序性能

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

深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第二节 栈基本工作原理

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

深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第四节 参数传递对堆栈的影响 1

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

深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第一节 理解堆与栈

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

C# 实现可克隆(ICloneable)的类型

问题 有时需要创建一个自定义类型,它能为开发人员提供一种简单的机制来创建该类型实例的副本。 解决方案 实现System.ICloneable接口。 原理 如果我们有两个值类型的变量,将其...

c#的System.ICloneable接口说明

System.ICloneable接口支持克隆,即用与现有实例相同的值创建类的新实例。msdn上的解释很简单,主要就是clone方法的实行,介绍深拷贝和浅拷贝,搞的很糊涂,那么到底是什么意思呢?看看下...
  • bugDemo
  • bugDemo
  • 2012年08月28日 23:46
  • 537

C#个人笔记(随时更新ing)

C#个人笔记(随时更新ing) 1.DataTable Select(string)    表达式,排序查询: searchString += "CreatTime Select(sea...
  • L0veIT
  • L0veIT
  • 2012年08月03日 10:23
  • 1857

十种经典的排序算法C#实现(持续更新ing)

参考网络及其他书籍自己敲的,经验证通过 /********选择排序********/ using System; using System.Collections.Generic; usi...

C#和C++中类实例化的差异——从C#到C++爬坑ing

以前一直是用C#做一些小程序(自学编程的菜鸟)进入工作单位以后需要用C++做一些GIS算法和功能的开发,然后就转到C++的道路上来了,以前做项目的经验比较少对底层内存的一些关系理解也不透彻到现在就比较...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing) 第五节 引用类型复制问题及用克隆接口ICloneable修复
举报原因:
原因补充:

(最多只允许输入30个字)