前言

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

简介

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

复制不仅仅是复制

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

值类型测试

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

Dude类是一个复杂类型,因为值 类型结构Shoe是它的成员, 它们都将存储在堆中。

 
 
当我们执行下面的方法时:
  1. public static void Main()
  2. {
  3. Class1 pgm = new Class1();
  4. Dude Bill = new Dude();
  5. Bill.Name = "Bill";
  6. Bill.LeftShoe = new Shoe();
  7. Bill.RightShoe = new Shoe();
  8. Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
  9. Dude Ted =  Bill.CopyDude();
  10. Ted.Name = "Ted";
  11. Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
  12. Console.WriteLine(Bill.ToString());
  13. Console.WriteLine(Ted.ToString());
  14. }

我们得到了期望的结果:

  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改成引用类型时,问题就产生了。
  1. public class Shoe{
  2. public string Color;
  3. }

执行同样上面的Main()方法,结果改变了,如下:

  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复制类。(译外话:复制类及其成员跟原始类不产生任何重叠,即我们所说的深复制)   看下面代码:
  1. ICloneable consists of one method: Clone()
  2. public object Clone()
  3. {
  4. }
  5. Here's how we'll implement it in the Shoe class:
  6. public class Shoe : ICloneable
  7. {
  8. public string Color;
  9. #region ICloneable Members
  10. public object Clone()
  11. {
  12. Shoe newShoe = new Shoe();
  13. newShoe.Color = Color.Clone() as string;
  14. return newShoe;
  15. }
  16. #endregion
  17. }

在Clone()方法里,我们创建了一个新的Shoe,克隆所有引用类型变量,复制所有值类型变量,最后返回新的对象Shoe。有些既有类已经实现了ICloneable,我们直接使用即可,如String。因此,我们直接使用Color.Clone()。因为Clone()返回object对象,我们需要进行一下类型转换。

下一步,我们在CopyDude()方法里,用克隆Clone()代替复制:
  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. return newPerson;
  8. }

再次执行主方法Main():

  1. public static void Main()
  2. {
  3. Class1 pgm = new Class1();
  4. Dude Bill = new Dude();
  5. Bill.Name = "Bill";
  6. Bill.LeftShoe = new Shoe();
  7. Bill.RightShoe = new Shoe();
  8. Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
  9. Dude Ted =  Bill.CopyDude();
  10. Ted.Name = "Ted";
  11. Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
  12. Console.WriteLine(Bill.ToString());
  13. Console.WriteLine(Ted.ToString());
  14. }

我们得到了期望的结果:

 
  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()方法:
  1. public class Dude: ICloneable
  2. {
  3. public string Name;
  4. public Shoe RightShoe;
  5. public Shoe LeftShoe;
  6. public override string ToString()
  7. {
  8. return (Name + " : Dude!, I have a " + RightShoe.Color  +
  9. " shoe on my right foot, and a " +
  10. LeftShoe.Color + " on my left foot.");
  11. }
  12. #region ICloneable Members
  13. public object Clone()
  14. {
  15. Dude newPerson = new Dude();
  16. newPerson.Name = Name.Clone() as string;
  17. newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
  18. newPerson.RightShoe = RightShoe.Clone() as Shoe;
  19. return newPerson;
  20. }
  21. #endregion
  22. }

在主方法Main()使用Dude.Clone():

  1. public static void Main()
  2. {
  3. Class1 pgm = new Class1();
  4. Dude Bill = new Dude();
  5. Bill.Name = "Bill";
  6. Bill.LeftShoe = new Shoe();
  7. Bill.RightShoe = new Shoe();
  8. Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
  9. Dude Ted =  Bill.Clone() as Dude;
  10. Ted.Name = "Ted";
  11. Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
  12. Console.WriteLine(Bill.ToString());
  13. Console.WriteLine(Ted.ToString());
  14. }

最后得到期望的结果:

  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.

