[.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 Phones { get; set; } = new List();

    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 { "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 { "9", "1", "1" },
        IdInfo = new IdInfo(1)
    };

    var p2 = p1.ShallowCopy() as Person;
    p2.Name = "史丹利";
    p2.Age = 36;
    p2.Address = "中国台湾中国台北市内湖区";
    p2.Phones = new List();
    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 { "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 Phones { get; set; } = new List();

    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(this.Phones);
        return other;
    }

}

重新执行测试程序

[TestMethod]
public void TestDeepCopy()
{
    var p1 = new Person
    {
        Name = "长泽雅美",
        Age = 30,
        Address = "日本静冈县磐田市",
        Phones = new List { "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] 利用序列化进行类深复制