Skip to content

第 1 章 为什么需要 Serde:M×N 问题与 M+N 解法

1.1 一个被所有人忽视的工程灾难

在 2016 年以前,Rust 社区的数据序列化是一场灾难。

想象你在写一个后端服务:从 HTTP 请求里读 JSON、把配置文件写成 TOML、用 MessagePack 和另一个微服务通信、把热数据写进 Bincode 格式的缓存文件。你定义了 30 个业务数据结构——UserOrderProductConfigEvent……然后你遇到了第一个问题:

每一个数据结构,都要为每一种格式写一份序列化代码。

30 个结构体 × 5 种格式 = 150 份几乎重复的胶水代码。这还没完。当业务变化、User 结构多了一个字段,你得去 5 个地方同步修改。任何一处漏改,就是一个运行时 bug。

这个问题有个正式的名字,叫 M×N 问题——M 种数据结构、N 种格式,实现复杂度是 M 乘以 N。2016 年以前的 Rust 生态就陷在这里。那时有一个叫 rustc_serialize 的标准库内置解法,但它有几个致命缺陷:只支持 JSON 一种格式、无法扩展自定义格式、错误处理粗糙、性能远不及手写。社区开始意识到,如果 Rust 想成为一门严肃的系统编程语言,必须先解决序列化这个底层问题。

2016 年 5 月(第一个稳定发布),一位名叫 David Tolnay 的工程师发布了 Serde 1.0。十年后的今天,Serde 是 crates.io 下载量最高的几个 crate 之一(和 synquote 等过程宏基础设施一起位列 Top 10,具体排名可在 crates.io 首页的 "Most Downloaded" 查证),几乎所有 Rust 项目都直接或间接依赖它。它定义了 Rust 生态如何处理序列化,并且在过程中意外地成为 Rust 过程宏系统最重要的案例。

本章要回答一个简单的问题:Serde 是怎么把 M×N 变成 M+N 的? 这个看似朴素的"数学公式转换"背后,藏着 Rust 零成本抽象最精彩的一次实战。

1.2 M×N 灾难的真实样貌

让我们先把"M×N 问题"具象化。假设没有 Serde,你需要为 User 结构体支持 JSON 和 Bincode 两种格式:

rust
// 没有 Serde 的朴素实现——仅为说明问题,不是真实 API
struct User {
    id: u64,
    name: String,
    email: String,
}

// JSON 编码
impl User {
    fn to_json(&self) -> String {
        format!(
            r#"{{"id":{},"name":"{}","email":"{}"}}"#,
            self.id, self.name, self.email
        )
    }

    fn from_json(s: &str) -> Result<User, String> {
        // ... 手写 JSON parser,解析三个字段
        todo!()
    }
}

// Bincode 编码(假设格式:u64 + u32长度+字节 + u32长度+字节)
impl User {
    fn to_bincode(&self) -> Vec<u8> {
        let mut out = Vec::new();
        out.extend_from_slice(&self.id.to_le_bytes());
        out.extend_from_slice(&(self.name.len() as u32).to_le_bytes());
        out.extend_from_slice(self.name.as_bytes());
        out.extend_from_slice(&(self.email.len() as u32).to_le_bytes());
        out.extend_from_slice(self.email.as_bytes());
        out
    }

    fn from_bincode(bytes: &[u8]) -> Result<User, String> {
        // ... 手写二进制 parser
        todo!()
    }
}

这段代码有三个问题:

第一,大量重复。 to_jsonto_bincodefrom_jsonfrom_bincode 四个方法做的事情本质上一样——把 User 的三个字段按某种规则写出去,或按某种规则读回来。规则变了,但"读三个字段"的动作没变。每次加字段、改字段名,四个方法都要修改。

第二,难以扩展。 如果要加 YAML 支持,你得再写两个方法 to_yamlfrom_yaml。如果有 30 个结构体要支持 5 种格式,那就是 30 × 5 × 2 = 300 个方法

第三,类型不安全。 from_json 返回 Result<User, String>,错误类型是 String——你失去了所有结构化的错误信息。调用者无法区分"字段缺失"和"类型不匹配"。

这就是 M×N 灾难。用一张图看得更清楚:

每一条连线代表一份实现代码。M 个结构体连向 N 种格式,一共 M×N 条线。当 M=30、N=5,总线数是 150——而且每加一种格式,要新画 30 条线;每加一个结构体,要新画 5 条线。

1.3 其他语言怎么解决这个问题

M×N 问题不是 Rust 独有的。所有支持多种序列化格式的语言都遇到过。让我们看一下其他主流语言的解法,才能理解 Serde 为什么选择了一条和它们都不同的道路。

Java / Python / Go:反射

Java 的 Jackson、Python 的 pickle、Go 的 encoding/json 都使用同一个思路:反射(reflection)

反射的核心能力是——在运行时,程序可以"看到"自己定义的类/结构体有哪些字段、每个字段是什么类型、甚至读写私有字段。有了这个能力,一个 JSON 序列化器只需要实现一次:

go
// Go 风格伪代码
func ToJSON(obj any) string {
    t := reflect.TypeOf(obj)      // 运行时拿到类型信息
    v := reflect.ValueOf(obj)
    var out strings.Builder
    out.WriteString("{")
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)       // 每个字段的元信息
        value := v.Field(i)       // 每个字段的实际值
        out.WriteString(fmt.Sprintf("\"%s\":%v,", field.Name, value))
    }
    out.WriteString("}")
    return out.String()
}

这段代码不需要知道 User 长什么样。它在运行时查询类型信息,动态遍历字段。一份代码通吃所有结构体——M×N 被反射降成了 N(每种格式写一份反射代码即可)。

这个方案看起来完美,但对 Rust 不可接受,原因有三:

第一,运行时开销。 反射必须查询类型元数据、做动态分发、遍历字段描述表。具体开销依赖语言实现、对象形状和格式库,不应脱离基准测试给出固定百分比。Rust 的核心承诺是"零成本抽象"——不能为了方便把每一次序列化都变成运行时类型查询。

第二,Rust 不支持反射。 Rust 编译器默认不生成类型元数据(为了二进制体积和隐私)。std::any::Any 提供有限的类型 ID 查询能力,但不能枚举字段。想要真正的反射,必须靠编译期代码生成——这又回到了 serde 的路线。

第三,运行时反射抹杀类型检查。 用 Go 的 encoding/json 时,如果字段拼写错了,编译器不会提醒你,要等到运行时解析失败才发现。Rust 的类型系统是它最大的武器,不能丢。

C++:手写 + 代码生成器

C++ 社区分两派。一派是 Boost.Serialization,用模板特化(template specialization)让用户手动实现每种类型的序列化——这本质上和手写没区别。另一派是 Protobuf、Thrift、Cap'n Proto,让用户先写一个独立的 .proto/.thrift 文件定义 schema,然后由外部代码生成器(protoc)生成 C++ 代码。

代码生成路线的优点是性能极致、schema 可跨语言共享。缺点是:

  • schema 和类型系统割裂。你得在两个地方维护同样的信息:一份 .proto、一份 C++ 类。虽然生成器能从 .proto 生成 C++ 代码,但反过来不行——已有的 C++ 类型不能"自动变成" Protobuf 消息。
  • 构建流程复杂.proto 编译要在 C++ 编译之前跑,构建系统(CMake、Bazel)要专门配置。
  • 格式绑定死。每个 .proto 绑定到 protobuf 格式,想换成 JSON?重写一遍。

动态语言的启示

注意 Java/Python/Go 的反射方案有一个很诱人的隐含优点:它把"序列化器"和"数据结构"完全解耦。写 User 的人不用为序列化操心,写序列化器的人也不用为 User 操心。这是一种漂亮的抽象——一种可扩展性。

Serde 的设计者想要保留这个解耦,同时避开运行时反射的性能代价和类型不安全问题。答案是:把反射搬到编译期

1.4 Serde 的核心洞察:用中间层击碎 M×N

Serde 的解法从一个数学观察开始:M×N 问题之所以是 M×N,是因为每个数据结构直接连到每种格式。如果插入一个中间层,让所有数据结构只连到中间层,所有格式也只连到中间层,那么总线数就变成 M+N。

这个中间层叫 Data Model——Serde 定义的一套"通用数据表示"。它有 29 种原语:booli8..i64u8..u64f32f64charstrbytesoptionunitunit_structunit_variantnewtype_structnewtype_variantseqtupletuple_structtuple_variantmapstructstruct_variant 等。

所有数据结构都只需要"告诉 Data Model 自己长什么样"User 告诉 Data Model "我是一个 struct,有 3 个字段,分别是 u64、String、String"。这通过 Serialize trait 实现。

所有格式只需要"接受 Data Model 的描述、按自己的规则写出去":JSON 收到 "struct 有 3 个字段" 的描述,就写 {"field1":..,"field2":..,"field3":..};Bincode 收到同样的描述,就按二进制布局写。这通过 Serializer trait 实现。

关键洞察SerializeSerializer 解耦了。User 实现 Serialize 时,不需要知道最终是 JSON 还是 Bincode;JSON 格式实现 Serializer 时,不需要知道数据来自 User 还是 Order

有了这个中间层:

  • 新加一个数据结构(比如 Event):只要为它实现 SerializeDeserialize,所有格式自动支持。这就是 #[derive(Serialize, Deserialize)] 的威力。
  • 新加一个格式(比如 CBOR):只要实现 SerializerDeserializer,所有现有的数据结构自动支持。这就是 serde_cborrmp-serdepostcard 等格式 crate 的工作原理。

M+N 达成。

但魔鬼在细节里。这套抽象要真正 work,有三个必须回答的问题:

  1. Data Model 的 29 种原语够不够? 如果某个数据结构表达不出来,整个抽象崩溃。
  2. 抽象会不会带来性能损失? 中间层看起来多了一层开销,Rust 的零成本承诺还作数吗?
  3. 谁来为每个 User 结构体写 Serialize 实现? 如果要用户手写,M×N 只是变成了 M×(N=1),并没有真正解决问题。

下面三节分别回答这三个问题——它们对应本书后续的三条主线。

1.5 Data Model:29 种原语的取舍

Data Model 的设计本身就是一门艺术。太少的原语无法表达复杂数据;太多的原语让格式实现复杂化。Serde 最终选了 29 种。

设计意图:29 种原语的选取不是凭感觉,而是对"真实世界数据形态"的归纳。Serde 覆盖了:7 种无符号整数 + 6 种有符号整数 + 2 种浮点 + bool/char + 2 种字符串(str/bytes)+ option/unit + 4 种"命名但无数据"的变体(unit_struct、unit_variant 等)+ newtype 包装 + 4 种序列类(seq、tuple、tuple_struct、tuple_variant)+ 3 种映射类(map、struct、struct_variant)。每一种都对应 Rust 类型系统中一个常见场景。

比如为什么要区分 seq(同类型元素变长序列)和 tuple(异构类型定长)?因为 JSON 数组和 Rust 的 Vec<T> 对应 seq,而 (i32, String, bool) 这种元组在 MessagePack 里可以被编码成更紧凑的固定长度数组——格式可以利用"我知道长度提前"这个信息做优化。

再比如为什么 structmap 分开?看起来它们都是"key-value 集合"。区别是 struct 的 key 在编译期已知(字段名),map 的 key 在运行时才确定。Bincode 利用这个区别,对 struct 不写字段名(反正编译期双方都知道),省下大量字节;而对 map 就必须写 key。

Data Model 的完整设计是第 2 章的主题。这里只说结论:29 种原语足以表达 Rust 类型系统中几乎所有可序列化的形态,且每种原语都给了格式实现者利用特定信息优化的空间。

1.6 零成本:trait 与单态化的合谋

抽象有没有代价?让我们看一段简化的 Serde 调用:

rust
let user = User { id: 1, name: "alice".into(), email: "a@x.com".into() };
let json = serde_json::to_string(&user).unwrap();

to_string 内部会:

  1. 创建一个 serde_json::Serializer
  2. 调用 user.serialize(&mut serializer)
  3. user.serialize 内部会调用 serializer.serialize_struct("User", 3)?
  4. 然后依次调用 serialize_field("id", &self.id)?serialize_field("name", &self.name)?

步骤 3、4 看起来是对一个 trait 对象的方法调用。trait 方法调用有两种实现:

  • 动态分发(dynamic dispatch)&dyn Serializer,运行时查虚表。有开销。
  • 静态分发(static dispatch)<S: Serializer>,编译期单态化。零开销。

Serde 选择了静态分发。看 Serialize trait 的真实定义:

rust
// serde/serde_core/src/ser/mod.rs
pub trait Serialize {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer;
}

