-从这篇开始首先会分析的是设计模式中5个创建型模型的一些学习心得,文章最后会附上目录与进度表,希望大家多多支持多多推荐。
在没有复印机的时代,如果是你跑去西天取经,你觉得佛祖会让你直接把经书拉回家么,咱没金蝉子那么大的面子么....
必然的结果就是抄书。你抄个十年八载的拉回大唐,原手抄版得保留作为典籍,但为了弘扬大乘佛法,就又得抄书,抄个几十万册然后分发给各大寺院。
在程序猿眼中,佛祖的经书就是一个对象,抄书就相当与 new 了一个新的对象,然后把经书中的内容填充到新的对象中。
想想看,我们每抄一本书,就相当与把书中的内容完完整整的读了一遍,无法忽略细节来创建一个对象,也就是说经书的构造函数执行的时间很长,每次都new的话效率实在太低了。
并且如果你不小心将"自古以来,黄岩岛都是中国的固有领土",抄成“自古以来,菲律宾都是中国的固有领土”<开个玩笑>,那整个大唐都会这么认为了。
那么多书要改起来,还不得改到猴年马月去了,那时候又没有微博,让你公开道个歉。不过我是支持这么干的,干嘛道歉呢啧啧。。把日本也纳入其中我都木意见。
一,原型模式
为了解决类似于这种从一个对象再创建<克隆>另外一个可定制的对象,而不需要知道任何创建的细节的问题,人类世界发明了印刷术和更高级的复印机。
而我们的原型模式就是程序世界中的复印机。
我们来具体的看下原型模式(Prototype)的实现:
这个图是原型模型的UML类图,在设计模式中会常用类图来图解其结构。<如对UML类图有疑惑,请移步该系列的第一篇来了解UML类图:传送门>
基本原型模式的代码
/// <summary> /// 原型抽象类 /// </summary> public abstract class Prototype { public string Text { get; set; } public abstract Prototype Clone();//抽象类关键就是有这样一个Clone方法 } /// <summary> /// 具体的原型类 经书 /// </summary> public class Book : Prototype { public override Prototype Clone() { return (Prototype)this.MemberwiseClone();//创建当前对象的浅表副本。 } }
MemberwiseClone()方法是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象,因此,原始对象及副本引用同一个对象。
这里就引出了深复制与浅复制的问题,这个在稍后探讨。
这样我们就不必一个一个的抄书了:
static void Main(string[] args) { Book original = new Book { Text = "抄死你不偿命的经文" }; Book copy1 = new Book { Text = original.Text };//抄完你已经快往生了.... Book copy2 = (Book)original.Clone();//克隆一下就可以了 Console.WriteLine("copy2.Text is {0}", copy2.Text); }
通过克隆<现实中可能是刻板印刷>,我们可以不清楚经书的内容,就可以得到一个原件的副本,当我们需要了解它的时候,再去了解它。
这就是上文提到过的原型模式的意义,一般在初始化的信息不发生变化的情况下,克隆是最好的办法,这既隐藏了对象创建的细节,又大大的提高了性能。
对于.NET来说,原型模式非常好实现,在System命名空间中提高了ICloneable接口,其中只有唯一的一个方法Clone()。
这样我们只需要实现这个接口就可以完成原型模式了,原型抽象类Prototyoe已经不需要了:
.NET实现原型模式:
public class Book : ICloneable { public string Text { get; set; } public Object Clone() { return (Object)this.MemberwiseClone(); } }
方法调用方式于上述例子无异。
好了,原型模式就介绍到这。现在我们回过头来看看之前说的深复制和浅复制的问题。
二,深复制与浅复制
前文已经介绍过MemberwiseClone()方法是创建浅表副本,对于对象字段为引用类型的,只复制引用,而不复制引用的对象。我们来改一下Book类:
public class Book : ICloneable { public string Text { get; set; } public Version Version { get; set; } public Object Clone() { return (Object)this.MemberwiseClone(); } public void ShowVersion() { Console.WriteLine("Version {0} Author is {1}",Version.Num,Version.Author); } } public class Version { public int Num { get; set; } public string Author { get; set; } }
这种情况下,Book类中就有引用类型的属性Version了。那么在克隆过程中会发生什么事情呢?
static void Main(string[] args) { Book original = new Book { Text = "抄死你不偿命的经文", Version = new Version { Num = 0, Author = "如来" } }; Book copy1 = (Book)original.Clone(); Book copy2 = (Book)original.Clone(); Book copy3 = (Book)original.Clone(); copy1.Version.Author = "孙猴子"; copy2.Version.Author = "猪八戒"; copy3.Version.Author = "JohnConnor";
copy1.Version.Num = 1;
copy2.Version.Num = 2;
copy3.Version.Num = 3; copy1.ShowVersion(); copy2.ShowVersion(); copy3.ShowVersion();
Console.ReadKey(); }
结果并不像我们所想的,每个版本的作者分别为猴子,八戒,和我。
三个版本信息引用的都是最后一次的设置,这是因为上文所述的,只复制引用,而不复制引用的对象,克隆的过程中并没有创建对对象,现在三个引用都指向了同一个Version对象。
这就叫做浅复制,被复制的对象的所有变量都含有与原来的对象相同的值,但所有的对其他对象的引用都仍然指向原来的对象。
但这很明显会有问题,比如上述问题,我们需要copy1,copy2,copy3中的Version引用指向不同的对象,才不会出现修改一个全都改变的结果。
这种叫做深复制,把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象。
三,深复制的实现
以上述抄经为例,代码结构图
public class Book : ICloneable { public string Text { get; set; } private Version version; public Version Version { get { return version;} set { this.version = (Version)value.Clone(); }//Version的Set方法中使用Clone的对象,这样就创建了新的Version对象 } public Object Clone()//实现Clone方法,对新对象的相关字段赋值,返回一个深赋值的Book对象 { Book obj = new Book(); obj.Text = this.Text; obj.Version = this.Version; return obj; } public void ShowVersion() { Console.WriteLine("Version {0} Author is {1}", Version.Num, Version.Author); } } public class Version : ICloneable { public int Num { get; set; } public string Author { get; set; } public Object Clone()//实现Clone方法 { return (Object)this.MemberwiseClone(); } }
这样我们再执行控制台程序输出克隆结果:
这次三个结果各不相同了把。
不过要注意,我们这里只有一层引用,如果深复制深入的层数过多或出现循环引用就比较麻烦,需要格外的小心。
深复制和浅复制涉及的场合还是比较多的,比如数据集对象DataSet,它就有两个方法Clone()和Copy(),Clone()用来复制其结构,但不复制数据,实现了原型模式的浅复制。
Copy()方法在复制结构的基础上,也复制数据,即原型模式的深复制。
最后补充一点
copy1.Text = "波若波罗蜜"; Console.WriteLine(original.Text);
Console.ReadKey();
大家觉得这句会输出什么呢?按照上述的理论,Text是string-引用类型,那所有的对象的Text应该引用的是同一个string对象,改一个就全改了。
然而这个担心是不必要的,输入会是最开始如来写下的"抄死你不偿命的经文"。
这里可能有人要跳出来说,string不就是引用类型么,为什么在不需要特殊处理就能完成深复制?
因为对string对象本身进行赋值的过程是,创建新的string对象来存储新值,返回新对象的引用。这是C# string类型的一个特点。
<若对string有任何疑问的,或是引用类型和值类型不太了解的,我之前有一篇文章深入内存探讨这个问题,点击传送门进入>
----------------------------------------------End--------------------------------------------------
JohnConnor设计模式笔记系列 目录
JohnConnor设计模式笔记(一) 学习设计模式之前你必须掌握的-看懂UML类图
JohnConnor设计模式笔记(二) 程序世界里的复印机-原型模式与浅复制/深复制
未完待续......