MemberwiseClone() 只做浅拷贝,值类型复制值、引用类型仅复制地址,导致原对象与克隆体共享 List、Equipment 等引用实例;适用场景极窄,仅限全值类型或明确需共享状态的情形。

MemberwiseClone() 为什么不是“能用就行”的克隆
MemberwiseClone() 是 object 的受保护方法,它只做浅拷贝:值类型字段复制值,引用类型字段只复制地址。这意味着两个对象共享同一份 List<string></string>、同一个 Equipment 实例、甚至同一个 Stream。
常见错误现象:
- 克隆后修改
clone.Items.Add("new"),original.Items也多了一项 - 调用
clone.Weapon.Durability--,原对象的武器耐久度同步下降 - 在文档编辑器中 Undo 时,上一版文档的图片列表被当前编辑污染
这不是 bug,是设计行为。它适用的场景极窄:class 完全由值类型组成,或你明确希望共享内部状态(比如共用一个缓存 ConcurrentDictionary)。
record with 表达式替代传统克隆
C# 9+ 的 record 天然适合原型模式的“基于旧值构造新值”语义,且编译器保证字段级不可变性(除非你显式声明 public set 或可变引用字段)。
使用条件:
- 模型结构稳定、适合不可变语义(如 DTO、配置快照、事件载荷)
- 不需要运行时动态决定克隆深度(
with固定为字段级复制) - 不依赖
ICloneable接口契约(比如第三方库强制要求传ICloneable)
示例:
public record Person(string Name, int Age, List<string> Tags);
var original = new Person("Alice", 28, new() { "dev", "csharp" });
var copy = original with { Name = "Bob" }; // Tags 引用仍共享!
</string>
注意:with 不等于深拷贝。若 Tags 需隔离,得手动处理:original with { Tags = new(original.Tags) }。
必须手写 Clone() 时怎么避免漏层级
当必须返回 ICloneable、或类型无法改造成 record、或含不可序列化资源(如 Socket、FileStream)时,就得手写深拷贝逻辑。
关键原则:
- 每个引用类型字段都要显式新建实例并填充内容(
new List<t>(source)</t>、source.Select(x => x.Clone()).ToList()) - 嵌套对象必须递归调用其
Clone()方法,不能漏层级 - 避免用
JsonSerializer.Serialize/Deserialize处理含循环引用、非public字段、或[JsonIgnore]的类型
典型写法:
public class Person : ICloneable
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
<pre class="brush:php;toolbar:false;">public object Clone()
{
var clone = (Person)this.MemberwiseClone();
clone.Address = (Address)this.Address.Clone(); // 显式递归
return clone;
}}
public class Address : ICloneable { public string Street { get; set; } public string City { get; set; } public object Clone() => this.MemberwiseClone(); // 叶子节点,浅拷贝安全 }
序列化深拷贝的三个硬限制
用 BinaryFormatter 或 JsonSerializer 做深拷贝看似省事,但有三处容易翻车:
-
BinaryFormatter已被标记为过时,且要求所有类型都加[Serializable];JsonSerializer无法处理private字段或[JsonIgnore]成员 - 含循环引用的对象会直接抛
InvalidOperationException(如父子双向导航属性) - 任何未标记
[Serializable]或不可序列化的字段(如FileStream、EventWaitHandle)会导致运行时异常
如果必须走序列化路线,优先选 System.Text.Json 并启用 ReferenceHandler.Preserve,但仍需提前检查字段可序列化性——这比手写 Clone() 更难排查。
真正难的不是“怎么克隆”,而是判断“哪些字段该深、哪些该浅、哪些根本不能碰”。尤其是当对象里混着配置、缓存、IO 资源和业务数据时,克隆逻辑很容易变成隐性耦合的温床。











