C#-浅拷贝与深拷贝.md

一、先搞清楚三个容易混淆的概念
很多人其实把这三件事混在一起了:
1. 引用复制
var b = a;这不是拷贝。
如果 a 是类对象,b 和 a 指向的是同一个对象。
改 b.Name,a.Name 也跟着变。
2. 浅拷贝
创建一个新对象,但对象内部的引用类型字段仍然指向原来的对象。
原对象 浅拷贝副本
┌──────────┐ ┌──────────┐
│ Name │──str │ Name │──str (string 不可变,独立)
│ Age │──int │ Age │──int (值类型,独立)
│ Address │──┐ │ Address │──┐
└──────────┘ │ └──────────┘ │
└──► ┌──────────┐ ◄┘
│ Address │ ← 两个对象共享同一个 Address!
└──────────┘3. 深拷贝
创建一个新对象,并且把内部引用到的对象也递归地复制出全新实例。
原对象 深拷贝副本
┌──────────┐ ┌──────────┐
│ Name │──str │ Name │──str (各自独立)
│ Age │──int │ Age │──int (各自独立)
│ Address │──►A1 │ Address │──►A2 (各自独立的 Address!)
└──────────┘ └──────────┘二、本质问题是什么
浅拷贝和深拷贝要解决的本质问题: “我改这个对象时,会不会不小心把另一个对象也改了?”
换句话说,就是控制对象状态是否相互隔离。
三、为什么浅拷贝有时不够用
关键在于 string 和值类型 与 可变引用类型 的区别:
| 字段类型 | 浅拷贝后是否独立 | 原因 |
|---|---|---|
int、bool、struct 等值类型 |
✅ 独立 | 值类型直接存储值,拷贝后各自持有独立的副本 |
string |
✅ 看起来独立 | string 是不可变类型,修改等于创建新对象,不会影响原来的引用 |
| 可变的引用类型(如自定义类) | ❌ 共享 | 浅拷贝只复制引用地址,两个对象指向同一个实例 |
一个直观的例子
var p1 = new Person
{
Name = "Tom",
Address = new Address { City = "Beijing" }
};
var p2 = p1.ShallowCopy();
p2.Name = "Jerry"; // p1.Name 还是 "Tom" ✅
p2.Address.City = "Shanghai"; // p1.Address.City 也变成 "Shanghai" ❌Name 是 string,互不影响。
Address 是引用类型,p1 和 p2 共享同一个 Address 实例,改一个,另一个也跟着变。
四、实现方式
浅拷贝
MemberwiseClone() 是 .NET 提供的内置浅拷贝方法:
public Customer ShallowCopy()
{
return (Customer)this.MemberwiseClone();
}深拷贝
.NET 没有内置的通用深拷贝方法,需要手动实现,把每个引用类型字段都递归地 new 出来:
public Customer DeepCopy()
{
return new Customer
{
Name = this.Name,
Age = this.Age,
Address = new Address // 关键:Address 也要 new 出新的
{
City = this.Address.City
}
};
}配套的 CopyFrom —— 在 WPF 中很实用
深拷贝用于"创建副本",CopyFrom 用于"把副本的结果写回原对象":
public void CopyFrom(Customer other)
{
Name = other.Name;
Age = other.Age;
Address.City = other.Address.City;
}典型用法:
var backup = original.DeepCopy(); // 1. 深拷贝创建副本
// ... 用户在副本上编辑 ...
original.CopyFrom(backup); // 2. 确认时把副本写回原对象五、为什么不推荐 ICloneable
.NET 提供了 ICloneable 接口,定义了 Clone() 方法。
但业界普遍不推荐使用它,原因只有一个:
Clone()方法没有说明它到底是浅拷贝还是深拷贝。 调用者无法从接口上判断行为,语义不清晰。
推荐的替代方案
// 方式1:明确命名的方法(最推荐)
Customer ShallowCopy()
Customer DeepCopy()
// 方式2:拷贝构造函数
public Customer(Customer other)
{
Name = other.Name;
Address = new Address(other.Address);
}
// 方式3:CopyFrom(用于回写,WPF 中特别好用)
void CopyFrom(Customer other)六、深拷贝不是"越深越好"
这是一个常见的误解。
深拷贝的真正含义不是"把所有对象全部递归复制到底", 而是**“复制到你需要隔离的边界为止”**。
有些对象本来就应该共享:
- 全局只读的字典、配置
- 不可变对象(Immutable)
- 枚举值
- 被多个对象刻意共享的资源
这些不需要深拷贝,强行复制反而浪费资源,还可能破坏语义。
七、如何判断该用浅拷贝还是深拷贝
问自己一句话:
复制出来的新对象,后续修改时,哪些部分必须与原对象隔离?
| 情况 | 建议 |
|---|---|
| 对象内全是值类型和 string | 浅拷贝足够 |
| 对象内有嵌套的可变引用类型 | 需要深拷贝 |
| 只需要隔离部分字段 | 选择性深拷贝,共享可以安全共享的部分 |
八、在 WPF 开发中的实际价值
场景1:编辑弹窗 —— 最常见、最实用
问题:如果弹窗直接绑定原对象,双向绑定会实时写回。 用户还没点确认,原对象就已经变了。点取消也无法恢复。
正确做法:
// 打开弹窗时:深拷贝创建副本,弹窗绑定副本
var editCopy = SelectedCustomer.DeepCopy();
var dialog = new EditDialog(editCopy);
if (dialog.ShowDialog() == true)
SelectedCustomer.CopyFrom(editCopy); // 确认:把副本写回
// 取消:直接丢弃副本,原对象完全不受影响为什么浅拷贝不够:
浅拷贝只隔离了顶层字段,嵌套对象(如 Address)仍然共享。
用户在弹窗中修改地址,原对象的地址也跟着变,取消无效。
场景2:DataGrid 行编辑、取消编辑(IEditableObject)
WPF 的 DataGrid 支持 IEditableObject 接口:
BeginEdit():开始编辑,此时备份一份深拷贝CancelEdit():取消编辑,用备份恢复EndEdit():确认编辑,丢弃备份
public class Customer : IEditableObject
{
private Customer? _backup;
public void BeginEdit() => _backup = this.DeepCopy();
public void CancelEdit() => CopyFrom(_backup!);
public void EndEdit() => _backup = null;
}如果 BeginEdit 里只做浅拷贝,CancelEdit 时嵌套对象的修改就无法恢复。
场景3:撤销/重做(Undo/Redo)
每次操作前保存一个完整快照:
undoStack.Push(CurrentDocument.DeepCopy()); // 操作前保存快照
// ... 执行操作 ...
// 撤销时:
var snapshot = undoStack.Pop();
CurrentDocument.CopyFrom(snapshot);为什么必须深拷贝: 如果只是浅拷贝,后续修改会顺着共享引用把"历史快照"也改掉,撤销就失效了。
场景4:复制列表项 / 配置模板
用户点击"复制",基于现有项创建一个独立的新项:
var newItem = selectedItem.DeepCopy();
newItem.Id = Guid.NewGuid();
Items.Add(newItem);如果只是浅拷贝,两个列表项的嵌套对象是共享的,修改一个会影响另一个,问题很隐蔽。
场景5:修改 WPF 共享资源(Freezable)
WPF 的资源字典中的对象通常是共享实例(如 Brush、Transform)。
直接修改会影响所有使用该资源的地方:
// ❌ 错误:直接修改共享资源
var brush = (SolidColorBrush)FindResource("PrimaryBrush");
brush.Color = Colors.Red; // 影响全局!
// ✅ 正确:克隆后再修改
var brush = ((SolidColorBrush)FindResource("PrimaryBrush")).Clone();
brush.Color = Colors.Red; // 只影响这一处
element.Background = brush;WPF 的 Freezable 内置了 Clone() 方法,这是 WPF 框架自己解决"共享资源修改"问题的方式,本质上就是深拷贝的思想。
九、WPF 中需要注意的边界
❌ 不要拷贝 UI 控件对象
Window、UserControl、Button 等 DependencyObject 不是普通 POCO,内部包含:
- Dispatcher 线程关联
- 双向绑定表达式
- 事件订阅
- 样式、模板、资源引用
靠 MemberwiseClone() 复制这些对象会产生不可预期的问题。
正确原则:复制数据,不复制界面对象。
⚠️ 谨慎拷贝 ViewModel
ViewModel 通常包含:
INotifyPropertyChanged事件订阅ICommand实例- Service / Repository 引用
- Messenger / EventAggregator 订阅
- 定时器等运行时状态
直接拷贝 ViewModel 会把这些运行时关系也复制过去,问题很隐蔽。
推荐做法:拷贝 Model,用 Model 构造新的 ViewModel,或者专门定义一个"编辑用 EditModel / DTO"。
⚠️ 谨慎用序列化来做深拷贝
有人会用 JSON 序列化再反序列化来实现深拷贝,偶尔可以用,但不推荐作为通用方案:
- 性能开销较大
- 丢失运行时状态(事件、命令等)
- 循环引用容易报错
- 对 WPF 对象完全无效
十、一张表总结三种方式的区别
引用复制 b = a |
浅拷贝 MemberwiseClone |
深拷贝 手动实现 | |
|---|---|---|---|
| 是否创建新对象 | ❌ 否 | ✅ 是 | ✅ 是 |
| 值类型字段是否独立 | ❌ 共享 | ✅ 独立 | ✅ 独立 |
| string 是否独立 | ❌ 共享 | ✅ 看起来独立 | ✅ 独立 |
| 嵌套引用类型是否独立 | ❌ 共享 | ❌ 共享 | ✅ 独立 |
| 取消编辑是否安全 | ❌ 不安全 | ⚠️ 部分安全 | ✅ 完全安全 |
| 适用场景 | 共享同一对象 | 对象内全是值类型/string | 对象内有嵌套可变引用类型 |
十一、核心结论
浅拷贝和深拷贝的实用价值,不在于"会背定义", 而在于:当你需要一个与原对象互不影响的副本时,你能选对方法,用对地方。
在 WPF 开发中,它最直接解决的问题是:
- 编辑弹窗"取消"后原对象没有被污染
- 撤销/重做历史快照不会被后续操作破坏
- 复制列表项时两份数据真正独立
- 修改共享资源时不影响全局