c#的object類提供了一個淺拷貝的方法MemberwiseClone,該方法克隆的是原始對象的獨立副本,但是這個副本和原始對象會共同指向同一個引用類型屬性的引用。比如官方提供了一個例子:
public class IdInfo
{
public int IdNumber;
public IdInfo(int IdNumber)
{
this.IdNumber = IdNumber;
}
}
public class Person
{
public int Age;
public string Name;
public IdInfo IdInfo;
public Person ShallowCopy()
{
return (Person) this.MemberwiseClone();
}
public Person DeepCopy()
{
Person other = (Person) this.MemberwiseClone();
other.IdInfo = new IdInfo(IdInfo.IdNumber);
other.Name = String.Copy(Name);
return other;
}
}
如果你對Person類的某個實例對象person1使用MemberwiseClone方法進行淺拷貝得到person2,那麼person1和person2的IdInfo指向的是同一個引用。
MemberwiseClone 方法創建一個淺表副本,方法是創建一個新的對象,然後將當前對象的非靜態字段複製到新的對象。 如果字段是值類型,則執行字段的逐位副本。 如果字段是引用類型,則會複製引用,但不會複製引用的對象;因此,原始對象及其複本引用相同的對象。
同時,官方文檔也提供了四種深拷貝的方案:
-
調用要複製的對象的類構造函數,以創建具有從第一個對象獲取的屬性值的第二個對象。 這假設對象的值由其類構造函數完全定義。
-
調用 MemberwiseClone 方法來創建對象的淺表副本,然後將原始對象的屬性深拷貝到新對象裏面。
-
序列化要深層複製的對象,然後將序列化的數據還原到其他對象變量。
-
使用帶有遞歸的反射來執行深層複製操作。
第一種和第二種方法,本質都是根據類的結構寫特定的拷貝方法。第三種和第四種方法則相對通用許多。
1.反射方式:
public T DeepCopyByReflection<T>(T obj)
{
Type type = obj.GetType();
if (obj is string || type.IsValueType) return obj;
if (type.IsArray)
{
Type elementType = Type.GetType(type.FullName.Replace("[]", string.Empty));
var array = obj as Array;
Array copied = Array.CreateInstance(elementType, array.Length);
for (int i = 0; i < array.Length; i++)
{
copied.SetValue(DeepCopyByReflection(array.GetValue(i)), i);
}
return (T)Convert.ChangeType(copied, obj.GetType());
}
object retval = Activator.CreateInstance(obj.GetType());
var fields = obj.GetType().GetFields(BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
var val = field.GetValue(obj);
if (val == null)
continue;
field.SetValue(retval, DeepCopyByReflection(val));
}
return (T)retval;
}
上面的代碼是一種通過反射來進行深拷貝的方法,它的本質是遞歸一個類,把所有的字段通過反射的方式重新賦值給新的類。這裏需要注意的是要使用GetFields而不是GetProperties方法來獲取目標類裏面的所有的字段,因爲GetProperties方法只能獲取到屬性,對於私有的字段,無法獲取到,這樣,深拷貝的時候就會漏掉這個私有字段。
它會調用其中一個構造方法,所以當你使用這個方法的時候,要特別注意你的那個無參構造方法裏面的功能邏輯代碼,要考慮執行了這個構造方法之後,會產生哪些後果。
另外這個時候如果你的類裏面包含一個Action,由於這個Action沒有無參構造方法,你再用這個克隆方法就會變得很麻煩。實際上不止Action,只要被克隆的對象裏面包含一個無參構造方法的實體對象,那麼用這種反射的方式克隆就會變得很麻煩,因爲需要在實例化的時候給構造方法傳遞參數。
另外,針對循環引用的問題,這個方法解決起來也會很棘手。
2.序列化方式:
// 利用XML序列化和反序列化實現
public T DeepCopyWithXmlSerializer<T>(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
XmlSerializer xml = new XmlSerializer(typeof(T));
xml.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
retval = xml.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
// 利用二進制序列化和反序列實現,支持循環引用
public T DeepCopyWithBinarySerialize<T>(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
// 序列化成流
bf.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
// 反序列化成對象
retval = bf.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
// 利用DataContractSerializer序列化和反序列化實現
public T DeepCopy<T>(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
DataContractSerializer ser = new DataContractSerializer(typeof(T));
ser.WriteObject(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
retval = ser.ReadObject(ms);
ms.Close();
}
return (T)retval;
}
序列化相對來說是一種不錯的方式,因爲它解決了無參構造方法的克隆問題,同時它也不會導致在克隆的時候調用構造函數,因爲克隆多調用一次構造函數,其實是我們不太希望的結果。
3.表達樹方式(Expression Trees):
這種方式的本質是動態生成一段克隆代碼,然後把被克隆對象的屬性逐個拷貝給新的對象。github上有一個開源項目,採用的就是這種方式。https://github.com/MarcinJuraszek/CloneExtensions
這個項目會根據你要拷貝的對象,利用表達樹自動生成深拷貝的代碼塊,幫你拷貝一個對象。它目前的問題是無法深拷貝私有變量。但是它解決了循環引用的問題。