译文---C#堆VS栈(Part Three)
前言
在本系列的第一篇文章《C#堆栈对比(Part Two)》中,介绍了值类型和引用类型在参数传递时的不同,本文将讨论如何应用ICloneable接口实现去修复引在堆上的用变量所带来的问题。
本文是系列文章的第三部分。
注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。
目录
C#堆栈对比(Part Three)
拷贝不是复制那么简单
为了更清楚的表达这个问题,我们来考察一下堆上的值类型与堆上的引用类型。首先,我们来看看值类型。跟随如下的类和结构体,我们有一个包含Name和两个Shoe字段的Dude类。我们有一个CopyDude方法方便我们产生一个新的Dude(花花公子)。
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结构体是类的一个成员,他们都在堆上。
注:这里体现了值类型是在栈上还是在堆上,完全取决于其声明时的地点。

当我们运行如下的方法时:
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改为引用类型呢?那将就是个问题,更改如下:
public class Shoe
{
public string Color;
}
更改之后再次运行代码,得到的结果如下:
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
红色的鞋子在另一个人身上,这明显是错的,你能看出这是怎么发生的吗?下图就是内存引用示例:

因为现在我们用Shoe的引用类型代替值类型,并且拷贝引用类型内容时仅仅是拷贝了指针(不是指针真正指向的对象),我们必须做一些额外工作使我们的引用类型的Shoe更符合值类型的行为。
注:上面这个例子中当Shoe为值类型时,已经伴随Dude的构造方法生成了一个完全独立的结构体Shoe对象,所以Bill为蓝色的鞋,Ted为红色的鞋;当Shoe为引用类型时,Shoe仅仅初始化了一次,所以Ted在使用Shoe时,其实更改的还是唯一初始化一次时的Shoe的内容,所以导致了最后大家都为红鞋。下文会应用深拷贝解决引用类型复制指针的问题。
幸运的是,我们有一个ICloneable接口来帮我们解决问题。这个接口是一个基本的契约,所有Dudes遵守这个契约并且规定如何按顺序的复制避免Shoe Sharing问题。我们所有将被复制的类应该使用ICloneable接口,包括Shoe类。
ICloneable包括一个方法:Clone()
下面我们将实现这个接口:
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内部,我们仅仅是New了一个新的Shoe对象,复制所有引用类型并且拷贝值类型,然后返回一个新对象。你可能注意到了String类已经实现了ICloneable接口,所以我们能调用Color.Clone方法。因为Clone返回一个对象的引用,我们必须在设置Shoe的颜色之前将类型显示转换成Shoe类型。
注:String类型是一种特殊的引用类型,其表现形式类似于值类型,因为字符串不可改变,如果改变则产生一个新对象,请参考这里。
下一步,在我们的CopyDude方法中我们需要克隆Shoes代替拷贝。
public Dude CopyDude()
{
Dude newPerson = new Dude();
newPerson.Name = Name;
newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
newPerson.RightShoe = RightShoe.Clone() as Shoe; return newPerson;
}
现在我们运行主方法:
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
这就是我们想要的。

