尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C#如何优雅处理引用类型的深拷贝
年前写过一个 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。
RELATED

相关推荐

TLA2518与TM4C129ENCPDT构建高精度信号采集系统

TLA2518与TM4C129ENCPDT构建高精度信号采集系统

1. 为什么需要高精度模拟信号采集?在工业自动化、医疗设备和消费电子等领域,我们经常需要将现实世界中的模拟信号(如温度、压力、声音等)转换为数字信号进行处理。这种转换的精度和可靠性直接决定了整个系统的性能表现。以医疗监护…

📅 2026/8/24 14:25:43
X射线聚焦系统

X射线聚焦系统

高能光子(X射线)的使用已成为许多医疗和同步加速器应用的共同特点。与可见光谱中的光不同,X射线与大多数物质仅发生微弱的相互作用,这使得聚焦元件的设计比波长谱的其他部分更具挑战性。下面我们展示了两种解决此任务的方法,即使用复合透镜和…

📅 2026/8/11 17:56:59
【华硕无畏X1405V在ubuntu22.04下无线网卡修复】

【华硕无畏X1405V在ubuntu22.04下无线网卡修复】

华硕无畏X1405V在ubuntu22.04下无线网卡修复 wifi型号:MT7902 lspci | grep -i network 0000:02:00.0 Network controller: MEDIATEK Corp. Device 7902修复前提:使用usb-网口连上有线网 修复方法:下载codex,让他帮我修 遇到的几个…

📅 2026/7/12 18:17:06
MORE NEWS

更多资讯

📰

如何用 hyprctl output create 为 Hyprland 动态添加和移除虚拟显示器?

如何用 hyprctl output create 为 Hyprland 动态添加和移除虚拟显示器? 【免费下载链接】Hyprland Hyprland is an independent, highly customizable, dynamic tiling Wayland compositor that doesnt sacrifice on its looks. 项目地址: https://gitcode.com/Gi…

📰

ClaudeCode AI编程助手服务器部署与优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

企业级数据可视化库选型:高吞吐、低延迟、强交互实战评测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

西门子S7-1200 PLC实现四节传送带控制系统详解

1. 项目概述:四节传送带控制系统的PLC实现在工业自动化领域,传送带系统是最基础也是最核心的物料输送设备之一。我最近用西门子S7-1200 PLC完成了一个四节传送带控制系统的课设项目,这个设计不仅包含了完整的梯形图程序,还整合了故…

📰

ARM架构与交叉编译实战:从工具链到Qt迁移的避坑指南

“Exec format error”——如果你在 x86_64 的 Ubuntu 上为树莓派或者某个 ARM 开发板编译完程序,把它 scp 过去执行,大概率会撞上这行报错。我第一次遇到时,第一反应是“我是不是包拷错了”,傻乎乎地反复确认了 SHA256 校验值&am…

📰

Cataclysm-DDA 建造教程:用 2 步建起第一堵砖墙

Cataclysm-DDA 建造教程:用 2 步建起第一堵砖墙 【免费下载链接】Cataclysm-DDA Cataclysm - Dark Days Ahead. A turn-based survival game set in a post-apocalyptic world. 项目地址: https://gitcode.com/GitHub_Trending/ca/Cataclysm-DDA 本文带你走通…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