續上篇,https://dotblogs.com.tw/yc421206/archive/2011/06/17/28785.aspx,提到了淺複製與深複製,這裡整理了一些深複製的用法,希望對你有幫助...
本文連結
開發環境
- Windows 10 Enterprise x64
- AutoMapper 5.2.0
- Newtonsoft.Json 9.0.1
- VS 2015 Update3
錯誤的用法
這是對物件運作不熟悉所產生的問題,仍然是許多人會犯的錯誤
[TestMethod]
public void 物件複製_錯誤的做法()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var target = source;
//改變狀態
target.Age = 20;
target.Address = "火星";
target.Name.FirstName = "張";
//source欄位的狀態都被改變了,因為仍然是參考同一份記憶體位置
Assert.AreNotEqual(source.Age, 18);
Assert.AreNotEqual(source.Address, "地球村");
Assert.AreNotEqual(source.Name.FirstName, "余");
}
淺複製
淺複製:是創建一個新的執行個體時,這個 "新的執行個體" 對 "目前執行個體" 中所有成員變數進行複製。
- 實質型別:建立新的記憶體並複製值給"新的執行個體",當 "新的執行個體" 的欄位狀態改變,不會影響 "目前執行個體" 的狀態。
- 參考型別:建立新的記憶體並參考原有的記憶體位置給"新的執行個體",當 "新的執行個體" 的欄位狀態改變,會影響 "目前執行個體" 的狀態
[TestMethod]
public void 物件複製_1_淺複製_MemberwiseClone()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var target = source.Clone();
//改變狀態
target.Age = 20;
target.Address = "火星";
target.Name.FirstName = "張";
//淺複製會複製實質型別的狀態,參考型別複製記憶體位置
Assert.AreEqual(source.Age, 18);
Assert.AreEqual(source.Address, "地球村");
Assert.AreNotEqual(source.Name.FirstName, "余");
}
Hard Code-手工復刻
效能最好但也是最難維護,欄位一多的時候,開發人員應該就會崩潰
[TestMethod]
public void 物件複製_2_深複製_手工復刻()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var target = new Person();
target.Address = source.Address;
target.Age = source.Age;
target.Name = new Name(source.Name.FirstName, source.Name.LastName);
//改變狀態
target.Age = 20;
target.Address = "火星";
target.Name.FirstName = "張";
Assert.AreEqual(source.Age, 18);
Assert.AreEqual(source.Address, "地球村");
Assert.AreEqual(source.Name.FirstName, "余");
}
接下來的方式都不需要理會屬性的異動,但也會有效能上的損耗
序列化
- 不須理會屬性的異動
- 序列化的方式很多,效能也不大一樣,能夠複製的內容也不大一樣,可能私有物件會無法複製,這裡我選用Json.Net
- 只能複製相同的型別
[TestMethod]
public void 物件複製_3_深複製_序列化()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var target = JsonConvert.DeserializeObject<Person>(JsonConvert.SerializeObject(source));
//改變狀態
target.Age = 20;
target.Address = "火星";
target.Name.FirstName = "張";
Assert.AreEqual(source.Age, 18);
Assert.AreEqual(source.Address, "地球村");
Assert.AreEqual(source.Name.FirstName, "余");
}
反射
- 不須理會屬性的異動
- 可複製不同的型別內容
- 下面的程式碼寫的很醜,有很大的改善空間
- Expression Tree會更快
[TestMethod]
public void 物件複製_4_深複製_反射()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var personType = typeof(Person);
var personPropertyInfos = personType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var personTarget = Activator.CreateInstance<Person>();
foreach (var personPropertyInfo in personPropertyInfos)
{
var personValue = personPropertyInfo.GetValue(source, null);
if (personPropertyInfo.PropertyType == typeof(Name))
{
var nameType = typeof(Name);
var namePropertyInfos = nameType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
personTarget.Name = Activator.CreateInstance<Name>();
foreach (var namePropertyInfo in namePropertyInfos)
{
var nameValue = namePropertyInfo.GetValue(source.Name, null);
namePropertyInfo.SetValue(personTarget.Name, nameValue, null);
}
}
else
{
personPropertyInfo.SetValue(personTarget, personValue, null);
}
}
//改變狀態
personTarget.Age = 20;
personTarget.Address = "火星";
personTarget.Name.FirstName = "張";
Assert.AreEqual(source.Age, 18);
Assert.AreEqual(source.Address, "地球村");
Assert.AreEqual(source.Name.FirstName, "余");
}
AutoMapper
- 不須理會屬性的異動
- 可複製不同的型別內容
- 彈性配置對應方式
這也是我最常用的方式
[TestMethod]
public void 物件複製_5_深複製_AutoMapper()
{
var source = new Person();
source.Address = "地球村";
source.Age = 18;
source.Name = new Name("余", "小章");
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Person, Person>();
cfg.CreateMap<Name, Name>();
});
var mapper = config.CreateMapper();
var target = new Person();
mapper.Map(source, target);
//改變狀態
target.Age = 20;
target.Address = "火星";
target.Name.FirstName = "張";
Assert.AreEqual(source.Age, 18);
Assert.AreEqual(source.Address, "地球村");
Assert.AreEqual(source.Name.FirstName, "余");
}
效能測試
也順手做了一下效能比較測試,開始測試前,每個測試方法都先熱機一次
[TestMethod]
[TestMethod]
public void 跑吧()
{
this.Run(1);
this.Run(10);
this.Run(100);
this.Run(1000);
this.Run(10000);
this.Run(100000);
this.Run(1000000);
this.Run(10000000);
this.Run(100000000);
}
private void Run(int count)
{
var source = this.CreateSource();
var mapper = this.CreateMapper();
//熱機
this.CloneByHardCode(source);
this.CloneByJsonNET(source);
this.CloneByReflection(source);
this.CloneByAutoMapper(source, mapper);
Trace.WriteLine("行執次數:" + count);
this.ProcessTime(() => this.CloneByHardCode(source), count, "Hard Code".PadRight(20, ' '));
this.ProcessTime(() => this.CloneByJsonNET(source), count, "JSON.NET".PadRight(20, ' '));
this.ProcessTime(() => this.CloneByReflection(source), count, "Reflection".PadRight(20, ' '));
this.ProcessTime(() => this.CloneByAutoMapper(source, mapper), count, "AutoMapper".PadRight(20, ' '));
Trace.WriteLine("");
}
....其餘省略
測試結果:
Debug Trace: 行執次數:1 測試方法:Hard Code ,花費時間:0.0498ms 測試方法:JSON.NET ,花費時間:0.1282ms 測試方法:Reflection ,花費時間:0.0989ms 測試方法:AutoMapper ,花費時間:0.5129ms 行執次數:10 測試方法:Hard Code ,花費時間:0.0034ms 測試方法:JSON.NET ,花費時間:0.0905ms 測試方法:Reflection ,花費時間:0.0346ms 測試方法:AutoMapper ,花費時間:0.0102ms 行執次數:100 測試方法:Hard Code ,花費時間:0.0079ms 測試方法:JSON.NET ,花費時間:0.7971ms 測試方法:Reflection ,花費時間:5.5068ms 測試方法:AutoMapper ,花費時間:0.0654ms 行執次數:1000 測試方法:Hard Code ,花費時間:0.1111ms 測試方法:JSON.NET ,花費時間:7.6725ms 測試方法:Reflection ,花費時間:2.8742ms 測試方法:AutoMapper ,花費時間:0.3146ms 行執次數:10000 測試方法:Hard Code ,花費時間:1.2209ms 測試方法:JSON.NET ,花費時間:86.386ms 測試方法:Reflection ,花費時間:29.5206ms 測試方法:AutoMapper ,花費時間:3.1759ms 行執次數:100000 測試方法:Hard Code ,花費時間:8.3022ms 測試方法:JSON.NET ,花費時間:775.4857ms 測試方法:Reflection ,花費時間:313.3791ms 測試方法:AutoMapper ,花費時間:28.1992ms 行執次數:1000000 測試方法:Hard Code ,花費時間:76.5713ms 測試方法:JSON.NET ,花費時間:7216.0836ms 測試方法:Reflection ,花費時間:2873.6796ms 測試方法:AutoMapper ,花費時間:283.3539ms 行執次數:10000000 測試方法:Hard Code ,花費時間:773.1358ms 測試方法:JSON.NET ,花費時間:72609.3684ms 測試方法:Reflection ,花費時間:28961.5389ms 測試方法:AutoMapper ,花費時間:2916.5714ms 行執次數:100000000 測試方法:Hard Code ,花費時間:7589.1716ms 測試方法:JSON.NET ,花費時間:726163.1746ms 測試方法:Reflection ,花費時間:290036.1305ms 測試方法:AutoMapper ,花費時間:29132.5581ms
範例位置:
https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.DeepClone2/
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET