[.NET][C#]物件建立之淺層複製(Shallow Copy) vs 深層複製(Deep Copy)

最近踩到一個legacy code 在C#物件複製的陳年小雷,拆解炸彈的同時也寫筆記! 

有時我們會在類別(class)中加入Object.MemberwiseClone方法來提供物件的複製(clone),舊程式使用新物件裡的屬性剛好都是用new關鍵字建立,大概像下面的方式使用屬性:  

p2.IdInfo = new IdInfo(17);

很幸運一直沒發生參考問題,最近改用直接指派,類似下面的寫法:  

p2.IdInfo.IdNumber = 17;

測試時大驚!原始物件p1的值竟然被覆蓋了,花了時間才發現自己對MemberwiseClone的定義不夠清楚。

 

測試步驟

1.實作Object.MemberwiseClone的淺層複製。

2.實驗原始物件值未被覆蓋(new 關鍵字)

3.實驗原始物件值被覆蓋(指派)

4.深層複製的寫法之一。


先在測試專案中新增測試用的類別(Class)並且新增淺層複製(Shallow Copy)的方法

public class Person
{
    public IdInfo IdInfo;
    public int Age { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public List<string> Phones { get; set; } = new List<string>();

    public Person ShallowCopy()
    {
        return (Person)this.MemberwiseClone();
    }
}
public class IdInfo
{
    public int IdNumber;
    public IdInfo(int IdNumber)
    {
        this.IdNumber = IdNumber;
    }
}

 


測試Object.MemberwiseClone的淺層複製的效果

輸入以下測試程式碼

[TestMethod]
public void TestShallowCopy()
{
    var person1 = new Person
    {
        Name = "長澤雅美",
        Age = 30,
        Address = "日本静岡縣磐田市",
        Phones = new List<string> { "9", "1", "1" },
        IdInfo = new IdInfo(1)
    };
    var person2 = person1.ShallowCopy() as Person;
    Console.WriteLine($"person1 id={person1.IdInfo.IdNumber} Name={person1.Name} , Address={person1.Address} , Age={person1.Age} ,
phone={person1.Phones.Count}");
    Console.WriteLine($"person2 id={person2.IdInfo.IdNumber} Name={person2.Name} , Address={person2.Address} , Age={person2.Age}, phone={person2.Phones.Count}");
}

測試結果:

淺層的物件複製就可以將每個屬性都clone過來了!

 


實驗原始物件值未被覆蓋(new 關鍵字)

輸入以下程式碼

[TestMethod]
public void TestShallowCopyReplace1()
{
    var p1 = new Person
    {
        Name = "長澤雅美",
        Age = 30,
        Address = "日本静岡縣磐田市",
        Phones = new List<string> { "9", "1", "1" },
        IdInfo = new IdInfo(1)
    };

    var p2 = p1.ShallowCopy() as Person;
    p2.Name = "史丹利";
    p2.Age = 36;
    p2.Address = "台灣台北市內湖區";
    p2.Phones = new List<string>();
    p2.IdInfo = new IdInfo(17);

    Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
    Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}

測試結果:

 New關鍵字!幸運沒事!雅美的電話和id並沒有被覆蓋!

 


實驗原始物件值被覆蓋(指派)

輸入以下程式碼,尤其是以下這兩行屬性值的修改

    p2.Phones.Clear();
    p2.IdInfo.IdNumber = 17;
public void TestShallowCopyReplace2()
{
    var p1 = new Person
    {
        Name = "長澤雅美",
        Age = 30,
        Address = "日本静岡縣磐田市",
        Phones = new List<string> { "9", "1", "1" },
        IdInfo = new IdInfo(1)
    };

    var p2 = p1.ShallowCopy() as Person;
    p2.Name = "史丹利";
    p2.Age = 36;
    p2.Address = "台灣台北市內湖區";
    p2.Phones.Clear();
    p2.IdInfo.IdNumber = 17;

    Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
    Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}

執行結果:

電話和ID兩個屬性果然被覆蓋了!

重新看一下MemberwiseClone的說明

 If a field is a value type, a bit-by-bit copy of the field is performed.   If a field is a reference type, the reference is copied but the referred object is not; therefore, the original object and its clone refer to the same object.  

如果欄位是實值型別,則會複製出欄位的複本。 如果欄位是參考型別,將只會複製參考!

 

傷腦筋!幸好馬上搜尋到余小張大大的文章,馬上從淺複製升級到深複製!

 


深層複製的寫法之一

首先要修改一下原始的類別,新增Deep Copy方法

public class Person
{
    public IdInfo IdInfo;
    public int Age { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public List<string> Phones { get; set; } = new List<string>();

    public Person ShallowCopy()
    {
        return (Person)this.MemberwiseClone();
    }

    public Person DeepCopy()
    {
        Person other = (Person)this.MemberwiseClone();
        other.IdInfo = new IdInfo(this.IdInfo.IdNumber);
        other.Name = string.Copy(this.Name);
        other.Address = string.Copy(this.Address);
        other.Phones = new List<string>(this.Phones);
        return other;
    }

}

重新執行測試程式

[TestMethod]
public void TestDeepCopy()
{
    var p1 = new Person
    {
        Name = "長澤雅美",
        Age = 30,
        Address = "日本静岡縣磐田市",
        Phones = new List<string> { "9", "1", "1" },
        IdInfo = new IdInfo(1)
    };

    var p2 = p1.DeepCopy() as Person;
    p2.Name = "史丹利";
    p2.Age = 36;
    p2.Address = "台灣台北市內湖區";
    p2.Phones.Clear();
    p2.IdInfo.IdNumber = 17;

    Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
    Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}

執行結果:

成功分開本來就是平行線的兩個人,偶像還是偶像,工程師還是工程師!來看日劇!

 


小結:

  1. 序列化(Serialize)及反射(reflection)都是其他深層複製的方法。
  2. 因為這次程式的改法是屬性一個一個自己複製,會有維護性的風險,推薦參考余小張大的[C#.NET]利用序列化進行類別深複製 。
  3. 字串String沒受影響是因為字串變更時會自動重新配置記憶體。

 

參考: 

Object.MemberwiseClone 方法()

余小張大的[C#.NET] 利用序列化進行類別深複製