缓存里放了一份配置对象,业务代码改了两个字段,下一次读缓存拿到的却是改过的值。查了半天,问题在赋值那行,它只是把引用指了过去。
先分清浅拷贝到底浅在哪
MemberwiseClone 复制的是字段值。值类型字段各存一份,引用类型字段还是指向同一个 List、同一个数组。改列表里的元素,两边一起变,这是最常见的坑。结构体里套引用类型,一样躲不过。
序列化一把梭,够用但别乱用
用 System.Text.Json 序列化再反序列化,能得到完全独立的副本,写法就两行。限制是对象得有公开无参构造,私有字段、只读属性、委托都带不过去。带循环引用的对象会直接抛异常,得先处理引用环。字段多、层级深的配置类用它最省事。
手写拷贝构造函数最可控
类里加一个接收同类型的构造函数,引用字段逐个 new 一份,列表用循环复制元素。代码长,但复制了哪些、跳过了哪些一眼能看清,性能也稳。字段一多容易漏,改类时记得同步,写完跑一遍单测专门验证两个对象互不影响。
记录类型省一层
用 record 加 with 表达式,能拿到改过某个属性的新实例,原对象不动。这招适合不可变配置,涉及嵌套集合时还是浅的,里面的列表照样共享。要真独立,就在 with 之后再手动复制集合。
数组和字典要单独处理
数组用自带的 Clone 只能复制一层,元素是对象的话照样共享。字典要么循环重建,要么用序列化。嵌套三层的结构,手写容易漏掉最里面那层,写完构造一个改内部字段的测试用例跑一遍,比肉眼看靠谱。
性能上的取舍
几千个对象频繁复制,序列化会成为瓶颈,测出来往往是普通构造函数的十几倍。这时候要么少复制,要么只在边界处复制一次。复制本身也是设计问题,到处都在深拷贝,说明共享和修改的边界没划清。先想清楚谁拥有这份数据,再决定用哪种写法。
复制前先问自己内部集合要不要一起独立。答案是要,就别用 MemberwiseClone,选序列化或手写都行。