CSharp 深拷贝

C#中有两种类型:引用类型和值类型。引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。对于引用类型,两种变量可引用同一对象;因此,对一个变量执行的操作会影响另一个变量所引用的对象。对于值类型,每个变量都具有其自己的数据副本,对一个变量执行的操作不会影响另一个变量

C# 类型参考

值类型:

boolbytechardecimaldoubleenumfloatintlongsbyteshortstructuintulongushort

引用类型

classinterfacedelegatedynamicobjectstring

理解问题

数组

通过一个例子来理解存在的问题:

1
2
3
4
5
6
7
int[] arrayA = new int[] { 1, 2, 3 };
int[] arrayB = arrayA;

arrayB[0] = 0;

Console.WriteLine(String.Format("arrayA: {0}, {1}, {2}", arrayA[0], arrayA[1], arrayA[2]));
Console.WriteLine(String.Format("arrayB: {0}, {1}, {2}", arrayB[0], arrayB[1], arrayB[2]));

这里将arrayA赋给了arrayB,并对arrayB[0]的值进行更改然后输出arrayA和arrayB的值。输出如下:

arrayA: 0, 2, 3
arrayB: 0, 2, 3

这种情况我们都遇到过。可以看到对arrayB[0]的更改影响了arrayA[0],这是因为“arrayB = arrayA”传递的是对数组的引用而不是值,所以当改变arrayB[0]的值时arrayA[0]也发生了改变

有时候我们希望能简单的复制一个数组的值到另一个数组,并希望改变其中一个数组的值时另一个数组不会受到影响。这里可以使用为数组提供的CopyTo方法

1
2
3
4
5
6
7
8
int[] arrayA = new int[] { 1, 2, 3 };
int[] arrayB = new int[3];
arrayA.CopyTo(arrayB, 0);

arrayB[0] = 0;

Console.WriteLine(String.Format("arrayA: {0}, {1}, {2}", arrayA[0], arrayA[1], arrayA[2]));
Console.WriteLine(String.Format("arrayB: {0}, {1}, {2}", arrayB[0], arrayB[1], arrayB[2]));

输出结果:

arrayA: 1, 2, 3
arrayB: 0, 2, 3

自定义类型

对于自定义类型也可以使用CopyTo方法或者使用AddRange方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User
{
public string Name { get; set; }
public string Gender { get; set; }
}



List<User> usersA = new List<User>
{
new User { Name = "Danny",Gender = "" }
};
List<User> usersB = new List<User>();
User[] usersC = new User[1];

usersB.AddRange(usersA);
usersA.CopyTo(usersC);

usersB[0] = new User { Name = "Bob", Gender = "Boy" };
usersC[0] = new User { Name = "Jack", Gender = "Boy" };

Console.WriteLine(String.Format("usersA: {0}", usersA[0].Name));
Console.WriteLine(String.Format("usersB: {0}", usersB[0].Name));
Console.WriteLine(String.Format("usersC: {0}", usersC[0].Name));

输出结果:

usersA: Danny
usersB: Bob
usersC: Jack

部分项

上面所讲的问题可以解决整个Array或List的复制情况,实际情况中我们遇到更多的是处理集合中的部分项,例如

1
2
3
4
5
6
7
8
9
10
List<User> usersA = new List<User>
{
new User { Name = "Danny",Gender = "" }
};
User userB = usersA.First();

userB.Name = "Bob";

Console.WriteLine(String.Format("usersA: {0}", usersA[0].Name));
Console.WriteLine(String.Format("usersB: {0}", userB.Name));

这里获取usersA的第一项,并将其赋给userB,然后改变了userB的Name属性。输出如下:

usersA: Bob
usersB: Bob

可以发现在改变了userB的Name属性后usersA的第一项中的Name属性也发生了变化

