从深克隆说起:我是如何重新理解"第三方库

从深浅克隆的一个困惑,到重新理解"第三方库的本质"


一个让我怀疑自己"没搞懂"的输出结果

前几天我在测试深克隆时,写了这样一段代码:

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 方案克隆 tomFather 字段会被静默丢弃。所以这个方案适用于没有循环引用的纯数据对象——这也是日常业务中最常见的场景。如果对象图有循环依赖,需要改用其他方案(比如手写递归 + 引用标记)。


灵光一现: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 是干什么的
会验证 能用代码证明 stringDateTime 为什么特殊
会发现 看到 Mapster 也能做同样的事
会抽象 理解"能递归访问对象成员的工具都能做深克隆"

最后一步才是真正的"深入理解"。不是记住了多少 API,而是能从一个工具的使用经验中,提炼出通用的原理,然后用这个原理去理解其他工具。


结语

我们每天在使用大量第三方库。有的用了一两年,还停留在"复制粘贴示例代码"的阶段;有的只用了一周,就已经能举一反三。

区别不在于用了多久,而在于有没有问过那个问题:

"它本质上在做什么?"

从深克隆这个具体问题出发,我不仅搞清楚了深浅克隆的区别,更意外地获得了一种重新理解"第三方库"的视角。

感谢我在排查深克隆问题时遇到的每一个"为什么"——正是它们,带我走到了这里。

posted @ 2026-09-08 16:46  孤沉  阅读(4)  评论(0)    收藏  举报