C#设计模式(6)——原型模式(Prototype Pattern) C# 深浅复制 MemberwiseClone
C#设计模式(6)——原型模式(Prototype Pattern)
一、引言
在软件系统中,当创建一个类的实例的过程很昂贵或很复杂,并且我们需要创建多个这样类的实例时,如果我们用new操作符去创建这样的类实例,这未免会增加创建类的复杂度和耗费更多的内存空间,因为这样在内存中分配了多个一样的类实例对象,然后如果采用工厂模式来创建这样的系统的话,随着产品类的不断增加,导致子类的数量不断增多,反而增加了系统复杂程度,所以在这里使用工厂模式来封装类创建过程并不合适,然而原型模式可以很好地解决这个问题,因为每个类实例都是相同的,当我们需要多个相同的类实例时,没必要每次都使用new运算符去创建相同的类实例对象,此时我们一般思路就是想——只创建一个类实例对象,如果后面需要更多这样的实例,可以通过对原来对象拷贝一份来完成创建,这样在内存中不需要创建多个相同的类实例,从而减少内存的消耗和达到类实例的复用。 然而这个思路正是原型模式的实现方式。下面就具体介绍下设计模式中的原型设计模式。
二、原型模式的详细介绍
在现实生活中,也有很多原型设计模式的例子,例如,细胞分裂的过程,一个细胞的有丝分裂产生两个相同的细胞;还有西游记中孙悟空变出后孙的本领和火影忍者中鸣人的隐分身忍术等。下面就以孙悟空为例子来演示下原型模式的实现。具体的实现代码如下:
///火影忍者中鸣人的影分身和孙悟空的的变都是原型模式 class Client { static void Main(string[] args) { // 孙悟空 原型 MonkeyKingPrototype prototypeMonkeyKing = new ConcretePrototype("MonkeyKing"); // 变一个 MonkeyKingPrototype cloneMonkeyKing = prototypeMonkeyKing.Clone() as ConcretePrototype; Console.WriteLine("Cloned1:\t"+cloneMonkeyKing.Id); // 变两个 MonkeyKingPrototype cloneMonkeyKing2 = prototypeMonkeyKing.Clone() as ConcretePrototype; Console.WriteLine("Cloned2:\t" + cloneMonkeyKing2.Id); Console.ReadLine(); } } /// <summary> /// 孙悟空原型 /// </summary> public abstract class MonkeyKingPrototype { public string Id { get; set; } public MonkeyKingPrototype(string id) { this.Id = id; } // 克隆方法,即孙大圣说“变” public abstract MonkeyKingPrototype Clone(); } /// <summary> /// 创建具体原型 /// </summary> public class ConcretePrototype : MonkeyKingPrototype { public ConcretePrototype(string id) : base(id) { } /// <summary> /// 浅拷贝 /// </summary> /// <returns></returns> public override MonkeyKingPrototype Clone() { // 调用MemberwiseClone方法实现的是浅拷贝,另外还有深拷贝 return (MonkeyKingPrototype)this.MemberwiseClone(); } }
上面原型模式的运行结果为(从运行结果可以看出,创建的两个拷贝对象的ID属性都是与原型对象ID属性一样的):
上面代码实现的浅拷贝的方式,浅拷贝是指当对象的字段值被拷贝时,字段引用的对象不会被拷贝。例如,如果一个对象有一个指向字符串的字段,并且我们对该对象做了一个浅拷贝,那么这两个对象将引用同一个字符串,而深拷贝是对对象实例中字段引用的对象也进行拷贝,如果一个对象有一个指向字符串的字段,并且我们对该对象进行了深拷贝的话,那么我们将创建一个对象和一个新的字符串,新的对象将引用新的字符串。也就是说,执行深拷贝创建的新对象和原来对象不会共享任何东西,改变一个对象对另外一个对象没有任何影响,而执行浅拷贝创建的新对象与原来对象共享成员,改变一个对象,另外一个对象的成员也会改变。
介绍完原型模式的实现代码之后,下面看下原型模式的类图,通过类图来理清原型模式实现中类之间的关系。具体类图如下:
三、原型模式的优缺点
原型模式的优点有:
- 原型模式向客户隐藏了创建新实例的复杂性
- 原型模式允许动态增加或较少产品类。
- 原型模式简化了实例的创建结构,工厂方法模式需要有一个与产品类等级结构相同的等级结构,而原型模式不需要这样。
- 产品类不需要事先确定产品的等级结构,因为原型模式适用于任何的等级结构
原型模式的缺点有:
- 每个类必须配备一个克隆方法
- 配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
四、.NET中原型模式的实现
在.NET中可以很容易地通过实现ICloneable接口(这个接口就是原型,提供克隆方法,相当于与上面代码中MonkeyKingPrototype抽象类)中Clone()方法来实现原型模式,如果我们想我们自定义的类具有克隆的功能,首先定义类继承与ICloneable接口并实现Clone方法。在.NET中实现了原型模式的类如下图所示(图中只截取了部分,可以用Reflector反编译工具进行查看):
五、总结
到这里关于原型模式的介绍就结束了,原型模式用一个原型对象来指明所要创建的对象类型,然后用复制这个原型对象的方法来创建出更多的同类型对象,它与工厂方法模式的实现非常相似,其中原型模式中的Clone方法就类似工厂方法模式中的工厂方法,只是工厂方法模式的工厂方法是通过new运算符重新创建一个新的对象(相当于原型模式的深拷贝实现),而原型模式是通过调用MemberwiseClone方法来对原来对象进行拷贝,也就是复制,同时在原型模式优点中也介绍了与工厂方法的区别(第三点)。
本专题中所有源码:设计模式之原型模式
C# 深浅复制 MemberwiseClone
2018-04-16 14:20 by 天才卧龙, 2325 阅读, 1 评论, 收藏, 编辑
学无止境,精益求精
十年河东,十年河西,莫欺少年穷
学历代表你的过去,能力代表你的现在,学习代表你的将来
最近拜读了大话设计模式:原型模式,该模式主要应用C# 深浅复制来实现的!关于深浅复制大家可参考MSDN:https://msdn.microsoft.com/zh-cn/library/system.object.memberwiseclone.aspx
所谓深浅复制可解读为:
浅复制:在C#中调用 MemberwiseClone() 方法即为浅复制。如果字段是值类型的,则对字段执行逐位复制,如果字段是引用类型的,则复制对象的引用,而不复制对象,因此:原始对象和其副本引用同一个对象!
深复制:如果字段是值类型的,则对字段执行逐位复制,如果字段是引用类型的,则把引用类型的对象指向一个全新的对象!
上述的解释可能看不太懂,我们作如下案例进行分析:
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
class Program { public static void Main() { //创建P1对象 Person p1 = new Person(); p1.Age = 42; p1.Name = "Sam"; p1.IdInfo = new IdInfo("081309207"); //通过浅复制 得到P2对象 Person p2 = p1.ShallowCopy(); //分别输出 Console.WriteLine("对象P1相关属性如下"); DisplayValues(p1); //p1.Name = ""; //p1.IdInfo.IdNumber = "XXXXX"; Console.WriteLine("对象P2相关属性如下"); DisplayValues(p2); //现在测试深复制 Person p3 = p1.DeepCopy(); p1.Name = "George"; p1.Age = 39; p1.IdInfo.IdNumber = "081309208"; Console.WriteLine("对象P1相关属性如下"); DisplayValues(p1); //p1.IdInfo.IdNumber = "CCCCCCC"; Console.WriteLine("对象P3相关属性如下"); DisplayValues(p3); Console.Read(); } public static void DisplayValues(Person p) { Console.WriteLine(" Name: {0:s}, Age: {1:d}", p.Name, p.Age); Console.WriteLine(" Value: {0:d}", p.IdInfo.IdNumber); } } public class IdInfo { public string IdNumber; public IdInfo(string IdNumber) { this.IdNumber = IdNumber; } } public class Person { public int Age; public string Name; public IdInfo IdInfo; public Person ShallowCopy() { return (Person)this.MemberwiseClone(); } public Person DeepCopy() { Person other = (Person)this.MemberwiseClone(); other.IdInfo = new IdInfo(IdInfo.IdNumber); other.Name = String.Copy(Name); return other; } }
上述代码分析如下:
原始对象P1,通过浅复制得到对象P2,通过深复制得到P3
原始对象P1中的值类型属性有:Age 和 Name ,引用类型对象有:IdInfo
根据上述浅复制的概念可知:P2中的Age 和 Name 相对于 P1是全新的,但P2中的 IdInfo 和 P1中的 IdInfo 是同一个对象,二者同在一个内存地址!
根据上述深复制的概念可知:P3中的Age 和 Name 相对于 P1是全新的,但P3中的 IdInfo 和 P1中的 IdInfo 不是同一个对象,也就是说 P3中的IdInfo是一个全新的对象,开辟了自己的内存地址!
上述代码测试如下:
我们现在讲代码修改如下:
public static void Main() { //创建P1对象 Person p1 = new Person(); p1.Age = 42; p1.Name = "Sam"; p1.IdInfo = new IdInfo("081309207"); //通过浅复制 得到P2对象 Person p2 = p1.ShallowCopy(); //分别输出 Console.WriteLine("对象P1相关属性如下"); DisplayValues(p1); p1.Name = "浅复制中,修改了P1的Name属性,但Name是值类型,所以不会影响P2"; p1.IdInfo.IdNumber = "浅复制中,修改了P1的IdInfo属性,但IdInfo是引用类型,所以会影响P2 (浅复制中引用类型原始对象和副本指向同一内存地址)"; Console.WriteLine("对象P2相关属性如下"); DisplayValues(p2); Console.Read(); }
在输出P2之前,我们修改了P1对象的值类型Name 和 引用类型 IdInfo 。
无论是浅复制还是深复制,副本中的值类型都是全新的!
浅复制中原始对象和副本的引用类型指向同一内存地址,所以,修改了P1的IdInfo会同时影响P2的IdInfo
输出如下:
继续修改代码,如下:
public static void Main() { //创建P1对象 Person p1 = new Person(); p1.Age = 42; p1.Name = "Sam"; p1.IdInfo = new IdInfo("081309207"); //现在测试深复制 Person p3 = p1.DeepCopy(); p1.Name = "George"; p1.Age = 39; p1.IdInfo.IdNumber = "081309208"; Console.WriteLine("对象P1相关属性如下"); DisplayValues(p1); p1.IdInfo.IdNumber = "深复制中,修改了P1的IdInfo属性,即使IdInfo是引用类型,也不会影响P3 (深复制中引用类型原始对象和副本分别指向不同的内存地址)"; Console.WriteLine("对象P3相关属性如下"); DisplayValues(p3); Console.Read(); }
深复制中原始对象和副本的引用类型指向各自的地址,两者完全是两个不同的对象!
因此:修改P1不会影响P3
so,是不是很简单,是不是很Easy.
深浅复制主要用于当创建一个对象需要消耗过多资源时,可以采取复制的方法提升效率!
大话设计模式的原话是这样滴:当你New一个对象时,每New一次,都需要执行一个构造函数,如果构造函数的执行时间很长,那么多次New对象时会大大拉低程序执行效率,因此:一般在初始化信息不发生变化的前提下,克隆是最好的办法,这既隐藏了对象的创建细节,又大大提升了性能!
当然,如果每个类都要写自己的深复制,这岂不是非常非常麻烦,因此,有一个通用的深复制方法,如下:
/// <summary> /// 通用的深复制方法 /// </summary> /// <typeparam name="T"></typeparam> [Serializable] public class BaseClone<T> { public virtual T Clone() { MemoryStream memoryStream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(memoryStream, this); memoryStream.Position = 0; return (T)formatter.Deserialize(memoryStream); } }
相关案例如下(通用的深复制方法使用时必须为相关类及类的引用类型加上可序列化标识:[Serializable]):
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
class Program { public static void Main() { //创建P1对象 Person p1 = new Person(); p1.Age = 42; p1.Name = "Sam"; p1.IdInfo = new IdInfo("081309207"); //现在测试深复制 Person p3 = p1.Clone(); p1.Name = "George"; p1.Age = 39; p1.IdInfo.IdNumber = "081309208"; Console.WriteLine("对象P1相关属性如下"); DisplayValues(p1); p1.IdInfo.IdNumber = "深复制中,修改了P1的IdInfo属性,即使IdInfo是引用类型,也不会影响P3 (深复制中引用类型原始对象和副本分别指向不同的内存地址)"; Console.WriteLine("对象P3相关属性如下"); DisplayValues(p3); Console.Read(); } public static void DisplayValues(Person p) { Console.WriteLine(" Name: {0:s}, Age: {1:d}", p.Name, p.Age); Console.WriteLine(" Value: {0:d}", p.IdInfo.IdNumber); } } [Serializable] public class IdInfo { public string IdNumber; public IdInfo(string IdNumber) { this.IdNumber = IdNumber; } } [Serializable] public class Person : BaseClone<Person> { public int Age; public string Name; public IdInfo IdInfo; public Person ShallowCopy() { return (Person)this.MemberwiseClone(); } public Person DeepCopy() { Person other = (Person)this.MemberwiseClone(); other.IdInfo = new IdInfo(IdInfo.IdNumber); other.Name = String.Copy(Name); return other; } public override Person Clone() { MemoryStream memoryStream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(memoryStream, this); memoryStream.Position = 0; return (Person)formatter.Deserialize(memoryStream); } } /// <summary> /// 通用的深复制方法 /// </summary> /// <typeparam name="T"></typeparam> [Serializable] public class BaseClone<T> { public virtual T Clone() { MemoryStream memoryStream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(memoryStream, this); memoryStream.Position = 0; return (T)formatter.Deserialize(memoryStream); } }
@陈卧龙的博客
做了个测试
使用MemberwiseClone();后
它引用的地址都变了。为什么还叫浅拷贝?
第一张图是直接将对象的堆地址的内存编号的二进制表示扔给了cmk和cmk2,这个地址引用是存储在栈上的,所以三个变量都指向了栈上的同一个内存编号,第二张图是浅拷贝,也即,是把堆地址的二进制表示拷贝一份放在cmk和cmk2各自在栈上的内存编号中,所以,编号不一样。
每个类都需要一个克隆方法,这句话有问题。其实有通用的,博主不知道而已!自己百度吧!或者参考鄙人的博客:http://www.cnblogs.com/chenwolong/p/MemberwiseClone.html 深浅复制。当然,我在写原型模式博客时,采用了博主的孙悟空案例!自己懒得写了!