前言

 
虽然在.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. Sort_Buffer_Size 设置对服务器性能的影响

    基础知识: 1. Sort_Buffer_Size 是一个connection级参数,在每个connection第一次需要使用这个buffer的时候,一次性分配设置的内存.2. Sort_Buffer ...

  2. Linux 内存机制【转载】

    原文地址:http://blog.csdn.net/tianlesoftware/article/details/5463790 一. 内存使用说明 Free 命令相对于top 提供了更简洁的查看系统 ...

  3. linux/unix命令参考

  4. 【C语言】字节对齐问题(以32位系统为例)

    1. 什么是对齐? 现代计算机中内存空间都是按照字节(byte)划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型 ...

  5. vue项目eslint环境配置与vscode配置eslint

    eslint基础环境搭建 全局安装eslint:npm install eslint -g 项目eslint初始化:eslint --init,按团队或自己的编程风格回答三道题. ? How woul ...

  6. java this 子类调父类,父类再调用子类已覆盖的方法及属性(又一次理解)

    之前一直以为 this关键字 是指调用者对象,但是这次才真正理解,this代表当前对象,但是指向调用者对象,其实就是多态的用法,如下所示:B 继承了 A,在B 中调用A类的方法,在A 中用this 访 ...

  7. http://www.cnblogs.com/chenmeng0818/p/6370819.html

    http://www.cnblogs.com/chenmeng0818/p/6370819.html js中的正则表达式入门   什么是正则表达式呢? 正则表达式(regular expression ...

  8. AndroidStudio2.2 Preview3中NDK开发之CMake和传统 JNI在目录结构和配置文件上的区别(转载)

    自从AndroidStudio更新到2.2,就有了CMake和传统JNI两种开发NDK的方法,主要就是在目录结构和build.gradle上的区别,下面我们将分别介绍目录区别和build.gradle ...

  9. Go指南练习_Stringer

    源地址 https://tour.go-zh.org/methods/18 一.题目描述 通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址. 例如,IPAddr{1, 2 ...

  10. Java如何从文件中打印与给定模式匹配的所有字符串?

    在Java编程中,如何从文件中打印与给定模式匹配的所有字符串? 以下示例显示了如何使用Util.regex类的Patternname.matcher()方法从文件中打印与给定模式匹配的所有字符串. p ...