續上篇,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