目录

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

一、先搞清楚三个容易混淆的概念

很多人其实把这三件事混在一起了:

1. 引用复制

var b = a;

这不是拷贝。 如果 a 是类对象,ba 指向的是同一个对象。 改 b.Namea.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 和值类型可变引用类型 的区别:

字段类型 浅拷贝后是否独立 原因
intboolstruct 等值类型 ✅ 独立 值类型直接存储值,拷贝后各自持有独立的副本
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 是引用类型,p1p2 共享同一个 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 的资源字典中的对象通常是共享实例(如 BrushTransform)。 直接修改会影响所有使用该资源的地方:

// ❌ 错误:直接修改共享资源
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 控件对象

WindowUserControlButtonDependencyObject 不是普通 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 开发中,它最直接解决的问题是:

  • 编辑弹窗"取消"后原对象没有被污染
  • 撤销/重做历史快照不会被后续操作破坏
  • 复制列表项时两份数据真正独立
  • 修改共享资源时不影响全局