[C#.NET] 物件複製的幾種做法

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

Image result for microsoft+mvp+logo