从深克隆说起:我是如何重新理解"第三方库
从深浅克隆的一个困惑,到重新理解"第三方库的本质"
一个让我怀疑自己"没搞懂"的输出结果
前几天我在测试深克隆时,写了这样一段代码:
csharp
var original = new Person
{
Name = "John",
BirthDate = new DateTime(1980, 10, 20),
Address = new Address { City = "New York" }
};
// 浅克隆
var clone = original.MemberwiseClone() as Person;
clone.Name = "Jane";
clone.BirthDate = new DateTime(1990, 10, 20);
clone.Address.City = "Los Angeles";
我预想的结果是:浅克隆嘛,引用类型共享,所以 Address.City 会跟着变。
但实际输出是:
text
Original: Name=John, BirthDate=1980/10/20, City=Los Angeles
Cloned: Name=Jane, BirthDate=1990/10/20, City=Los Angeles
等等。Name和BirthDate都没变,但City变了。
这就奇怪了——如果浅克隆是"引用类型共享",为什么同样是引用类型的 string(Name)没有跟着变?我是不是把深浅克隆的定义记反了?
带着这个困惑,我开始一步步验证。
浅克隆到底"浅"在哪里?
重新梳理后发现:浅克隆的核心问题,只针对"可变的、自定义的引用类型"。
| 字段类型 | 浅克隆行为 | 修改克隆是否影响原对象 |
|---|---|---|
| 值类型(int、DateTime、struct) | 逐字节复制,内存独立 | ❌ 不影响 |
| string(不可变引用类型) | 复制引用,但无法修改原对象 | ❌ 不影响(重新赋值 = 换引用) |
| 可变引用类型(class、数组、集合) | 复制引用,指向同一对象 | ✅ 会影响 |
string 为什么特殊?
字符串是引用类型(class),但它是不可变的(immutable)。clone.Name = "Jane" 并不是修改了原来的 "John" 对象,而是在堆上新建了一个字符串 "Jane",然后让 clone.Name 指向它。原对象的 Name 仍然指向 "John",所以看起来没受影响。
DateTime 为什么不受影响?
DateTime 是值类型(struct),浅克隆时直接复制了整块内存。克隆对象和原对象各有一份独立的 DateTime 数据,天然互不影响。
所以真正的验证深克隆的方法很简单:
修改克隆对象的可变引用类型字段,看原对象的对应字段是否跟着变。
在上面的例子里,clone.Address.City = "Los Angeles" 让原对象的 City 也变成了 Los Angeles——这就证明了 MemberwiseClone() 是浅克隆。
那接下来问题就变成了:我该怎么实现深克隆?
实现深克隆的几种思路
方案一:手写递归克隆
csharp
public Person Clone()
{
return new Person
{
Name = this.Name,
BirthDate = this.BirthDate,
Address = new Address { City = this.Address.City }
};
}
问题:每个类都要写一遍,嵌套一深代码量爆炸。
方案二:反射遍历属性
csharp
foreach (var prop in type.GetProperties())
{
// 递归处理...
}
问题:代码复杂,循环引用难处理,性能也一般。
方案三:序列化/反序列化
csharp
string json = JsonConvert.SerializeObject(this);
return JsonConvert.DeserializeObject<T>(json);
看到这个方案时,我的第一反应是:用序列化工具来做克隆,是不是有点"取巧"?
但仔细一想,这个逻辑其实很自洽:序列化是把对象拆解成文本,反序列化是从文本重建对象——拆解→重建,中间所有引用关系都被切断了。这恰好就是深克隆需要的效果。
一个"取巧"方案,和一个值得注意的边界
我最终写下了这样的代码:
csharp
public class TangdaoClone<T>
{
public T Clone()
{
var settings = new JsonSerializerSettings
{
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
string json = JsonConvert.SerializeObject((T)this, settings);
return JsonConvert.DeserializeObject<T>(json, settings);
}
}
// 使用方式
public class Person : TangdaoClone<Person>
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public Address Address { get; set; }
}
var clone = original.Clone();
但这里有一个需要注意的地方:ReferenceLoopHandling.Ignore 遇到循环引用时会静默跳过相关字段。如果对象图里有循环依赖,数据会丢失,而且你不会收到任何提示。
比如这种情况:
csharp
class Person
{
public string Name { get; set; }
public Person Father { get; set; }
}
var john = new Person { Name = "John" };
var tom = new Person { Name = "Tom", Father = john };
john.Father = tom; // 循环引用
用上面的 JSON 方案克隆 tom,Father 字段会被静默丢弃。所以这个方案适用于没有循环引用的纯数据对象——这也是日常业务中最常见的场景。如果对象图有循环依赖,需要改用其他方案(比如手写递归 + 引用标记)。
灵光一现:Mapster 也能做?
就在我自以为想清楚的时候,我又想起了另一个库:Mapster。
我平时用它做 DTO 转换:
csharp
var dto = entity.Adapt<PersonDto>();
然后我突然闪过一个念头:那它能做深克隆吗?
验证代码:
csharp
using Mapster;
var original = new Person
{
Name = "John",
BirthDate = new DateTime(1980, 10, 20),
Address = new Address { City = "New York" }
};
var clone = original.Adapt<Person>();
clone.Name = "Jane";
clone.BirthDate = new DateTime(1990, 10, 20);
clone.Address.City = "Los Angeles";
Console.WriteLine($"Original City: {original.Address.City}"); // New York
Console.WriteLine($"Cloned City: {clone.Address.City}"); // Los Angeles
输出:
text
Original City: New York
Cloned City: Los Angeles
完全可以。
一个做 DTO 转换的库,一个做 JSON 序列化的库,都能做深克隆。为什么?
看穿工具的"本质能力"
我把两个库放在一起对比:
| 库 | 文档里的定位 | 它本质上在做什么 |
|---|---|---|
| Newtonsoft.Json | JSON 序列化/反序列化 | 对象 → 文本 → 对象 |
| Mapster | DTO 对象映射 | 对象A → 遍历成员 → 对象B |
中间那层东西不同——一个是文本,一个是内存拷贝——但终点完全一样:
生成一个全新的、独立的对象。
那一刻我才真正理解了一个道理:
任何一个能"递归访问对象所有成员"的工具,理论上都可以用来做深克隆。
不管是拆成 JSON、XML,还是拆成字段列表逐一遍历,本质都是在"复制对象图"。
更关键的是,这个认知让我重新审视了所有我每天在用的第三方库。
一个思维转变:从"怎么用"到"为什么能这么用"
以前我看一个库,只看它文档里写的用途:
- Newtonsoft.Json → 序列化 JSON
- Mapster → DTO 转换
- AutoMapper → 对象映射
- XmlSerializer → 序列化 XML
现在我会多问一句:它本质上是在做什么?
比如:
Newtonsoft.Json:对象 ↔ 文本Mapster:成员遍历 + 赋值System.Text.Json:对象 ↔ 文本(和 Newtonsoft 本质相同)XmlSerializer:对象 ↔ XML 文本
一旦理解了工具的"本质能力",你就不会只把它用在文档写的那几个场景里。
- 序列化工具可以做深克隆
- 映射工具可以做深克隆
- 日志工具可以串联出调用链
- 配置工具可以生成代码模板
这个认知是怎么来的?
回头看我自己的学习路径:
| 阶段 | 状态 | 标志 |
|---|---|---|
| 会用 API | 知道 JsonConvert.SerializeObject() 怎么调用 |
|
| 会配置 | 知道 ReferenceLoopHandling.Ignore 是干什么的 |
|
| 会验证 | 能用代码证明 string 和 DateTime 为什么特殊 |
|
| 会发现 | 看到 Mapster 也能做同样的事 | |
| 会抽象 | 理解"能递归访问对象成员的工具都能做深克隆" |
最后一步才是真正的"深入理解"。不是记住了多少 API,而是能从一个工具的使用经验中,提炼出通用的原理,然后用这个原理去理解其他工具。
结语
我们每天在使用大量第三方库。有的用了一两年,还停留在"复制粘贴示例代码"的阶段;有的只用了一周,就已经能举一反三。
区别不在于用了多久,而在于有没有问过那个问题:
"它本质上在做什么?"
从深克隆这个具体问题出发,我不仅搞清楚了深浅克隆的区别,更意外地获得了一种重新理解"第三方库"的视角。
感谢我在排查深克隆问题时遇到的每一个"为什么"——正是它们,带我走到了这里。

浙公网安备 33010602011771号