浅拷贝和深拷贝
有两种对象克隆的方法:浅拷贝和深拷贝。浅拷贝只是复制引用,而不会复制引用的对象。深拷贝会复制引用的对象。
因此,原始对象中的引用和浅拷贝对象中的同一个引用都指向同一个对象。而深拷贝的对象包含了对象的一切直接或间接的引用。参看维基百科(http://en.wikipedia.org/wiki/Object_copy)来获得更多解释。
ICloneable接口
ICloneable接口包含一个Clone方法,可以用来创建当前对象的拷贝。
public interface ICloneable
{
object Clone();
}
ICloneable的问题是Clone方法并不会显式地指定是执行浅拷贝或深拷贝,因此调用者将无法确定实际情况。因此,有一些关于把ICloneable从.NET框架中淘汰的讨论。MSDN文档似乎暗示Clone方法是进行的深拷贝,但是文档没有明确的说明:
ICloneable接口包含一个成员方法,Clone,意在支持超过MemberWiseClone所提供的功能... MemberWiseClone进行的是浅拷贝...
类型安全的克隆
ICloneable的另一个缺点是Clone方法返回的是一个对象,因此每次调用Clone都要进行一次强制类型转换。
Person joe = new Person();
joe.Name = "Joe Smith";
Person joeClone = (Person)joe.Clone();
一种可以避免进行强制类型转换的方式是提供你自己的类型安全的Clone方法。注意,你依然要提供ICloneable.Clone方法的以满足iCloneable接口的要求。
public class Person : ICloneable
{
public string Name;
object ICloneable.Clone()
{
return this.Clone();
}
public Person Clone()
{
return (Person)this.MemberwiseClone();
}
}
MemberwiseClone() 方法创建一个浅表副本,具体来说就是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其复本引用同一对象。
选择克隆方法
1. 手工克隆
一个能够保证对象完全按照你所想的那样进行克隆的方式是手工克隆对象的每一个域(field)。这种方式的缺点是麻烦而且容易出错:如果你在类中增 加了一个域,你很可能会忘记更新Clone方法。还要在克隆引用对象指向原始对象的时候,注意避免无限循环引用。下面是一个进行深拷贝的简单例子:
public class Person : ICloneable
{
public string Name;
public Person Spouse;
public object Clone()
{
Person p = new Person();
p.Name = this.Name;
if (this.Spouse != null)
p.Spouse = (Person)this.Spouse.Clone();
return p;
}
}
2. 使用MemberWiseClone方法
MemberWiseClone是Object类的受保护方法,能够通过创建一个新对象,并把所有当前对象中的非静态域复制到新对象中,从而创建一 个浅拷贝。对于值类型的域,进行的是按位拷贝。对于引用类型的域,引用会被赋值而引用的对象则不会。因此,原始对象及其克隆都会引用同一个对象。注意,这 种方法对派生类都是有效的,也就是说,你只需在基类中定义一次Clone方法。下面是一个简单的例子:
public class Person : ICloneable
{
public string Name;
public Person Spouse;
public object Clone()
{
return this.MemberwiseClone();
}
}
3. 用反射进行克隆
用反射进行克隆是使用Activator.CreateInstance方法来创建一个相同类型的新对象,然后用反射对所有域进行浅拷贝。这种方法 的优点是它是全自动的,不需要在对象中添加或删除成员的时候修改克隆方法。另外它也能被写成提供深拷贝的方法。缺点是使用了反射,因此会比较慢,而且在部 分受信任的环境中是不可用的。示例代码
4. 使用序列化进行克隆
克隆一个对象的最简单的方法是将它序列化并立刻反序列化为一个新对象。和反射方法一样,序列化方法是自动的,无需在对对象成员进行增删的时候做出修 改。缺点是序列化比其他方法慢,甚至比用反射还慢,所有引用的对象都必须是可序列化的(Serializable)。另外,取决于你所使用的序列化的类型 (XML,SOAP,二进制)的不同,私有成员可能不能像期望的那样被克隆。示例代码在这里,这里和这里。
序列化克隆的一种 System.SerializableAttribute
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace Sample
{
[Serializable]
public class Person : ICloneable
{
public string Name;
object ICloneable.Clone()
{
return this.Clone();
}
public Person Clone()
{
using(MemoryStream ms = new MemoryStream())
{
object cloneObject;
BinaryFormatter bf = new BinaryFormatter(null,new StreamingContext(StreamingContextStates.Clone));
bf.Serialize(ms,this);
ms.Seek(0,SeekOrigin.Begin);
cloneObject = bf.Deserialize(ms);
ms.Close();
return (Person)cloneObject;
}
}
}
}
5. 使用IL进行克隆
一种罕见的解决方案是使用IL(中间语言)来进行对象克隆。这种方式创建一个动态方法(DynamicMethod),获取中间语言生成器 (ILGenerator),向方法中注入代码,把它编译成一个委托,然后执行这个委托。委托会被缓存,因此中间语言只在初次克隆的时候才会生成,后续的 克隆都不会重新生成一遍。尽管这种方法比使用反射快,但是这种方法难以理解和维护。示例代码
6. 使用扩展方法进行克隆
Havard Stranden用扩展方法(extention method)创建了一个自定义的克隆框架。这个框架能够创建对象及其引用的对象的深拷贝,不管对象结构有多复杂。缺点是,这是一个不提供源代码的自定义 框架(更新:现在已经包括源代码了,参见本文评论),并且它不能在不使用无参数构造器的时候,拷贝由私有方法创建的对象。另一个问题,也是所有自动化的深 克隆方法共有的问题是,深拷贝通常需要灵活地处理不能进行简单自动化特殊情况(例如未受管理的资源)。