0. 一般使用狀況(無使用任何深層複製)
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source;
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("Source" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果(原始資料被更動了) */
//Source Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
1. 使用 BinaryFormatter 複製
優點: 幾乎可完美處理任何複雜的物件,網路上查到的大多也都是這個方式
缺點: 執行時間較其他方式長,複製對象 Class 必需標記 [Serializable] 標籤有點麻煩
先建立一個擴充方法如下
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public static class CommonExtensions
{
/// <summary>
/// 深層複製(複製對象必需可序列化)
/// </summary>
/// <typeparam name="T">複製對象類別</typeparam>
/// <param name="source">複製對象
/// <returns>複製出的物件</returns>
public static T DeepClone<T>(this T source)
{
if (!typeof(T).IsSerializable)
{
throw new ArgumentException("The type must be serializable.", nameof(source));
}
if (source != null)
{
using (MemoryStream stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, source);
stream.Seek(0, SeekOrigin.Begin);
return (T)formatter.Deserialize(stream);
}
}
return default(T);
}
}
使用方式如下
[Serializable]
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source.DeepClone();
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("Source" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果(原始資料不變) */
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Love, Funny
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
2. 使用 Newtonsoft.Json 套件
優點: 輕巧、簡單、速度比 BinaryFormatter 快上不少,基本上不知道要挑哪一個方式又想自己寫時,我會傾向建議使用此方式
缺點: Private 欄位無法被複製到,遇到循環參考會出錯(解決方式可參考這篇)
建立一個擴充方法如下
using System;
using Newtonsoft.Json;
public static class CommonExtensions
{
/// <summary>
/// 深層複製
/// </summary>
/// <typeparam name="T">複製對象類別</typeparam>
/// <param name="source">複製對象
/// <returns>複製出的物件</returns>
public static T DeepCloneByJson<T>(this T source)
{
if (Object.ReferenceEquals(source, null))
{
return default(T);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}
}
使用方式如下
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source.DeepCloneByJson();
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("Source" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果(原始資料不變) */
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Love, Funny
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
3. 使用 Expression Trees 方式(參考來源)
優點: 速度又比第二種方式更快,也可處理 Private 欄位、循環參考的問題
缺點: Framework 需 .NET 4 以上,我有遇到當型態是 IEnumerable<T>
時,會跳出錯誤(目前我是先轉 List<T>
型態解決),除此之外,還沒遇到其它問題
由於該擴充方法的程式碼有點冗長,所以這邊直接提供原始碼檔案下載,然後使用方式參考下面
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source.DeepCopyByExpressionTree();
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("Source Object" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone Object" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果(原始資料不變) */
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Love, Funny
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
4. 在 Class 裡手動撰寫 DeepCopy 和 ShallowCopy 方法
優點: 速度最快,假設有其他更快的方式,請告知我,感恩
缺點: 必須撰寫較多的程式碼,後續維護較麻煩且容易有人為上的疏失(假如你夠細心的話可無視)
ShallowCopy 微軟提供了內建方法Object.MemberwiseClone()
供呼叫,和 DeepCopy 的差意主要是前者只複製實值(Value)型別的欄位,如果是參考(reference)型別的欄位,則只複製該物件欄位的參考位址,所以該欄位的值還是屬於和大家共用的狀態,至於什麼是參考型別還實值型別,由於不在本篇討論的範圍,有興趣的讀者可以自行參考這兩篇文章:一、Value Type(實值型別) vs Reference Type(參考型別)、二、實值型別與參考型別的記憶體配置
這邊分別比較使用這兩種方式的結果
public class Item
{
public Item ShallowCopy()
{
return (Item)this.MemberwiseClone();
}
public Item DeepCopy()
{
var other = (Item)this.MemberwiseClone();
other.Name = String.Copy(Name);
other.Features = new List<string>();
foreach (var m in Features)
{
other.Features.Add(m);
}
return other;
}
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
/* ShallowCopy */
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source.ShallowCopy();
clone.Name = "Harry Potter";
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("*** ShallowCopy ***");
Console.WriteLine("Source Object" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone Object" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
/* DeepCopy */
source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
clone = source.DeepCopy();
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine(Environment.NewLine + "*** DeepCopy ***");
Console.WriteLine("Source Object" + Environment.NewLine + "Name: " + source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone Object" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果 */
//*** ShallowCopy(原物件的實值型別欄位沒有被更動,但欄位Features的值被更動了) ***
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Adventure, Magic
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
//*** DeepCopy(原物件的欄位都沒有被更動) ***
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Love, Funny
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
到這邊我猜有些讀者應該和我一樣,會有一個疑問就是string
也是參考型別,但在執行 ShallowCopy 的時候,怎麼 source 物件的 Name 屬性確沒有被修改到,抱著該疑問查找了一下資料後,原因主要在於 CLR string
屬於不可變變數(Immutable),一但宣告建立後就不可再改變或重組,所以當變更 clone 物件的 Name 值時,實際上是重新分配了一塊記憶體給 clone 的 Name 屬性使用,造成兩個物件的 Name 屬性已指向不同的記憶體區塊
5. Nuget 上的 DeepCloner 套件(原始碼)
優點: 速度比使用 Expression Trees 方式還快一些,懶得自己寫程式碼時,是一個還不錯的選擇,官方文件也有提供和各種方式的效能比較
缺點: 需另外安裝額外的套件,且需 .NET Framework 4 以上,不過它有支援 .Net Standard 算是一種優點吧!?
使用前需先從 Nuget 上安裝,使用方式如下
public class Item
{
public string Name { get; set; }
public int Number { get; set; }
public List<string> Features { get; set; }
}
class Program
{
static void Main(string[] args)
{
var source = new Item { Name = "The Lord of the Rings", Number = 10, Features = new List<string> { "Love", "Funny" } };
var clone = source.DeepClone();
clone.Name = "Harry Potter";
clone.Number = 30;
clone.Features[0] = "Adventure";
clone.Features[1] = "Magic";
Console.WriteLine("Source Object" + Environment.NewLine + "Name: "+ source.Name + Environment.NewLine + "Number: " + source.Number + Environment.NewLine + "Features: " + string.Join(", ", source.Features));
Console.WriteLine();
Console.WriteLine("Clone Object" + Environment.NewLine + "Name: " + clone.Name + Environment.NewLine + "Number: " + clone.Number + Environment.NewLine + "Features: " + string.Join(", ", clone.Features));
}
}
/* 輸出結果(原始資料不變) */
//Source Object
//Name: The Lord of the Rings
//Number: 10
//Features: Love, Funny
//
//Clone Object
//Name: Harry Potter
//Number: 30
//Features: Adventure, Magic
總結
經我自己測試後,大概整理出以下的心得
效能(處理時間): 4 > 5 > 3 > 2 > 1
實用性(之後我專案傾向採用的機率來排序): 5 > 2 = 3 > 1 > 4
事實上,真的很難挑出一個十全十美的處理方式,我認為選擇出一種最適合目前手上專案的才是最重要的