原型模式( Prototype Pattern )
1 原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
Prototype:声明一个拷贝自身的接口。
ConcretePrototype:实现一个拷贝自身的操作。
Client:让一个原型拷贝自身从而创建一个新的对象。
原型模式的核心是一个 Clone()
方法,通过该方法进行对象的拷贝,net 提供了一个 ICloneable
的接口来标示这个对象是可拷贝的,为什么说是“标示”呢?首先 ICloneable
是一个接口并无具体实现,直接查看方法的注释并反编译看下源码,在 net 中具有这个标记的对象才有可能被拷贝。那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是实现 ICloneable
接口的 Clone()
方法。这种不通过 new
关键字来产生一个对象,而是通过对象复制来实现的模式就叫做原型模式。
using System;
using System.Runtime.CompilerServices;
namespace System
{
/// <summary>Supports cloning, which creates a new instance of a class with the same value as an existing instance.</summary>
[NullableContext(1)]
public interface ICloneable
{
/// <summary>Creates a new object that is a copy of the current instance.</summary>
/// <returns>A new object that is a copy of this instance.</returns>
object Clone();
}
}
2 原型模式的应用
2.1 原型模式的优点
- 性能优良
原型模式是在内存二进制流的拷贝,要比直接new
一个对象性能好,特别是要在一个循环体内产生大量的对象时,原型模式可以更好的体现其优点。 - 逃避构造函数的约束
这既是它的优点也是缺点,直接在内存中拷贝,构造函数时不会执行,优点就是减少了约束,缺点也是减少了约束,需要从实际应用时考虑。
2.2 原型模式的使用场景
- 资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 - 性能和安全要求的场景
通过new
产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 - 一个对象多个修改者的场景
一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
3 原型模式的注意事项
3.1 构造函数不会被执行
一个继承了 ICloneable
接口并实现了 Clone()
方法的类,有一个无参数构造或有参构造函数,通过 new
关键字产生了一个对象 X ,然后再通过 X.Clone()
方式拷贝了一个新对象 T ,那么在对象拷贝时构造函数是不会被执行的。咱们通过一个代码样例来验证下:
可拷贝类
public class PrototypeConstructorSample : ICloneable
{
public PrototypeConstructorSample()
{
Console.WriteLine("构造函数被执行了..");
}
public object Clone()
{
return MemberwiseClone();
}
}
在 net 中
ICloneable
、Clone()
、MemberwiseClone()
搭伙使用。MemberwiseClone():创建一个当前
Object
对象的浅度副本。
MemberwiseClone() 创建一个浅度副本,创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用类型,则复制引用但不复制引用的对象;因此,原始对象及其副本引用同一对象,也就是在内存上指向同一块内存。
场景执行程序
class Program
{
static void Main(string[] args)
{
var sample1 = new PrototypeConstructorSample();
var sample2 = sample1.Clone();
}
}
运行结果:
构造函数被执行了..
看运行结果输出只有一次证明确实通过 Clone()
拷贝的没有执行构造函数,这点从原理来讲也是可以讲得通的, Object
类的 Clone
方法的原理是从内存中(具体的说就是堆内存)以二进制流的方法进行拷贝,重写分配一个内存块,那构造函数没有被执行也是非常正确的了。
3.2 浅拷贝和深拷贝
在解释什么是浅拷贝和深拷贝之前,我们先来看个样例。
可拷贝类
public class PrototypeSample2 : ICloneable
{
/// <summary>
/// 集合值
/// </summary>
public List<string> ListValue { get; set; }
/// <summary>
/// 输出值
/// </summary>
public void ConsoleValue()
{
Console.WriteLine("---------Start");
foreach (var item in ListValue)
{
Console.WriteLine(item);
}
Console.WriteLine("---------End");
}
public object Clone()
{
return MemberwiseClone();
}
}
场景执行程序
class Program
{
static void Main(string[] args)
{
// new 一个对象
PrototypeSample2 sample1 = new PrototypeSample2();
// 给对象中属性赋值
sample1.ListValue = new List<string>() { "A" };
// 通过 Clone() 拷贝一个新对象并显性转换
PrototypeSample2 sample2 = (PrototypeSample2)sample1.Clone();
// 再次赋值
sample2.ListValue.Add("B");
// 输出 sample1 属性 ListValue 的值
sample1.ConsoleValue();
}
}
运行结果:
---------Start
A
B
---------End
看运行结果都会想,怎么 sample1
会有 B
呢?是因为 net 做了一个偷懒的拷贝动作,Object
类提供了 Clone()
方法的实现方法 MemberwiseClone()
只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝。
使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:
- 类的成员变量,而不是方法内变量;
- 必须是一个可变的引用对象,而不是一个原始类型或不可变对象。
ps:String 类型是一种特殊的引用类型(编译器做特殊处理了),在 net 原型模式使用时把其当成值类型,与 int、long、char 一致的处理即可。
现在咱们已经了解了浅拷贝,可是浅拷贝是具有风险的,那怎么才能深入的拷贝呢?ok,咱们修改下代码,如下:
拷贝操作
/// <summary>
/// 拷贝操作(深拷贝、浅拷贝)
/// </summary>
public static class Cloneable
{
/// <summary>
/// 深拷贝(二进制)
/// </summary>
/// <param name="value">待拷贝的对象</param>
public static T Clone2<T>(this T value)
{
if (!typeof(T).IsSerializable)
throw new ArgumentException("值类型必须是可序列化");
if (value == null)
return default(T);
// 声明以二进制格式序列化和反序列化的对象
var formatter = new BinaryFormatter();
// 声明一个内存流
var stream = new MemoryStream();
using (stream)
{
// 将对象序列化到指定流
formatter.Serialize(stream, value);
// 写入
stream.Seek(0, SeekOrigin.Begin);
// 流反序列化
return (T)formatter.Deserialize(stream);
}
}
}
可拷贝类
[Serializable]// 支持序列化/反序列化
public class PrototypeSample2
{
/// <summary>
/// 集合值
/// </summary>
public List<string> ListValue { get; set; }
/// <summary>
/// 输出值
/// </summary>
public void ConsoleValue()
{
Console.WriteLine("---------Start");
if (ListValue != null)
{
foreach (var item in ListValue)
{
Console.WriteLine(item);
}
}
Console.WriteLine("---------End");
}
}
场景执行程序
class Program
{
static void Main(string[] args)
{
// new 一个对象
PrototypeSample2 sample1 = new PrototypeSample2();
// 给对象中属性赋值
sample1.ListValue = new List<string>() { "A" };
// 通过 Clone2() 拷贝一个新对象并显性转换
PrototypeSample2 sample2 = sample1.Clone2();
// 再次赋值
sample2.ListValue.Add("B");
// 输出 sample1 属性 ListValue 的值
sample1.ConsoleValue();
}
}
运行结果:
---------Start
B
---------End
在浅拷贝的代码基础之上做了如下修改:
- 增加一个自定义以二进制拷贝方式的拷贝操作的工具类。
- “可拷贝类”去掉“
ICloneable
”的继承,删除了Clone()
方法的实现,增加了支持序列化/反序列化[Serializable]
的标识。 - “场景执行程序” 对
PrototypeSample2
的拷贝使用自定义工具类的Clone2()
的二进制拷贝方法。
该工具类的拷贝方法就实现了完全的拷贝,两个对象之间没有任何的瓜葛,你修改你的,我修改我的,不相互影响,在内存中也是两个块内存,这种拷贝就叫做深拷贝。