前言
几年前写过一个 bug,根因很土:该深拷贝的地方没深拷贝,副本一改,原件跟着变。排查的时候老板以为动的是库里的数据,其实就是一个本地对象被共享了。
先把词说清楚:
浅拷贝:值类型复制一份;引用类型复制的是引用,两边还指着同一个子对象。你改副本里的引用成员,原件也会变。
只复制对象自身的一层:字段/属性里如果是值类型,会复制一份值;如果是引用类型,复制的是引用(指针),新旧对象仍指向同一块堆上的子对象。
深拷贝:引用链上也建新对象,改副本不该动到原件的嵌套数据。
从根对象开始,递归地为引用类型也创建新实例,并把内容复制过去,直到整棵「对象图」在逻辑上独立。改拷贝不应意外改动原对象里的嵌套数据。
ICloneable:能深,但接口不保证
ICloneable只有一个object Clone(),文档不会替你承诺浅还是深,看实现。你想做深拷贝,可以,全写在Clone()里就行。
浅拷贝场景下,改拷贝里的引用类型字段,往往会影响原对象(反之亦然),除非你再给那个字段赋一个新实例。
// 浅拷贝示例(Address 还是同一个引用) public class DeepAndShallowCopy { public static void ShallowCopy() { var rawUser = new UserDto { Id = 1, Name = "name1", Address = new UserDto.AddressDto { City = "CS" } }; var copyUser = rawUser.Clone() as UserDto; copyUser.Id = 2; copyUser.Name = "name2"; copyUser.Address.City = "CS2"; // 浅拷贝:动的是同一块 Address,原数据跟着变 Console.WriteLine($"rawUser={JsonSerializer.Serialize(rawUser)}"); Console.WriteLine($"copyUser={JsonSerializer.Serialize(copyUser)}"); } } public class UserDto : ICloneable { public int Id { get; set; } public string Name { get; set; } public AddressDto Address { get; set; } public object Clone() { return new UserDto { Id = Id, Name = Name, Address = Address }; } public class AddressDto { public string City { get; set; } } }深拷贝就要让Address也Clone()一份。引用类型多就一层层写,啰嗦但清楚。
public class DeepAndShallowCopy { public static void DeepCopy() { var rawUser = new UserDto { Id = 1, Name = "name1", Address = new UserDto.AddressDto { City = "CS" } }; var copyUser = rawUser.Clone() as UserDto; copyUser.Id = 2; copyUser.Name = "name2"; copyUser.Address.City = "CS2"; // 深拷贝:Address 已是新实例,原数据不变 Console.WriteLine($"rawUser={JsonSerializer.Serialize(rawUser)}"); Console.WriteLine($"copyUser={JsonSerializer.Serialize(copyUser)}"); } } public class UserDto : ICloneable { public int Id { get; set; } public string Name { get; set; } public AddressDto Address { get; set; } public object Clone() { return new UserDto { Id = Id, Name = Name, Address = Address.Clone() as AddressDto }; } public class AddressDto : ICloneable { public string City { get; set; } public object Clone() { return new AddressDto { City = City }; } } }手写这条路:性能好,行为自己说了算。代价是对象图一大就容易漏,漏一处就是浅拷贝;另外Clone()返回object,调用处总要转一下类型,有点烦。
序列化 / AutoMapper:省事,但要心里有数
我们 CRUD 程序员经常不想维护一整张克隆图,就会想走捷径。
System.Text.Json
思路就是序列化再反序列化,得到一棵新对象。代码少,DTO、配置这类能完整序列化的类型用起来很省事。
public class DeepAndShallowCopy