浅、深复制以及原型模式
文章的知识梳理顺序:
首先比较了值类型赋值操作与引用类型的赋值操作的不同,接着讨论对于包含引用类型的值(或引用)类型进行赋值操作的情况,引出浅复制的概念,进一步思考如何进行深复制,介绍实现深复制的两种方式:1、 实现ICloneable接口 2、序列化的方法,最后介绍设计模式中的原型模式(Prototype)。
值类型的赋值操作:
首先新建一个结构体 Point:
struct Point { // 结构的字段 public int X; public int Y; // 带参数的构造函数 // 结构体中不允许定义无参的构造函数 public Point(int x, int y) { X = x; Y = x; } // 重写 ToString() 方法 public override string ToString() { return string.Format("X: {0}, Y: {1}", X, Y); } }
在 Main() 方法中写入以下代码:
static void Main(string[] args) { Point p = new Point(2, 2); Point p1 = p; Console.WriteLine(p); Console.WriteLine(p1); // 更改 p1.X 的值 Console.WriteLine("\np1.X 被更改为 5"); p1.X = 5; Console.WriteLine(p); Console.WriteLine(p1); Console.ReadKey(); }
输出:
X: 2, Y: 2 X: 2, Y: 2 p1.X 被更改为 5 X: 2, Y: 2 X: 5, Y: 2
p1.X 值的改变并没有影响到 p.X 的值。
原因是:当把一个值类型赋值给另一个值类型时,就是对字段成员逐一进行复制。对于 System.Int32 这样的简单数据类型,唯一需要复制的成员就是数值。然而,对于我们上面定义的 Point ,X和Y值都会被复制到新的结构变量中。因此修改其中一个结构变量的字段值并不会引起另一个结构变量的值的改变。
引用类型赋值:
现在我们新建一个类类型 PointRef,只需更改上面结构体的构造函数的名字,代码如下:
class PointRef { // 结构的字段 public int X; public int Y; public PointRef(int x, int y) { X = x; Y = x; } // 重写 ToString() 方法 public override string ToString() { return string.Format("X: {0}, Y: {1}", X, Y); } }
接着更改 Main() 方法:
static void Main(string[] args) { PointRef p = new PointRef(2, 2); PointRef p1 = p; Console.WriteLine(p); Console.WriteLine(p1); // 更改 p1.X 的值 Console.WriteLine("\np1.X 被更改为 5"); p1.X = 5; Console.WriteLine(p); Console.WriteLine(p1); Console.ReadKey(); }
输出:
X: 2, Y: 2 X: 2, Y: 2 p1.X 被更改为 5 X: 5, Y: 2 X: 5, Y: 2
与上面的输出结果对比可以发现,这次 p.X 的值被改变了。
原因是:引用类型在进行赋值操作时,赋的是引用,也就是将p在托管堆上的引用赋给p1,即p和p1指向托管堆中的同一个对象,因此无论是对p或p1的字段值进行更改,都会引起托管堆上对象的字段值发生更改,继而导致另一个引用发生更改。(关于托管堆:值类型变量和值均分配在内存的栈上;引用类型变量分配在栈上,而实际的值分配在托管堆上,变量中存放的是指向值的地址。)
包含引用类型的值类型:
如果在值类型中包含引用类型,这时对值类型进行赋值操作,会出现什么结果?赋的是值还是引用?
为了演示,首先更改上面的 PointRef 类:
class PointRef { public string pointName; public PointRef(string name) { pointName = name; } }
接着添加一个新的结构 Line:
struct Line { // 结构的字段 public int length; // 包含一个引用类型的成员 public PointRef point; public Line(int len, string name) { length = len; point = new PointRef(name); } public override string ToString() { return string.Format("Line->length: {0},PointRef->pointName: {1}", length, point.pointName); } }
在结构体 Line 中我们引用了 PointRef类型的成员point。接着我们在 Main() 方法中试着对 Line 进行赋值操作观察结果:
static void Main(string[] args) { Line line = new Line(10, "No-Name"); Line newLine = line; Console.WriteLine(line); Console.WriteLine(newLine); Console.WriteLine("\n=>更改 length 值为15,pointName 的值为 MyPoint"); newLine.length = 15; newLine.point.pointName = "MyPoint"; Console.WriteLine(line); Console.WriteLine(newLine); Console.ReadKey(); }
我们创建两个Line的实例line和newLine,它们的内部分别包含着Point类型的一个实例,接着我们对 newLine 的length字段和内部point实例的pointName字段进行更改。
输出:
Line->length: 10,PointRef->pointName: No-Name Line->length: 10,PointRef->pointName: No-Name =>更改 length 值为15,pointName 的值为 MyPoint Line->length: 10,PointRef->pointName: MyPoint Line->length: 15,PointRef->pointName: MyPoint
观察输出结果中的最后两行代码,会发现对newLine 的更改操作,只在pointName字段体现出来,而length的值并未发生改变。pointName 是Point类中的成员。
原因是:默认情况下,当值类型包含其它引用类型时,赋值将生成一个引用的副本,如上题中生成两个独立的的结构体,但每个结构体都包含指向内存中同一个对象的引用,即它们都指向同一个 point 对象。这种复制也叫做浅复制。
浅复制之 MemberwiseClone() 成员的使用:
System.Object 定义了一个名为 MemberwiseClone() 的成员。这个方法的定义如下:如果字段是值类型的,则对该字段执行逐位复制,如果字段是引用类型,则复制引用但不复制引用的对象,因此,原始对象及其副本引用同一对象。【此处所指的引用类型是对象引用类型,string 类型依然会进行逐位复制。】
.Net 中提供了 ICloneable 接口,我们只需实现其的 Clone() 方法就能完成对象复制。
对结构体 Line 进行更改,让它实现 ICloneable 接口,并实现 Clone() 方法:
struct Line:ICloneable { // 结构的字段 public int length; // 包含一个引用类型的成员 public PointRef point; public Line(int len, string name) { length = len; point = new PointRef(name); } // 实现 ICloneable 的 Clone() 方法 public object Clone() { // 浅复制 return this.MemberwiseClone(); } public override string ToString() { return string.Format("Line->length: {0},PointRef->pointName: {1}", length, point.pointName); } }
在客户端做如下修改:
static void Main(string[] args) { Line line = new Line(10, "No-Name"); Line newLine = (Line)line.Clone(); Console.WriteLine(line); Console.WriteLine(newLine); Console.WriteLine("\n=>更改 length 值为15,pointName 的值为 MyPoint"); newLine.length = 15; newLine.point.pointName = "MyPoint"; Console.WriteLine(line); Console.WriteLine(newLine); Console.ReadKey(); }
注意:Line newLine = (Line)line.Clone(); 这行代码实现了克隆。
输出:
Line->length: 10,PointRef->pointName: No-Name Line->length: 10,PointRef->pointName: No-Name =>更改 length 值为15,pointName 的值为 MyPoint Line->length: 10,PointRef->pointName: MyPoint Line->length: 15,PointRef->pointName: MyPoint
这样,我们就使用了 MemberwiseClone() 方法实现了浅复制。虽然不使用这个方法我们也能进行浅复制,但是,Clone() 方法不仅仅能够进行浅复制,我们还可以更改逻辑代码来实现深复制。
深复制:
很多时候我们不仅仅是复制对象本身,还想在克隆过程中创建任何引用类型变量的新实例,实现真正的深复制。
1、Clone() 方法实现深复制
修改 Clone() 方法如下:
public object Clone() { // 深复制 Line line = (Line)this.MemberwiseClone(); PointRef newPoint = new PointRef(this.point.pointName); line.point = newPoint; return line; }
方法首先对Line 对象进行一次浅复制,接着将创建的新的 PointRef 对象 newPoint,并将赋值给Line对象的 point 属性,然后返回 line.这样我们就完整的克隆了对象line。
输出:
Line->length: 10,PointRef->pointName: No-Name Line->length: 10,PointRef->pointName: No-Name =>更改 length 值为15,pointName 的值为 MyPoint Line->length: 10,PointRef->pointName: No-Name Line->length: 15,PointRef->pointName: MyPoint
这次,我们发现最后输出的最后一行中 pointName 已被更改为 MyPoint,而原对象的 pointName 则未发生改变。这样,我们就实现了深复制。
2、以序列化的方式实现深复制
当一个对象被持久化到流时,所有的相关数据(基类,包含的对象等)也会被自动序列化,因此当对象被序列化时,CLR 会处理所有相关的对象。鉴于此,我们可以借助这种方式来进行深复制。
首先引用相应的命名空间:
using System.Runtime.Serialization.Formatters.Binary; using System.IO;
接着在 结构体 Line 和 类 PointRef 上使用特性 [Serializable](将此特性放置在结构体和类的定义之前即可),只有被此特性标识的对象才能被序列化。
修改 Clone() 方法,如下:
public object Clone() { // 序列化的方式进行深复制 BinaryFormatter formatter = new BinaryFormatter(); MemoryStream ms = new MemoryStream(); formatter.Serialize(ms, this); ms.Position = 0; return formatter.Deserialize(ms); }
输出:
Line->length: 10,PointRef->pointName: No-Name Line->length: 10,PointRef->pointName: No-Name =>更改 length 值为15,pointName 的值为 MyPoint Line->length: 10,PointRef->pointName: No-Name Line->length: 15,PointRef->pointName: MyPoint
这样,以序列化的方式实现深复制就完成了。
原型模式(Prototype)
原型模式,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的结构图:
原型模式的示例代码如下:
using System; namespace Prototype.Structural { /// <summary> /// MainApp startup class for Structural /// Prototype Design Pattern. /// </summary> class MainApp { /// <summary> /// Entry point into console application. /// </summary> static void Main() { // Create two instances and clone each ConcretePrototype1 p1 = new ConcretePrototype1("I"); ConcretePrototype1 c1 = (ConcretePrototype1)p1.Clone(); Console.WriteLine("Cloned: {0}", c1.Id); ConcretePrototype2 p2 = new ConcretePrototype2("II"); ConcretePrototype2 c2 = (ConcretePrototype2)p2.Clone(); Console.WriteLine("Cloned: {0}", c2.Id); // Wait for user Console.ReadKey(); } } /// <summary> /// The 'Prototype' abstract class /// </summary> abstract class Prototype { private string _id; // Constructor public Prototype(string id) { this._id = id; } // Gets id public string Id { get { return _id; } } public abstract Prototype Clone(); } /// <summary> /// A 'ConcretePrototype' class /// </summary> class ConcretePrototype1 : Prototype { // Constructor public ConcretePrototype1(string id) : base(id) { } // Returns a shallow copy public override Prototype Clone() { return (Prototype)this.MemberwiseClone(); } } /// <summary> /// A 'ConcretePrototype' class /// </summary> class ConcretePrototype2 : Prototype { // Constructor public ConcretePrototype2(string id) : base(id) { } // Returns a shallow copy public override Prototype Clone() { return (Prototype)this.MemberwiseClone(); } } }
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。
因为.Net 中提供了 ICloneable 接口,所以这里我们无需抽象出 Prototype 类,而是直接实现 ICloneable 中的 Clone() 方法来创建一个原型模式的实例。
为了更好的从形式上与结构图相似,将 Line 由结构体(struct) 更改为类(class),将类 PointRef 实现 ICloneable 接口,并实现 Clone() 方法。这里的 Line 类和 PointRef 类对应于图中的 ConcretePrototype1 和 ConcretePrototype2,ICloneable 对应于抽象类 Prototype。代码如下:
class Line : ICloneable { // 结构的字段 public int length; // 包含一个引用类型的成员 public PointRef point; public Line(int len, string name) { length = len; point = new PointRef(name); } // 实现 ICloneable 的 Clone() 方法 public object Clone() { // 深复制 Line line = (Line)this.MemberwiseClone(); PointRef newPoint = (PointRef)point.Clone(); line.point = newPoint; return line; } public override string ToString() { return string.Format("Line->length: {0},PointRef->pointName: {1}", length, point.pointName); } } class PointRef:ICloneable { public string pointName; public PointRef(string name) { pointName = name; } public object Clone() { return this.MemberwiseClone(); } } class Program { static void Main(string[] args) { Line line = new Line(10, "No-Name"); Line newLine = (Line)line.Clone(); Console.WriteLine(line); Console.WriteLine(newLine); Console.WriteLine("\n=>更改 length 值为15,pointName 的值为 MyPoint"); newLine.length = 15; newLine.point.pointName = "MyPoint"; Console.WriteLine(line); Console.WriteLine(newLine); Console.ReadKey(); } }
这样,我们就完成了原型模式的实例。