将事物包裹起来
作为一个练习,我们希望总是克隆引用类型和复制值类型。(这将降低当你调试程序错误时所购买治疗头疼的阿司匹林的数量)
所以,在头疼降低的情况下,让我们走的更远一些并且让我们整理下Dude类实现ICloneable接口方法代替CopyDude方法。
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
}
我们所要做的仅仅是通过使用Dude.Clone改变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.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的操作符“=”真是的克隆了字符串,所以你不必担心重复的引用。然而你必须注意内存膨胀。如果你回头看看图示,字符串是引用类型,它真的本应该是一个指向堆的指针,但是简单起见,它的作用类似于值类型。
总结
作为一个练习,如果我们打算每次都拷贝对象,我们应该实现ICloneable接口。这将确保我们的引用类型有点像模仿值类型的行为。正如你所见到的那样,记录我们正在处理的变量是重要的,因为引用类型和值类型在创建内存上的区别。
在这下一篇文章中,我们将审视一种降低内存印记的方式。
1. 引用类型的拷贝一定要注意是深拷贝,还是简单的指针复制的浅拷贝。
2. System.String类型是特殊的引用类型,实际作用效果类似于值类型。
3. 引用类型应该实现ICloneable接口,实现深拷贝,即对象拷贝而非指针拷贝。
译文---C#堆VS栈(Part Three)的更多相关文章
- 译文---C#堆VS栈(Part One)
前言 本文主要是讲解C#语言在内存中堆.栈的使用情况,使读者能更好的理解值类型.引用类型以及线程栈.托管堆. 首先感谢原文作者:Matthew Cochran 为我们带来了一篇非常好的文章,并配以大量 ...
- 译文---C#堆VS栈(Part Two)
前言 在本系列的第一篇文章<C#堆栈对比(Part One)>中,介绍了堆栈的基本功能和值类型以及引用类型在程序运行时的表现,同时也包含了指针作用的讲解. 本文为文章的第二部分,主要讲解参 ...
- 译文---C#堆VS栈(Part Four)
前言 在本系列的第一篇文章<C#堆栈对比(Part Three)>中,介绍了值类型和引用类型在Copy上的区别以及如何实现引用类型的克隆以及使用ICloneable接口等内容. 本文为文章 ...
- JVM学习(2)——技术文章里常说的堆,栈,堆栈到底是什么,从os的角度总结
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 堆栈是栈 JVM栈和本地方法栈划分 Java中的堆,栈和c/c++中的堆,栈 数据结构层面的堆,栈 os层面 ...
- java中内存分配策略及堆和栈的比较
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间 ...
- 在JS中关于堆与栈的认识function abc(a){ a=100; } function abc2(arr){ arr[0]=0; }
平常我们的印象中堆与栈就是两种数据结构,栈就是先进后出:堆就是先进先出.下面我就常见的例子做分析: main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main( ...
- C语言堆和栈
堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈.2.堆区(heap ...
- JAVA中用堆和栈的概念来理解equals() "=="和hashcode()
在学习java基本数据类型和复杂数据类型的时候,特别是equals()"=="和hashcode()部分时,不是很懂,也停留了很长时间,最后终于有点眉目了. 要理解equals() ...
- C/C++ 堆和栈的区别
堆和栈的区别 一个由C/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.其 操作方式类似于数据结构中的栈. 2.堆区(h ...
随机推荐
- [php-src]扩展中封装业务与 call_user_function 的使用建议
内容均以php5.6.14为例. 从一个封装 uniqid 的例子来讲. /* {{{ wrapper of uniqid */ PHP_FUNCTION(fox) { // #1. zval *pr ...
- Maven 整合 spring profile实现多环境自动切换
Maven 整合 spring profile实现多环境自动切换 时间:2014-03-19 15:32来源:Internet 作者:Internet 点击:525次 profile主要用在项目多环境 ...
- OD使用教程8
方式一基本的打补丁方式: 打开程序之后首先会跳出一个nag窗口,从中我们知道了可以将nag窗口作为切入点,只要找到了nag的触发点就等同于找到注册与未注册的判断的点 右键-查找-所有参考文本字串 ...
- python __call__内置函数
__call__实现可以直接调用对象的作用
- Java 第17章 继承
继承的概念 继承机制是面向对象程序设计不可缺少的关键概念,是实现软件可重用的根基, 是提高软件系统的可扩展性与可维护性的主要途径. 所谓继承是指一个类的定义可以基于另外一个已经存在的类,即子类基于父类 ...
- openjudge2985(数字组合)
描述 有n个正整数,找出其中和为t(t也是正整数)的可能的组合方式.如:n=5,5个数分别为1,2,3,4,5,t=5:那么可能的组合有5=1+4和5=2+3和5=5三种组合方式.输入输入的第一行是两 ...
- viewpage滑动查看图片并再有缩略图预览
首先看下效果图, 主要功能分为3大块 一是滑动查看,通过viewpage来实现,方法见 http://www.cnblogs.com/lovemo1314/p/6109312.html 二.点击放大 ...
- 响应式web设计之CSS3 Media Queries
开始研究响应式web设计,CSS3 Media Queries是入门. Media Queries,其作用就是允许添加表达式用以确定媒体的环境情况,以此来应用不同的样式表.换句话说,其允许我们在不改变 ...
- 那年有关 return ; 的一切
现在只知道在dev C++里面声明了函数的返回类型不为void就不能写return; ,但是如果返回值为void就可以写return; ,而且如你所愿.
- Android中处理OnClick和OnTouch方法冲突的解决方案
目前想到的最好的解决方法,大家有更好的欢迎告知. 问题:在一个view中有一个按钮,要求可以通过点按移动这个按钮,同时单纯的点击而不移动这个按钮的话可以跳转到新的Activity. 遇到的困难:按钮的 ...