
年前写过一个 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 { public static void DeepCopyByJsonSerializer() { var rawUser new UserDto { Id 1, Name name1, Address new UserDto.AddressDto { City CS } }; var copyUser rawUser.DeepCopy(); copyUser.Id 2; copyUser.Name name2; copyUser.Address.City CS2; Console.WriteLine($rawUser{JsonSerializer.Serialize(rawUser)}); Console.WriteLine($copyUser{JsonSerializer.Serialize(copyUser)}); } } public class UserDto { public int Id { get; set; } public string Name { get; set; } public AddressDto Address { get; set; } /// summary /// 序列化 → 反序列化换一批新实例 /// /summary public UserDto DeepCopy() { var rawUserString JsonSerializer.Serialize(this); return JsonSerializer.DeserializeUserDto(rawUserString)!; } public class AddressDto { public string City { get; set; } } }好处是加字段一般不用改克隆逻辑只要还能序列化。麻烦在循环引用要单独配委托、怪类型、非公开成员也可能过不去。AutoMapperpublic static void DeepCopyByAutoMapper() { var config new MapperConfiguration(config { config.CreateMapUserDto, UserDto(); // 子类型也要建同型映射否则 Address 可能还是同一条引用 config.CreateMapUserDto.AddressDto, UserDto.AddressDto(); }); IMapper mapper config.CreateMapper(); var rawUser new UserDto { Id 1, Name name1, Address new UserDto.AddressDto { City CS } }; var copyUser mapper.MapUserDto(rawUser); copyUser.Id 2; copyUser.Name name2; copyUser.Address.City CS2; Console.WriteLine($rawUser{JsonSerializer.Serialize(rawUser)}); Console.WriteLine($copyUser{JsonSerializer.Serialize(copyUser)}); }项目里本来就有 Mapper 的话顺手Map一下也行。它本职是 DTO 映射不是克隆库子图没配齐、策略不对照样可能浅拷贝。别指望「默认就是深拷贝」。JSON 和 Mapper 本质上都是在「按数据重建对象」只是经常能重建出一棵独立的树和手写Clone的语义不是一回事。record我眼里的版本答案前面ICloneable要到处asJSON 像绕路AutoMapper 容易配成玄学。record从语言层面把「数据」这件事说清楚了默认值语义、相等性、ToString、非破坏性修改编译器帮你生成一大坨样板你只要在业务代码里写with。为什么说它像版本答案不是银弹但在「数据拷贝 / 派生」这条线上很对味相等按值比同类型的两个实例成员一样就相等写单元测试、去重、缓存 key 都省心。class 默认比引用想比内容要自己重写Equals/GetHashCode一懒就埋雷。with是语法级的「从旧副本改几处」读代码的人一眼知道「基于 rawUser 出了一个新对象」不用跳进Clone()里猜深还是浅。打印友好自动生成的ToString把主要字段打出来日志里好认排障少猜几次。语法上有两种常见写法知道就行传统属性写法和 class 差不多只是类型是record白嫖相等性和with。位置参数 recordpublic record UserDto(int Id, string Name, AddressDto Address);编译器帮你生成主构造函数、解构、with里按位置对应DTO 里很省字。默认的record是引用类型相当于record class。还有record struct那是值类型语义拷贝整坨 struct 时是按位复制和「引用图里拆不拆」又是另一套题别混在一块讲深拷贝时搞晕自己就行。with和深拷贝的关系再强调一遍避免面试翻车with会复制你没改到的成员引用类型的成员如果没在with里换掉新旧两边仍指着同一个子对象。所以要深就显式写Address rawUser.Address with { ... }或给一个新的实例。嵌套深就链式with丑一点但诚实——至少「哪里拆引用」全摊在调用点不靠隐式魔法。想往「真·快照」靠可以把属性收成init或只在构造函数里赋值外面用with派生。{ get; set; }照样能改字段别嘴上说 record 不可变、手还在到处 set。和class分工可以这样记class 扛行为、生命周期长、引用身份有时就是业务含义record 扛可比较的数据快照、适合命令/事件/读模型里那种「从上一版捏一版」的写法。public class DeepAndShallowCopy { public static void DeepCopyByRecord() { var rawUser new UserDto { Id 1, Name name1, Address new UserDto.AddressDto { City CS } }; var copyUser rawUser with { Id 2, Name name2, Address rawUser.Address with { City CS2 } }; Console.WriteLine($rawUser{JsonSerializer.Serialize(rawUser)}); Console.WriteLine($copyUser{JsonSerializer.Serialize(copyUser)}); } } //注意这不是标准用法只是为了演示 public record UserDto { public int Id { get; set; } public string Name { get; set; } public AddressDto Address { get; set; } public record AddressDto { public string City { get; set; } } }标准写法示意省样板相等/ToString/with照样有public record AddressDto(string City); public record UserDto(int Id, string Name, AddressDto Address); var rawUser new UserDto(1, name1, new AddressDto(CS)); var copyUser rawUser with { Name name2, Address rawUser.Address with { City CS2 } };怎么选没有银弹按常见情况排个序够用就行核心模型、性能敏感、要一眼能审代码手写Clone或工厂从旧对象构造新的。普通 DTO / 配置、能序列化、没环JSON 往返最省事。项目里 Mapper 已经到处都是可以CreateMapT,T()把子类型也配全并记得做配置校验和用例别光靠手感。业务就是「从旧状态派生一个新状态」、嵌套也愿意写清楚recordwith。