对于这种情况有两种方法可供选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class User
{
public string Name { get; set; }
public string Gender { get; set; }

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



List<User> usersA = new List<User>
{
new User { Name = "Danny",Gender = "" }
};
User userB = new User()
{
Name = usersA.First().Name,
Gender = usersA.First().Gender
};
User userC = usersA.First().ShallowCopy();

userB.Name = "Bob";
userC.Name = "Jack";

Console.WriteLine(String.Format("usersA: {0}", usersA[0].Name));
Console.WriteLine(String.Format("usersB: {0}", userB.Name));
Console.WriteLine(String.Format("usersC: {0}", userC.Name));

第一种方法是实例化一个新的对象,并将对每个属性一一赋值;第二种方法是使用MemberwiseClone创建User对象的“表浅副本”,输出结果:

usersA: Danny
usersB: Bob
usersC: Jack

注:
MemberwiseClone方法通过创建一个新对象,然后将当前对象的非静态字段复制到新的对象来创建卷影副本。如果某个字段的值类型,则执行字段的按位复制。如果字段是引用类型,则复制引用,原始对象和其克隆引用同一对象

深拷贝

至此为止我们都没有涉及到需要使用深拷贝的复制,为了演示这一问题我们定义一个名为Contact的类,并将其作为User类中的一个属性的类型。同时我还对已有的代码做了部分更改,以突出显示这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class User
{
public string Name { get; set; }
public string Gender { get; set; }

public Contact Contact { get; set; }

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

public class Contact
{
public string Type { get; set; }
public string Detail { get; set; }
}



List<User> usersA = new List<User>
{
new User { Name = "Danny",Gender = "", Contact = new Contact{ Type = "Email" , Detail = "name@domain.com" } }
};
User userB = new User()
{
Name = usersA.First().Name,
Gender = usersA.First().Gender,
Contact = usersA.First().Contact
};
User userC = usersA.First().ShallowCopy();

userB.Contact.Type = "Telephone";
userC.Contact.Detail = "000000";

Console.WriteLine(String.Format("usersA: {0}, {1}", usersA[0].Contact.Type, usersA[0].Contact.Detail));
Console.WriteLine(String.Format("usersB: {0}, {1}", userB.Contact.Type, userB.Contact.Detail));
Console.WriteLine(String.Format("usersC: {0}, {1}", userC.Contact.Type, userC.Contact.Detail));

我使用了之前的usersA列表并为新增的Contact属性赋值。从输出结果可以看到,无论是创建新的对象之后重新赋值,还是使用MemberwiseClone方法创建对象的表浅副本usersA中的Contact都是以引用的方式传递的。输出结果:

usersA: Telephone, 000000
usersB: Telephone, 000000
usersC: Telephone, 000000

为了解决这个问题我在User中定义了名为DeepCopy的方法,该方法中使用MemberwiseClone将User中的简单类型复制到新对象,并使用Contact类的构造器(该构造器的参数包含了Contact对象的所有属性)创建新的Contact对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class User
{
public string Name { get; set; }
public string Gender { get; set; }

public Contact Contact { get; set; }

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

public User DeepCopy()
{
User other = (User)this.MemberwiseClone();
other.Contact = new Contact(Contact.Type, Contact.Detail);
return other;
}
}

public class Contact
{
public string Type { get; set; }
public string Detail { get; set; }

public Contact()
{
}

public Contact(string type, string detail)
{
this.Type = type;
this.Detail = detail;
}
}



List<User> usersA = new List<User>
{
new User { Name = "Danny",Gender = "", Contact = new Contact{ Type = "Email" , Detail = "name@domain.com" } }
};
User userB = new User()
{
Name = usersA.First().Name,
Gender = usersA.First().Gender,
Contact = usersA.First().Contact
};
User userC = usersA.First().DeepCopy();

userB.Contact.Type = "Telephone";
userC.Name = "sa";
userC.Contact.Detail = "000000";

Console.WriteLine(String.Format("usersA: {0}, {1}", usersA[0].Contact.Type, usersA[0].Contact.Detail));
Console.WriteLine(String.Format("usersB: {0}, {1}", userB.Contact.Type, userB.Contact.Detail));
Console.WriteLine(String.Format("usersC: {0}, {1}", userC.Contact.Type, userC.Contact.Detail));

可以看到更改通过DeepCopy方法获取的User对象并不会对已有对象造成任何影响。输出结果:

usersA: Telephone, name@domain.com
usersB: Telephone, name@domain.com
usersC: Contact.Type Email, Contact.Detail 000000

对于C#深拷贝存在很多的方法或开源项目,这里介绍的是最容易理解和保险的方法。如果存在大量深拷贝的场景请综合性能和编码量选择合适的深拷贝方法