標籤:
淺拷貝和深拷貝
有兩種對象複製的方法:淺拷貝和深拷貝。淺拷貝只是複製引用,而不會複製引用的對象。深拷貝會複製引用的對象。
因此,原始對象中的引用和淺拷貝對象中的同一個引用都指向同一個對象。而深拷貝的對象包含了對象的一切直接或間接的引用。參看維基百科(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)建立了一個自訂的複製架構。這個架構能夠建立對象及其引用的對象的深拷貝,不管對象結構有多複雜。缺點是,這是一個不提供原始碼的自訂 架構(更新:現在已經包括原始碼了,參見本文評論),並且它不能在不使用無參數構造器的時候,拷貝由私人方法建立的對象。另一個問題,也是所有自動化的深 複製方法共有的問題是,深拷貝通常需要靈活地處理不能進行簡單自動化特殊情況(例如未受管理的資源)。
C#對象複製介紹