浅拷贝仅复制引用而非对象本身,C#中唯一内置方式是MemberwiseClone(),它对值类型复制新值、引用类型只复制地址,导致子对象共享;需谨慎用于DTO映射、UI编辑副本等场景。

浅拷贝就是复制引用,不是复制对象本身
在 C# 里,MemberwiseClone() 是唯一内置的浅拷贝方式,它只复制字段值:值类型字段被复制一份新值,引用类型字段只复制引用地址——新旧对象指向同一个子对象。这意味着改 objB.Name 可能意外影响 objA.Name,如果 Name 是一个可变的 StringBuilder 或自定义类实例。
常见错误现象:objB.DataList.Add(item) 后,objA.DataList.Count 也变了;调试时发现两个对象的 Id 相同但 IsDirty 状态不一致,却始终同步变化。
- 只对 class(且非 sealed)有效,struct 不支持
MemberwiseClone() - 必须是 public 或 protected 方法调用,不能在外部直接对任意对象调用
- 不会触发任何构造函数或
ICloneable.Clone()实现,纯内存位拷贝
怎么安全地用 MemberwiseClone
别裸用。必须显式实现 ICloneable 或自定义 Clone() 方法,并在内部调用 MemberwiseClone(),否则语义不清、IDE 警告多,且别人无法直觉判断这是浅拷还是深拷。
使用场景:DTO 映射前临时隔离状态、UI 层快速生成编辑副本、单元测试中构造相似但独立的测试对象。
- 类必须声明为
public class MyClass,不能是internal或private嵌套类(否则MemberwiseClone()报MethodAccessException) - 若含
readonly字段,MemberwiseClone()仍会复制其值,但后续无法通过属性修改——这点常被忽略 - 返回类型建议写具体类型而非
object,避免强制转换和类型丢失
public class Person : ICloneable
{
public string Name { get; set; }
public List<string> Hobbies { get; set; } // 引用类型 → 共享同一 List
<pre class="brush:php;toolbar:false;">public Person Clone() => (Person)MemberwiseClone();
}
浅拷贝 vs new + 手动赋值,选哪个
手动 new + 赋值看起来啰嗦,但控制力强:你可以决定哪些字段要“真正复制”,哪些就该共享,哪些要特殊处理(比如跳过缓存字段、重置时间戳)。而 MemberwiseClone() 是全量字段无差别复制,容易把不该拷的也拷了。
性能上,MemberwiseClone() 略快(底层是内存块复制),但差异微乎其微,别为这点省略可维护性。
- 含事件委托字段?
MemberwiseClone()会复制委托链,可能引发重复触发或内存泄漏 - 有
IDisposable成员?浅拷后两个对象共用一个资源,Dispose()调一次就废 - 字段是
string?安全,因为string不可变,共享没问题
为什么别用 BinaryFormatter 做浅拷贝
BinaryFormatter 本质是序列化,它默认做的是“深度图拷贝”(deep graph clone),不是浅拷。即使对象图里只有引用,它也会递归序列化整个引用链——这既慢又危险(.NET 5+ 已标记为 obsolete,反序列化有严重安全风险)。
有人误以为加 [NonSerialized] 就能模拟浅拷,但实际行为不可控:未标记字段仍可能因间接引用被拉入序列化范围,而且它完全绕过了类型安全性与构造逻辑。
- 在 .NET Core / .NET 5+ 中,
BinaryFormatter默认禁用,启用需额外配置且不推荐 - 哪怕降级到 .NET Framework,只要类含任何不可序列化成员(如
Thread、DbContext),就会抛SerializationException - 真正的浅拷贝不该依赖序列化机制——那是深拷贝的思路
浅拷贝的关键不在“怎么写那一行代码”,而在“你是否清楚每个字段的生命周期和所有权”。一不留神,共享的引用就成了并发修改的雷区,或者 GC 时悬空引用的源头。










