原型模式是一种很简单也是很常见的一种模式,用一句来概括就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
举一个简单的例子,在考试后我们查看各个参考人员的试卷。一般来说,同一考场同一考次的试卷是一样的(别杠,举个例子啦)。那么怎么生成每个人的试卷呢?第一种是每个人new一个各自的试卷对象,如果试卷有千百万份呢,这时候new的性能就不太乐观了。所以有了更好的性能解决方案,首先创建一个人试卷对象,然后直接通过拷贝,在拷贝之后重新设置试卷的分数,姓名,答题答案不就行了吗?
既然是原型模式,肯定有一个或多个类来提供原型。在本案例中,我们来构建一个简单的试卷类。
public class Paper
{
public string Name { get; set; }
public int Source { get; set; } = -1;
public Subject Subject { get; set; }
public void Info()
{
if (string.IsNullOrEmpty(Name) || Source < 0 || Subject == null)
throw new Exception("啊哈,这张是空白试卷。");
Console.WriteLine($"{Name}得了{Source}分,题目{Subject.Name}的答案是:{Subject.Answer}");
}
/// <summary>
/// 浅拷贝
/// </summary>
/// <returns></returns>
public Paper ShallowCopy()
{
return (Paper)this.MemberwiseClone();
}
/// <summary>
/// 深拷贝
/// </summary>
/// <returns></returns>
public Paper DeepCopy()
{
return DeepCopy(this);
}
private T DeepCopy<T>(T obj)
{
if (obj is string || obj.GetType().IsValueType)
return obj;
object retval = Activator.CreateInstance(obj.GetType());
FieldInfo[] fields = obj.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
foreach (FieldInfo field in fields)
{
try { field.SetValue(retval, DeepCopy(field.GetValue(obj))); }
catch { }
}
return (T)retval;
}
}
/// <summary>
/// 试题
/// </summary>
public class Subject
{
public string Name
{
get
{
return "1 + 1 = ?";
}
}
public string Answer { get; set; }
}
可以看到,Paper中包含了试卷人名称,分数以及一道试题。我们通过调用Info方法可以查看试卷的信息,通过调用xxxCopy来进行试卷的复制。
下面是调用场景:
/// <summary>
/// 原型模式
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
Paper paperXiaoHone = new Paper()
{
Name = "小红",
Source = 90,
Subject = new Subject()
{
Answer = "2",
}
};
Console.WriteLine("=======复制前=========");
paperXiaoHone.Info();
//深拷贝
//Paper paperXiaoming = paperXiaoHone.DeepCopy();
//浅拷贝
Paper paperXiaoming = paperXiaoHone.ShallowCopy();
paperXiaoming.Name = "小明";
paperXiaoming.Source = 85;
paperXiaoming.Subject.Answer = "1";
Console.WriteLine();
Console.WriteLine("=======复制后=========");
paperXiaoHone.Info();
paperXiaoming.Info();
Console.ReadKey(false);
}
现在来看看小红和小明的试卷信息。
通过上面简单的例子可以看出来,原型模式使用很简单,只需要在模板类中定义一个Copy方法,通过调用Copy方法进行克隆即可。但是在克隆的时候特别需要注意的是对象拷贝分为深拷贝和浅拷贝,浅拷贝是将对象中的所有字段复制到新的对象(副本)中。其中,值类型字段的值被复制到副本中后,在副本中的修改不会影响到源对象对应的值。而引用类型的字段被复制到副本中的是引用类型的引用,而不是引用的对象,在副本中对引用类型的字段值做修改会影响到源对象本身。深拷贝则是创建一个完全独立的副本,无论修改值类型还是引用类型都不会影响源对象本身。
在C#中,浅拷贝可以直接通过Object类中的MemberwiseClone方法进行拷贝。而深拷贝则要复杂一些,需要通过自行重新创建引用对象、序列化或者反射等方式去拷贝源对象。
我们用上面的例子来进行一个简单的测试。首先是浅拷贝:
可以看到,在修改小明的试卷答案(Subject对象)后,小红的答案也随之变好了。说明拷贝对象(小明试卷)只拷贝了源对象(小红试卷)中Subject对象的引用地址,导致修改修改Subject对象后,源对象中Subject也随之改变了。虽然在C#中string为引用类型,但从运行结果来看,string在拷贝对象中依然是独立存在的,这是因为string在C#中是不可变数据类型(参考链接3)。
然后再来测试一下深拷贝。
可以看到,通过深拷贝克隆出来的对象是完全独立的一个对象,修改其中的引用类型变量也不会影响到源对象。
注意:浅拷贝和深拷贝中的代码都在上面demo中,这里深拷贝用的方法时反射。
另外,仔细的观众朋友也发现了运行结果中每一句话前面的数字,这是我在构造函数中通过Random生成的试卷编号。可以发现,不管是源对象还是拷贝对象,这个值都没有变化。这也是对象拷贝中需要注意到的地方,拷贝时构造函数不会被执行。这是原型模式的缺点,同时也是原型模式的优点。正是因为这种内存拷贝方式,使得类创建性能大大增强,特别体现在循环中大量创建对象时(不同方式的深拷贝性能不同,有的甚至比new一个新对象的性能还差)。
代码下载:点击下载
参考链接:
1.对象克隆(C# 快速高效率复制对象另一种方式 表达式树转)
2.Object.MemberwiseClone 方法
3.探索c#之不可变数据类型
文章评论