serialize 方法的 serializer 参数是泛型参数 S: Serializer不是 &dyn Serializer。这意味着当你写 user.serialize(json_serializer) 时,编译器会为 User + serde_json::Serializer 这个具体组合生成一份专门的代码(单态化),把所有 trait 方法调用静态化成直接函数调用,然后内联、常量传播、死代码消除一通优化——最终产物和你手写的 user.to_json() 几乎一模一样。

设计意图:这里有一个关键权衡。静态分发零开销,但每个 (数据结构, 格式) 组合都会生成一份单态化代码,二进制膨胀。Serde 选了零开销、容忍膨胀——对系统编程来说这是正确的取舍。想要动态分发的用户可以用 erased-serde 这个 crate,它把 Serde 包装成 trait 对象,代价是 10-20% 性能损失。

单态化 + 内联的组合是第 18 章的主题。那一章会给出汇编证据:serde_json::to_string(&user) 在 release 模式下编译出的代码,和手写 format!(...) 几乎字节相同。

1.7 derive 宏:让用户不再手写

M+N 的最后一个拼图是——谁来为 30 个业务结构体写 Serialize 实现?

如果要用户手写:

rust
impl Serialize for User {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut state = serializer.serialize_struct("User", 3)?;
        state.serialize_field("id", &self.id)?;
        state.serialize_field("name", &self.name)?;
        state.serialize_field("email", &self.email)?;
        state.end()
    }
}

这段代码的每一行都是机械的——它完全由 User 的字段结构决定。我们可以让编译器自动生成它。这就是过程宏的用武之地:

rust
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

一行 #[derive(Serialize)] 告诉编译器:"请扫描 User 的字段,为我生成上面那段代码"。

具体怎么生成?Serde 提供了一个独立 crate serde_derive,它注册了一个过程宏:

rust
// serde/serde_derive/src/lib.rs:59
#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(input as DeriveInput);
    ser::expand_derive_serialize(&mut input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

当编译器看到 #[derive(Serialize)],它把 User 的定义(完整的 TokenStream)传给 derive_serialize 函数,函数返回新的 TokenStream——也就是 impl Serialize for User { ... } 那段代码。编译器把这段代码插入当前 crate,继续编译。

整个过程发生在编译期。 运行时没有任何 "derive" 的痕迹——只有普通的 trait 实现代码。这就是为什么 Serde 能既有"反射级别的便利"又保持"零开销"。

第 5-9 章会完整讲过程宏工具链;第 10-13 章会拆解 serde_derive 的实现细节。这里只需要记住一个事实:Serde 用过程宏把"手写 Serialize 实现"这件事自动化了,完成了 M+N 解法的最后一块拼图。

1.8 三个基石:Data Model、trait、过程宏

把前三节串起来,Serde 的架构可以画成这张图:

三个基石:

  1. Data Model(第 2-4 章):29 种原语定义了"所有可序列化数据的通用表示"。SerializeSerializer trait 通过这套表示通信。
  2. 过程宏(第 5-14 章):serde_derive 在编译期读取用户定义的数据结构,自动生成 Serialize/Deserialize 实现。用户只写一行 #[derive(...)]
  3. 零成本抽象(贯穿全书,第 18 章总结):trait 泛型 + 单态化 + 内联让整套抽象在编译后消失,产物和手写代码性能无差别。

这三者缺一不可:

  • 没有 Data Model,Serialize 没有统一接口,M×N 回归。
  • 没有过程宏,用户要手写 M 份实现,等于 M×(N=1),收益减半。
  • 没有零成本抽象,中间层的性能代价会让用户抛弃 Serde 转向手写。

1.9 Serde 和它的竞争者们

Serde 不是 Rust 里唯一的序列化方案。理解竞争格局能帮你判断什么时候用 Serde、什么时候不用。

Prost / protobuf-rust:如果你必须用 Protobuf 格式(比如跨语言 RPC),Prost 是比 serde-protobuf 更好的选择——它基于 .proto 文件生成 Rust 类型,类型和 schema 一一对应。但它绑死 Protobuf,换格式重来一遍。

rkyv:零拷贝反序列化专用库。把二进制数据直接映射为 Rust 结构体,完全跳过"解析"步骤。据 rkyv 官方 benchmark 和社区测评,针对大 payload 反序列化性能可以达到 Serde + Bincode 的数十倍(具体倍数视场景波动极大,读者应以自己场景实测为准),代价是格式固定、不兼容任何其他语言。适合 Rust-only 的高性能缓存。

手写 nom/winnow:对于协议实现(HTTP/2 帧、Postgres wire protocol),Serde 不合适——这些协议有复杂的控制流,不是简单的 struct-to-bytes。nom 是 Rust 最流行的 parser combinator 库,适合这类场景。

Serde 的统治领域:应用层数据交换——JSON API、配置文件、缓存、消息队列、数据库行映射。在这些场景里,Serde 是事实标准,没有竞争者。

一个实用判断:如果你的数据可以用 struct + enum + Vec + HashMap 表达,并且你可能换格式,用 Serde。如果不满足(协议解析、极致零拷贝、单一格式锁定),考虑专用方案。

1.9.1 M+N 真实成本:把"中间层"放到秤上称一称

§1.4 给出 Serde 的核心公式 M+N——本章前半反复说"消除 M×N 灾难"——但**这个中间层本身值多少行代码?**实测把 §1.8 提到的"三个基石"对应到具体 crate——

基石crate真实行数
Data Model + traitserde_core (含 ser/ + de/)11139(ser/ 3441 + de/ 7698,ch04 §4.10.1 实测)
过程宏(用户侧自动生成)serde_derive8969(ch10 §10.2 实测)
工具链:TokenStream/quoteproc-macro26030(ch06 §6.11.1 实测 v1.0.106)
三者合计约 26000 行

这就是 "M+N" 的真实物理重量——#[derive(Serialize, Deserialize)] 这一行用户代码的背后、约 26000 行基础设施在支撑。但比较一下"如果不用 Serde"的代价——

  • M = 50 个用户类型、N = 8 种格式(JSON/YAML/TOML/MessagePack/CBOR/Bincode/Postcard/Avro)
  • 每对 (T, F) 平均 手写 80 行(包含 schema、解析、错误处理)
  • M×N 路线:50 × 8 × 80 = 32000 行用户代码
  • M+N 路线:50 × 30 行 derive 注解 + 8 × 1 个格式 crate 引入 = 大约 1500 行用户代码 + 26000 行 Serde 基础设施(写一次、所有用户共享)

对单个用户:0.05% 的基础设施成本(26000 / 50_user_projects = 520 行/项目摊销)换来 30x 用户代码减少(1500 vs 32000)。

对整个生态:26000 行只写一次,节省下来的是全 Rust 生态所有用户的 M×N 工程量——按 crates.io 上 100K+ 个用 serde 的 crate 估算、节省的总代码量是 26000 的 几个数量级

这条计算让 §1.4 "M+N 击碎 M×N" 从口号变成账本——任何质疑"中间层值不值得"的人都能拿这组数字算 ROI。这也解释了为什么 Serde 从 2017 一路到现在十年保持核心 API 不变(§3.11)——26000 行基础设施破坏不起,社区会把任何 breaking change 推回去

1.9.2 三个 crate 把 M+N 固化成源码边界

M+N 不是一个口号,它在源码里被切成三条边界:

边界源码锚点负责的事不负责的事
serde / serde_coreserde/src/lib.rs:252-254 重导出 SerializeSerializerDeserializeDeserializer定义 Data Model 和 trait 协议不知道 JSON、Bincode、YAML 的字节格式
serde_deriveserde_derive/src/lib.rs:113-124 注册 Serialize / Deserialize 两个 derive 入口读取用户类型,生成 trait impl不解析 JSON,不执行用户业务逻辑
serde_jsonserde_json/src/lib.rs:396-406 暴露 from_strto_stringValue 等格式 API把 Serde Data Model 映射到 JSON 字节和 Value不关心用户结构体字段如何被宏展开

这个分工解释了为什么 Serde 能长期稳定。serde/src/lib.rs:268-279 只是把 derive 宏作为可选 feature 重导出,核心 trait 本身不依赖过程宏;serde_derive/src/lib.rs:77-80 把输入先交给 syn::parse_macro_input,说明宏入口只处理 TokenStream 到 AST 的转换;serde_json/src/lib.rs:400-404 暴露 to_string / to_writer / Serializer,说明格式 crate 的公共面是"把任何 Serialize 类型写成 JSON",而不是"认识每一个用户类型"。

工程上这带来三条直接后果。

第一,格式 crate 可以独立演化。serde_json 可以优化字符串转义、浮点格式化、错误定位,不需要修改 serde_derive。第 17 章会看到 serde_json/src/ser.rsSerializerFormatter 分层,正是这个边界的格式侧落地。

第二,宏生成代码可以保持格式无关。第 12、13 章展开 cargo expand 时,生成代码调用的是 serialize_structdeserialize_structserialize_fieldMapAccess::next_value 这些 trait 方法,而不是 write('{')parse_string()。这意味着同一份 derive 结果可以喂给 JSON、MessagePack、CBOR。

第三,用户依赖可以按需裁剪。只写手工 impl 的库可以依赖 serde 而不开启 derive;需要 JSON 才引入 serde_json;需要宏才启用 serde = { features = ["derive"] }。M+N 的真正价值不只是减少代码量,而是让"类型协议、代码生成、格式实现"三者可以分别测试、分别发布、分别优化。

还有一个经常被忽略的收益:错误隔离。用户类型写错属性,错误来自 serde_derive;JSON 输入不合法,错误来自 serde_json;某个格式不支持 map 的非字符串 key,错误来自格式 crate。边界清楚,排查路径就短。反过来,如果把反射、格式解析、类型规则塞进一个运行时大框架,用户看到的往往只是"serialization failed",很难判断是类型声明、格式语法还是协议约束出了问题。Serde 的 M+N 不是把复杂度消灭,而是把复杂度放到可定位的层里。

因此,本书后面读源码时会始终沿着这三层走:先看 trait 协议,再看 derive 如何生成协议调用,最后看格式 crate 如何兑现协议约束。

1.10 本章小结与预告

本章的核心是一个公式:

Serde = Data Model(中间层) + trait(抽象边界) + 过程宏(代码生成)

这三样东西把序列化的工程复杂度从 M×N 降到 M+N。每一部分都不是 Serde 发明的——中间层思想来自操作系统的分层设计,trait 泛型是 Rust 原生特性,过程宏是编译器提供的能力。Serde 的天才之处在于把它们拼在一起,做到了其他语言都做不到的事情:运行时零开销 + 编译期类型安全 + 用户零负担

后续章节的组织如下:

  • 第 2 章 深入 Data Model,列出全部 29 种原语,解释每一种的设计意图。
  • 第 3、4 章 拆解 Serialize/Serializer/Deserialize/Deserializer 四个核心 trait 的 API 设计。为什么 Serializer trait 内有 30 个方法(ch03 §3.3 实测:23 个一次性 + 7 个状态机入口)、再加 7 个 SerializeXxx 子 trait 共 9 个 serialize_* 方法(ch03 §3.11.1)= 用户实现 Serializer 总负担 39 个方法(ch04 §4.10.1)?为什么 Deserializer 是 31 个方法、Visitor 是 27 个?Visitor 模式从哪冒出来的?
  • 第 5-9 章 从零开始建立过程宏知识体系。TokenStream 是什么、syn 如何解析、quote 如何生成、如何写第一个可工作的 derive 宏。
  • 第 10-14 章serde_derive 源码。你会看到本章描述的理论在真实代码里如何落地。
  • 第 15-16 章 高阶主题——借用反序列化(为什么 &'de str 能零拷贝?)、#[serde(with)]remote 的实现。
  • 第 17 章 serde_json 源码:一个格式 crate 是如何把自己接入 Serde Data Model 的。
  • 第 18 章 总结 Serde 的设计哲学,并提炼出你可以用在自己项目里的模式。

动手实验

完成以下实验能帮你建立第 1 章的直觉:

  1. 安装 cargo-expandcargo install cargo-expand。这是阅读本书最重要的工具。
  2. 最小复现:新建一个 crate,cargo new --lib serde-demo。在 Cargo.toml 加上 serde = { version = "1", features = ["derive"] }serde_json = "1"
  3. 观察 derive 展开:在 src/lib.rs 写:
    rust
    use serde::Serialize;
    
    #[derive(Serialize)]
    pub struct User {
        pub id: u64,
        pub name: String,
    }
    然后运行 cargo expand。你会看到 #[derive(Serialize)] 实际展开成了 50 多行代码——一个完整的 impl Serialize for User 块。把它抄下来,这就是你的第一份 Serde 源码研究样本。
  4. 思考题:如果把 u64 改成 Vec<u64>,展开的代码会变成什么样?为什么?(提示:Vec 不是 struct 字段,它自己会被序列化成 seq。)

延伸阅读

基于 VitePress 构建