[No0000146]深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)理解堆与栈3/4的更多相关文章

  1. [No0000145]深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)理解堆与栈2/4

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

  2. [No0000144]深入浅出图解C#堆与栈 C# Heap(ing) VS Stack(ing)理解堆与栈1/4

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

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

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

  4. Java 堆内存与栈内存异同(Java Heap Memory vs Stack Memory Difference)

    --reference Java Heap Memory vs Stack Memory Difference 在数据结构中,堆和栈可以说是两种最基础的数据结构,而Java中的栈内存空间和堆内存空间有 ...

  5. java - Stack栈和Heap堆的区别

    首先分清楚Stack,Heap的中文翻译:Stack—栈,Heap—堆.         在中文里,Stack可以翻译为“堆栈”,所以我直接查找了计算机术语里面堆和栈开头的词语:        堆存储 ...

  6. 内存,堆,栈,heap,stack,data

    1. 基本类型占一块内存. 引用类型占两块. 2. 类是静态概念. 函数中定义的基本类型变量和对象的引用类型变量都在函数的栈内存. 局部变量存在栈内存. new创建的对象和数组,存在堆内存. java ...

  7. iOS:堆(heap)和栈(stack)的理解

    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release 栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就 ...

  8. iOS中的堆(heap)和栈(stack)的理解

    操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在三个不同的内存区域,分成三个段:“text segment “,“stack segment ”,“heap segme ...

  9. stm32 堆和栈(stm32 Heap & Stack)【worldsing笔记】

    关于堆和栈已经是程序员的一个月经话题,大部分有是基于os层来聊的.   那么,在赤裸裸的单片机下的堆和栈是什么样的分布呢?以下是网摘:     刚接手STM32时,你只编写一个 int main() ...

随机推荐

  1. 关于ř与tableau的集成---- k均值聚类

    1.利用R内置数据集iris: 2.通过Rserve 包连接tableau,服务器:localhost,默认端口6311: 3.加载数据集iris: 4.编辑字段:Cluster <span s ...

  2. 动态改变APP图标

    一.iOS动态更换App图标(一):基础使用 该功能应用的场景 1.白天/夜间模式切换,在切换App主色调同时切换App图标. 2.各类皮肤主题(淘宝就可换肤),附带App图标一块更换. 3.利用Ap ...

  3. apache的性能调配 MaxClients 与MaxRequestsPerChild

    因近期服务不稳定,现象和这个比较类似http://hi.baidu.com/xinfeng999/blog/item/1aea470e214ab1cd7acbe1ed.html根据现象来对APACHE ...

  4. [转]Java 反射在实际开发中的应用

    一:Java类加载和初始化 1.1 类加载器(类加载的工具) 1.2 Java使用一个类所需的准备工作 二:Java中RTTI 2.1 :为什么要用到运行时类型信息(就是RTTI) 2.2  :RTT ...

  5. 物联网架构成长之路(9)-双机热备Keepalived了解

    1. 前言 负载均衡LB,高可用HA,这一小结主要讲双机热备方案保证高可用.这里选择Keepalived作为双机热备方案,下面就对具体的配置进行了解.2. 下载Keepalived wget http ...

  6. 每日英语:Don't Call Us Bossy

    [Confident girls are often called the other B-word, and it can keep them from reaching their full po ...

  7. 设置全局git忽略文件 gitconfig

    cat ~/.gitconfig [user] email = yuanhuikai@liquidnetwork.com name = yuanhuikai[core] excludesfile = ...

  8. 基于jQuery鼠标滚轮滑动到页面节点部分

    基于jQuery鼠标滚轮滑动到页面节点部分.这是一款基于jQuery+CSS3实现的使用鼠标滚轮或者手势滑动到页面节点部分特效.效果图如下: 在线预览   源码下载 实现的代码. html代码: &l ...

  9. [转]PowerDesigner大小写转换

    原文地址:https://blog.csdn.net/fzqlife/article/details/72769959?utm_source=blogxgwz7 在菜单栏找到:Tools-->E ...

  10. Openlayers离线载入天地图

    概述: 经过一个春节的休整,今天最终開始了! 任何时候.都不要忘记学习.学习是一辈子的事情!今天,我来说说怎样实现天地图的离线以及Openlayers载入离线数据实现天地图数据的展示. 实现: 1.获 ...