Serde 元编程
第 18 章 Serde 的设计哲学与可迁移模式
18.1 走到这里,你带走什么
翻开这个专栏,你看到的是一个具体问题——Rust 如何做序列化。翻到最后一章,你应该带走的不是 "Serde API 清单",而是工程哲学——一套可以迁移到任何复杂系统设计的思维范式。
前 17 章把 Serde 的每一块拆开给你看:Data Model、trait 设计、过程宏、代码生成、属性系统、生命周期、格式实现。每一章都是"Serde 怎么做这件事"。本章换一个问题:Serde 为什么这么做,其他人能学什么?
这个问题的答案是本专栏最重要的部分——也是最可能帮到你的部分。因为你很可能一辈子都不会再写一个 Serde,但你几乎肯定会设计"多输入多输出的转换系统"、"代码生成工具"、"扩展点友好的库"——这些场景下 Serde 的设计思想直接适用。
18.2 设计哲学一:用抽象击碎 M×N
第 1 章讲过 Serde 的核心武器是 Data Model 这个中间层,把 M×N 问题变成 M+N。这不是 Serde 发明的——它是软件工程几十年积累的核心模式。但 Serde 把它在 Rust 里做到了极致,以至于成为社区事实标准。
模式总结:当你面对 "M 种 X × N 种 Y" 的组合爆炸时,设计一个中间抽象 Z,让 X 和 Y 都通过 Z 通信。
应用场景:
- 日志系统:M 种日志源(应用、数据库、网络)× N 种输出(文件、stdout、syslog、网络聚合)→ 中间层是结构化日志事件。
- 构建工具:M 种语言源代码 × N 种打包格式(zip、tar、docker、deb)→ 中间层是"构建产物描述"。
- 消息中间件:M 种生产者 × N 种消费者 → 中间层是统一消息格式。
- ORM:M 种 SQL 数据库 × N 种 Rust 类型 → 中间层是数据库无关的值/查询描述。
关键设计决策:
- 中间层的原语数量:太多让格式实现变复杂;太少表达力不够。Serde 选了 29 种(第 2 章),够覆盖绝大多数 Rust 类型的形状。
- 中间层是"描述"而非"编码":Data Model 只描述"这是一个 struct,有 N 个字段"——不规定"用什么字节表示"。这让每种格式可以各自优化。
- 格式无关的 hooks:传递 name、variant_index 等"元信息"给格式,让它选择如何利用。
反面教训:如果 Serde 规定 "Data Model 必须按 JSON 风格编码",那 Bincode 就没存在价值——它不能用紧凑二进制。这是 过度约束中间层 的典型失败。
Serde 的中间层足够抽象、又足够结构化——这是艺术和工程的结合。
18.3 设计哲学二:类型即协议
Serde 的 trait 设计把协议写进了类型:
type Ok/type Error:返回值类型编码操作结果type SerializeXxx:关联类型编码状态机'de生命周期参数:编码数据存活期Sized:编码"不支持动态分发"(serde_core/src/ser/mod.rs:355的pub trait Serializer: Sized)for<'de>HRTB:编码"不依赖特定生命周期"(serde_core/src/de/mod.rs:632:pub trait DeserializeOwned: for<'de> Deserialize<'de> {})
结果:大量运行时才能发现的问题被 Rust 编译器提前拦截。
举例:
// 错误:试图从 IoRead 反序列化 &'de str
let s: &str = serde_json::from_reader(reader)?;
// 编译错:implementation of `Deserialize` is not general enough——&str 不是 DeserializeOwned
// (serde_json 的 src/de.rs:2614-2618 的 from_reader 签名写死了 T: de::DeserializeOwned)
// 错误:试图序列化同一个 Serializer 两次
let ok = serializer.serialize_str("a")?;
let _ = serializer.serialize_str("b")?; // 编译错:serializer 已被 move
// (serde_core/src/ser/mod.rs:720 的 serialize_str 取的是 self 而不是 &self)
这些本来可能是运行时 panic 或奇怪 bug 的场景,在 Serde 里变成编译错。代价是 Rust API 确实更复杂——用户要理解 'de、Sized、DeserializeOwned 等概念——但编译通过之后,协议用错这一类问题就排除了,剩下的运行时错误主要来自数据本身(类型不匹配、缺字段,或对非自描述格式调用 deserialize_any)。
应用启示:
- 协议的"哪些调用组合合法" → 用类型系统编码,而不是运行时检查。
- 错误状态 → 用 enum 而不是 error code,让 match 强制穷尽处理。
- 资源管理 → 用 RAII(Drop),而不是"记得 close"。
Rust 的原则:把能编译期解决的问题,不要推到运行时。Serde 是这个原则的教科书示范。
《Rust 编译器与运行时揭秘》专栏贯穿讲的就是这个哲学——所有权、借用、类型推导都是"用类型系统编码协议"的具体机制。读过那个专栏再来看本章会有更深体会。
18.4 设计哲学三:静态 > 动态
第 3 章讲过 Serializer trait 的 Sized 约束和泛型参数——它禁止动态分发,强制静态。根因就写在 serde_core/src/ser/mod.rs:355 这一行:pub trait Serializer: Sized——有 Sized 这条 supertrait,dyn Serializer 根本构造不出来。
这是典型的"功能换性能"取舍:
- 动态分发 (
&dyn Serializer):灵活——运行时可以传任意 Serializer 实现;有开销——虚表查询、无法内联。 - 静态分发 (
<S: Serializer>):零开销——编译期单态化、全部内联;不灵活——Serializer类型编译期确定。
Serde 选了静态。代价是"用户不能运行时决定用哪个格式"(需要 erased-serde 这个额外库绕过)——但换来生产级性能。对"序列化"这种每个字段都要过一遍的高频调用路径,虚表调用和内联失效的代价会被放大。
应用启示:
- 库 API 的默认应该是静态分发,让使用者享受零开销;只在确实需要运行时多态的地方提供 trait object 版本。
- 过程宏的生成代码应该单态化友好——用泛型方法、内联提示,让编译器能消除抽象。
反面案例:很多动态语言移植到 Rust 的库(特别是早期 ORM、配置库)不假思索地用 &dyn Trait——结果因为虚表查询和内联失效,性能比 monomorphic 版本差(差多少取决于调用密度,要以实测为准)。Rust 的零成本承诺是静态分发优先的结果。
18.5 设计哲学四:把复杂度留给库实现者
用户用 Serde 时的典型代码:
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}
let json = serde_json::to_string(&user)?;
let user: User = serde_json::from_str(&json)?;
这就是用户需要知道的全部。寥寥几行代码,没有任何 impl Trait、没有 trait object、没有生命周期标注。
代价在 Serde 实现方:
- 8969 行的 derive 宏(
serde_derive/src/全部.rs实测,第 10 章 §10.2) - 复杂的 trait 层次(Serializer/SerializeSeq/Visitor/DeserializeSeed/...)
- where 子句推导逻辑:
serde_derive/src/bound.rs:98的with_bound按字段类型推断该给哪些泛型参数加T: Serialize,serde_derive/src/bound.rs:354的with_lifetime_bound再补'de - 错误累积机制(Ctxt)
- 临时 struct 包装(SerializeWith)
这是最重要的哲学——"库实现复杂一次,用户简单万次"。
应用启示:
- 评估 API 好坏,看用户代码的平均行数,不是库内代码行数。
- 愿意为了用户简洁,付出实现复杂度的代价。
- 隐藏尽可能多的细节——让用户只看到"做我要做的事"那条路径。
反面案例:过度抽象的库经常让用户代码变长。比如某些"通用" 消息处理库,用户要写 Message::new(MessageType::Text).with_payload(payload).with_metadata(meta).validate().serialize::<JsonFormat>()——每个函数都通用,用户每次都要关心十件事。Serde 的 serde_json::to_string(&x) 一行完事。
18.6 设计哲学五:扩展点 vs 稳定核心
Serde 1.0 从 2017 年发布,至今 API 几乎没有破坏性变化。同时它又极其灵活——用 #[serde(with)] 可以定制任何字段、用 #[serde(remote)] 可以给外部类型加支持、用 serde_json::Value 这类动态树可以做运行时 JSON 处理。
这两个看似矛盾——"稳定"和"灵活"——是怎么做到的?
答案:扩展点和稳定核心的分离:
- 稳定核心:
Serialize/Deserialize/Serializer/Deserializer/Visitor等 trait,API 不变。 - 扩展点:属性(
with、remote、flatten)、DeserializeSeed、Error::custom——让用户在核心之上加功能,而不是改核心。
每加一个新需求,先问自己:能用扩展点实现吗?能——用扩展点。不能——考虑加新扩展点,而不是修改核心。
Serde 的扩展点清单:
#[serde(with)]/serialize_with/deserialize_with:字段定制序列化#[serde(remote)]:外部类型代理#[serde(flatten)]:结构展平#[serde(from)]/#[serde(try_from)]/#[serde(into)]:类型转换钩子DeserializeSeed(serde_core/src/de/mod.rs:803):带状态的反序列化Serializer的三个带默认实现的便捷入口collect_seq(serde_core/src/ser/mod.rs:1296)、collect_map(serde_core/src/ser/mod.rs:1333)、collect_str(serde_core/src/ser/mod.rs:1373):前两个把迭代器批量喂成 seq / map,collect_str把Display值写成字符串;格式 crate 想优化就覆盖,不想管就不用写- 自定义
Formatter(serde_json 提供的,src/ser.rs:1541):改变格式化风格
每一个扩展点都是 "我们核心做不到这件事、但你可以通过这个 hook 自己做" 的设计。
应用启示:
- 库设计要区分"核心抽象"(稳定)和"扩展钩子"(可以新增)。
- 用户需求 → 先尝试加 hook;迫不得已才改核心。
- 版本演化:加新 hook 是非破坏性(老用户不受影响),改核心是破坏性。
18.7 设计哲学六:错误是 API 的一部分
Serde 的错误系统值得单独讲。它包含:
- 结构化错误:
invalid_type(unexp: Unexpected, exp: &dyn Expected)(serde_core/src/de/mod.rs:213)、invalid_value(serde_core/src/de/mod.rs:231)、invalid_length(serde_core/src/de/mod.rs:245)、missing_field(serde_core/src/de/mod.rs:289)——都是Errortrait 上带默认实现的构造函数,不是字符串。 - 上下文继承:
Unexpected保留"实际收到了什么"、Expected保留"期望什么"。 - Span 定位(compile-time 错误):
serde_derive的错误指向用户代码的精确位置。 - 错误累积(
Ctxt):一次报告所有错误,不是"第一个错就退出"。
每一项都显著改进用户体验。反面案例:某些格式库的错误就是 "parse error at line 42" ——用户要猜是"类型不匹配"还是"字段缺失"还是"格式错误"。
错误系统的原则:
- 错误 = 类型:用 enum 或 struct 表达错误种类,不是字符串。
- 错误 = 上下文:每个错误携带"在哪里出的、期望什么、收到什么"。
- 错误 = 指引:错误信息应该帮用户找到问题,而不是让他们进一步调试。
实现代价:Serde 的错误类型比单纯 String 复杂得多——Error trait 带几个 Self 返回的构造函数、Unexpected enum 有 18 个变体(serde_core/src/de/mod.rs:338-398:Bool/Unsigned/Signed/Float/Char/Str/Bytes/Unit/Option/NewtypeStruct/Seq/Map/Enum/UnitVariant/NewtypeVariant/TupleVariant/StructVariant/Other)。但这些复杂度让最终错误信息对用户有价值。
应用启示:
- 不要用
Result<T, String>——用具体的 Error 类型。 - 每个错误变体配一个"可操作"建议——用户看到后知道怎么修。
- 编译期错误尤其要精确——Rust 的 diagnostic 生态(
syn::Error等)是整个社区的共同积累。
18.8 设计哲学七:工程细节决定上限
Serde 的"意外细节"让它从"好库"变成"伟大库":
_serde::命名空间:过程宏用Span::call_site()生成的标识符不享受宏卫生(第 8 章 §8.8),只能靠固定别名和全路径避免命名冲突。生成代码里写的是_serde::Serializer,这个别名由serde_derive/src/dummy.rs:11的extern crate serde as _serde;就地引入——用户就算根本没use过serde,生成代码也不受影响;若在 Cargo.toml 里给 serde 改了名,就用#[serde(crate = "...")]走serde_derive/src/dummy.rs:7的use #path as _serde;分支。__private版本化:serde/build.rs:7-13的模板在第 25-27 行用CARGO_PKG_VERSION_PATCH替换出pub mod __private228 { ... },serde_derive/src/lib.rs:96-105同样拼出concat!("__private", env!("CARGO_PKG_VERSION_PATCH"))。作用是逼 serde 与 serde_derive 的 patch 版本对齐:比如 serde 1.0.228 配上 serde_derive 1.0.219,后者生成的_serde::__private::...在 serde 1.0.228 里已经不存在,立刻报错找不到__private,而不是撞进一个悄悄改过的内部 API(1.0.225 引入,commitcbe98a98;第 10 章 §10.3 详述)。这是兜底:1.0.228 的serde_core/Cargo.toml还用一条永不生效的[target.'cfg(any())'.dependencies]把 serde_derive 钉在=1.0.228,正常的 cargo 解析根本配不出两个不同版本。dummy const包装:serde_derive/src/dummy.rs:23把整个 impl 塞进const _: () = { ... },让生成代码引入的_serde别名和辅助项只在这个匿名常量的块里可见,不污染用户 namespace。- lexical 浮点解析库:serde_json 里 3708 行(
src/lexical/实测)只为把十进制文本正确舍入成 f64,且整个模块被src/lib.rs:435-436的#[cfg(feature = "float_roundtrip")]门住;格式化方向另交给 itoa / zmij(第 17 章)。 - optimistic borrowing:从
&str/&[u8]反序列化时 JSON 字符串优先零拷贝,只在有转义时复制(from_reader走的IoRead则总要复制)——serde_json 的src/read.rs:513-523的快路径在 scratch 缓冲区还空着(说明一路没遇到反斜杠转义)时直接Reference::Borrowed(&self.slice[start..self.index]),否则才落到Reference::Copied。 - Ctxt::Drop:强制调用 check() 的 linter 机制——
serde_derive/src/internals/ctxt.rs:61-66在 drop 时若错误还没被取走就panic!("forgot to check for errors")。
这些都不是"宏观架构"——是低层工程细节。但它们决定了 Serde 在真实使用时的"体感"。好库有宏观架构,伟大库还有 100 个微观细节。
应用启示:
- 定期 profile 自己的库——找到最耗时的那几个点,专门优化。
- 读其他成熟库的源码——每找到一个"我没想到"的细节,就是一个学习点。
- 不怕投入工程量——如果某个优化能让用户体验显著提升,值得写几千行专用代码。
18.9 Serde 的边界与未来
Serde 也不是完美——有些事它做不到或做不好:
边界 1:图结构。Serde 的 Data Model 是树,无法直接表达 DAG/cycle。用户需要手动设计 ID 映射(序列化对象 ID、反序列化时查表)。
边界 2:类型元数据。Serde 不能序列化"这是什么类型"本身——只能序列化类型实例。typetag 第三方 crate 部分解决了这个问题(通过 string-based dispatch)。
边界 3:运行时动态格式。Serializer: Sized(serde_core/src/ser/mod.rs:355)禁止 trait object,导致"运行时决定用什么格式"困难。erased-serde 解决但有性能代价。
边界 4:stream 式处理。Serde 的反序列化倾向于"一次性读入"——对 GB 级 JSON 流,要手工用 Deserializer::into_iter(serde_json 的 src/de.rs:154,返回 StreamDeserializer)等 workaround。
未来方向(社区讨论):
- Specialization 稳定后:
&[u8]可以自动走 bytes 路径(而不是需要serde_bytes)。 - GAT (Generic Associated Types) 进一步利用:可能改进 Deserializer trait 的 API。
- Async serialization:目前所有 API 是同步的,异步场景(流式 network 反序列化)需要 workaround。
Serde 主要作者 dtolnay 对大改动非常保守——API 稳定性压倒一切。所以未来 Serde 的变化会继续"加扩展点而不改核心"。
18.10 可迁移的 11 个模式
归纳全专栏讲到的设计模式,列出你可以迁移到自己工程的 11 个:
1. 中间层抽象(第 2 章):M×N → M+N。选 N 个原语,让双方通过它们通信。
2. 状态机原语(第 3 章):begin → add×N → end 模式,支持流式和批量统一。
3. Visitor 反向控制(第 4 章):不让格式返回值,而让调用方传 visitor 接收——解耦类型和格式。
4. 关联类型连带约束(第 3 章):serde_core/src/ser/mod.rs:373 写的是 type SerializeSeq: SerializeSeq<Ok = Self::Ok, Error = Self::Error>;——Ok 和 Error 两个都被钉死,跨 trait 保证一致性。
5. 空 enum + PhantomData 表达"不可能"(第 3 章):serde_core/src/ser/impossible.rs:60-66 的 Impossible 就是一个 void: Void 字段(enum Void {},无居民)加两个 PhantomData,用类型系统编码"永远不会被调用"。
6. 过程宏三阶段(第 10 章):parse → AST 加工 → 生成代码。每阶段单独可测试、单独演化。
7. 错误累积上下文(第 10 章):Ctxt 收集所有错误后统一报告,一次看到全部问题。
8. 命名空间隔离(第 10 章 §10.3):_serde:: 前缀 + __private<patch> + dummy const 包装,让生成代码不污染用户。
9. 临时 wrapper 把函数变 trait(第 16 章):用 struct + impl 包装自由函数,让它能被 trait 方法调用。
10. 扩展点 vs 核心分离(第 11、16 章):核心 trait 稳定、属性/seed/hook 可新增。
11. 生命周期编码存活期(第 15 章):用 'de 生命周期参数把"能否零拷贝"写进类型。
每一条都是工程级的——不是"好想法",是"真的能在生产中工作的方法"。下次你设计一个库,这 11 条是检查清单。
| 模式类别 | Serde 中的落点 | 可迁移到的工程场景 |
|---|---|---|
| 抽象分层 | Serialize / Serializer 把数据类型和格式拆开 |
插件系统、协议适配层、存储后端抽象 |
| 类型约束 | 关联类型、Impossible、PhantomData |
编译期状态机、权限能力建模 |
| 代码生成 | derive 三阶段、命名空间隔离 | SDK 生成器、ORM schema 生成、RPC stub |
| 错误体验 | Ctxt 错误累积、span 定位 |
编译器插件、配置校验、CI 规则引擎 |
| 扩展治理 | 核心 trait 稳定,属性和 seed 扩展 | 长期维护的公共库 API |
18.11 来自 Serde 的 10 个判断原则
除了具体模式,还有一些更抽象的判断准则——当你面临设计选择时,可以问自己:
1. 这个功能能用扩展点实现吗?能就用扩展点,不改核心。
2. 这个选择让用户代码变短还是变长?优先让用户代码短。
3. 这是能在编译期解决的问题吗?不要推到运行时。
4. 类型能编码这个约束吗?能就用类型,别靠运行时检查。
5. 默认路径是最常见的那条吗?让常见场景零配置工作。
6. 错误信息对用户可操作吗?让他们知道怎么修,不只是"出错了"。
7. 这个模式能 scale 到 10x 复杂度吗?不要为今天的简单场景牺牲未来的复杂场景。
8. 内部复杂度是否换来了外部简洁?复杂度"进",简洁"出"。
9. 这个 API 5 年后还能兼容吗?向后兼容是库的天条。
10. 这个功能是否让"不用它的人"承担成本?如果是,重新设计让它可选。
这 10 条原则都能在 Serde 里找到对应的具体决策。它们也是任何长期维护的库应该遵守的原则。
18.12 Serde 和其他语言生态的对话
把视线扩展到 Rust 之外,Serde 的定位更清晰:
Java/Kotlin 的 Jackson:运行时反射,无需代码生成。代价是显著的运行时开销(反射元数据查询 + 动态分发)、类型错误只能运行时发现。但开发体验"零配置"——新加字段自动序列化。对比:Serde 的 #[derive(Serialize)] 达到了"开发体验零配置 + 零运行时开销"——Rust 生态的标杆方案。
Go 的 encoding/json:标准库内置、反射驱动、字段 tag 用 struct tag 语法。简单朴素但灵活性差——自定义序列化要 implement MarshalJSON 方法,不能组合。对比:Serde 的扩展点系统(with/remote/flatten)让复杂场景有一等支持,而 Go 往往要 fallback 到手写。
Python 的 pydantic:基于类型注解(runtime)做校验和序列化。性能好于 Python 平均水平(用 Cython/Rust 加速),但仍有运行时开销。Pydantic v2 的核心(pydantic-core)甚至用 Rust 重写。但要说清楚方向:Serde 本身没有 schema 概念——它只定义 Data Model 和 trait 协议,不生成也不校验 JSON Schema;Rust 侧的 schema 生成由第三方 crate schemars 在 serde 之上另建一套 JsonSchema trait 和 derive 宏完成,serde-rs 组织里没有这个东西。所以准确的说法是"两个生态在'类型即协议'这件事上收敛到了同一个答案",而不是谁把谁的特性搬进了自己的核心。
C++ 的 boost::serialization:基于模板特化 + 手动注册。性能好但用起来痛苦——代码量大、编译慢。对比:Serde 用过程宏而非模板做代码生成,实现复杂度转移到 proc-macro crate,用户代码保持简洁。
JavaScript/TypeScript 的 JSON:语言内置、无类型检查(除非用 Zod/io-ts 这类 runtime validator)。TS 的类型系统只在编译时存在,运行时无从验证。对比:Rust 的 Serde 在编译期和运行时都有类型保证——结构不对编译不过、数据不对反序列化失败。
一个观察:"编译期做 vs 运行时做" 是 Serde 和其他生态最本质的分歧。静态语言里 Serde 代表"编译期派"的巅峰——其他选择(反射、runtime schema)在 Rust 里都会损失性能或类型安全。
18.13 一些被问得最多的"为什么不……"
社区讨论 Serde 时有一些经典反问。回答它们能加深理解:
"为什么不直接支持反射,像 Java 那样?"
答:Rust 没有 Java 式的运行时反射(Any / TypeId 只能做类型标识,读不到字段)。加反射要么放弃零成本抽象(所有类型都要带元数据,二进制膨胀),要么做不完整(不能枚举私有字段)。Serde 的选择是"用 macro 生成代码代替反射"——编译期完成所有分析,运行时就是纯数据操作。
"为什么不用声明宏(macro_rules!)做 derive?"
答:稳定通道上声明宏不能注册成 derive 宏(第 5 章讲过)——#[derive(...)] 只对 #[proc_macro_derive] 标注的函数开放,声明宏没办法附着到一个已经写好的类型上去读它的字段。derive 必须用过程宏——这是硬性约束。
"为什么 Serializer 用 trait 而不是枚举分派?"
答:如果 Serializer 是 enum(比如 enum Format { Json, Bincode, MsgPack }),加新格式要改 Serde 核心。用 trait + 单态化,新格式只是一个新的 impl Serializer——Serde 核心不用改。扩展性压倒简洁性。
"为什么 derive 宏要分独立 crate?"
答:第 5 章讲过——proc-macro crate 有特殊编译约束,不能导出普通类型。不分开会阻塞 Serde 核心的使用。这是 Rust 的工程约束,不是 Serde 的选择。
"为什么反序列化这么复杂?不能直接 return 值吗?"
答:因为 Rust 没有动态类型。反序列化必须在编译期知道目标类型,所以 "return 值" 实际是 "把值写进类型的 Visitor 模板"。Visitor 模式是 "多形态输入 + 编译期类型确定" 的工程结果——不是 Serde 创造的复杂,是问题本身的复杂。
"为什么没有原生异步 API?"
答:目前是历史+工程问题。Serde 1.0 发布时(2017)Rust async 还不稳定。后续要加 async 版本等于复制整个 trait 层次(AsyncSerialize / AsyncDeserializer),代码量翻倍。社区选择"同步 API 基础 + 异步框架适配"(比如 Tokio 里用 spawn_blocking 包 Serde)。语言侧的障碍其实已经清掉了:rustc 源码里 rustc_feature 的已接受特性表把 generic_associated_types 记在 "1.65.0"、async_fn_in_trait 记在 "1.75.0",两项都早已稳定。但 serde 的 trait 层次至今没有 async 版本——这是维护者在稳定性上的取舍,不再是语言限制。
18.14 和本系列其他专栏的呼应
至此 18 章走完。把 Serde 放回到整个 Rust 生态,它不是孤立的——它是本系列"Rust 后端系列"的核心齿轮:
- 《Rust 编译器与运行时揭秘》专栏:Serde 的 derive 宏、生命周期、trait 分发、单态化都是那个专栏讲过的编译器机制的应用。
- 《Tokio 源码深度解析》专栏:Tokio 本身不依赖 Serde,但跑在 Tokio 上的服务,配置、观测性输出、消息发送几乎都离不开 Serde。Tokio 的
#[tokio::main]宏和 Serde 的#[derive(Serialize)]走同一套过程宏工具链。 - 《MCP 协议设计与实现》专栏:在 Rust 侧,MCP 协议的 JSON-RPC 消息、工具 schema 都通过 Serde 表达。MCP 的 Rust 实现本质上是"用 Serde 做协议"。
- 《LangChain 设计与实现》专栏 / 《LangGraph 设计与实现》专栏:虽然这两个专栏讲的是 Python 版本,但它们的 Tool Call 格式、LangSmith 消息 schema 都用 JSON——如果 Rust 实现 LangChain-like 框架(已经有一些在做),Serde 是必经之路。
Serde 是 Rust 生态的"通货"——像 Linux 里的 pipe、Unix 里的 file descriptor——它本身不突出,但所有上层应用都依赖它。
18.15 本专栏结束,你的旅程开始
读完本专栏,你对 Serde 的技术掌握应该到了:
- 读 serde_derive 任何源码不卡壳
- 能自己写 derive 宏(第 9 章的 Builder 只是热身)
- 能为第三方类型写 Serialize/Deserialize(手写版)
- 能为自定义格式实现 Serializer/Deserializer(例如你想支持某个私有二进制格式)
- 能诊断 Serde 相关编译/运行时错误的根因
- 能在 API 设计中合理选用 Serde 的特性组合
但更重要的是——你学到了一套工程思维。这套思维不只适用于 Serde,适用于任何"多输入多输出的转换系统"、"代码生成工具"、"扩展点友好的库"。
建议下一步:
- 做完 proc-macro-workshop(不只 Builder,Debug、Seq 都做)。
- 写一个自己的 derive 宏项目——比如给 ORM、配置管理、权限系统写一个。
- 读另一个大 derive 项目:bevy_reflect、sqlx、clap——看同样的模式如何应用在不同领域。
- 贡献 Serde:issue tracker 里长期有 good-first-issue。哪怕改一个 typo 或加一个测试,都是深入 Rust 生态最好的方式。
最后一个动手实验:拿出前言推荐的 cargo-expand——第 9 章起你已经用它看过 #[derive(Serialize)] 的展开。现在再看一次。18 章前,那些展开代码是天书;现在,每一行你都能指出"这是第 X 章讲的模式"。那种通透感,就是本专栏最大的回报。
动手实验
- 审视自己的项目:在你最近的 Rust 项目里找 Serde 使用——列出你用了多少属性、多少高阶特性。评估哪些地方可以用
Cow换String提升性能(Cow字段要加#[serde(borrow)]才会尝试借用,带转义的字符串和from_reader仍然只能复制)。 - 画 Serde 架构图:用自己的话画一张"Serde 系统全貌",包括 Data Model、trait 层、derive 宏、格式实现四层。贴在墙上。下次读 Rust 生态其他库时问"它的这一层对应 Serde 的哪一层"。
- 写一个自己的"小 Serde":选一个领域(比如 CLI 参数解析),设计自己的中间层、自己的 trait、自己的 derive 宏。100-500 行就好。经过这个过程,你的"库设计能力"会有质的提升。
- 贡献一个 PR:随便改 serde/serde_derive/serde_json 里一个 typo 或加一个测试。走一遍 PR 流程——fork、commit、提交、回应 review——这是你融入 Rust 生态的入场券。
延伸阅读
- Serde 官网:每一页都值得读一遍。
- serde-rs GitHub 组织:serde、json(serde_json)、bytes(serde_bytes)等官方仓库(serde_test 已迁到 serde-deprecated 组织归档;serde_yaml 一直在 dtolnay 个人名下,2024 年已归档)。
- Rust crates.io 前 100 依赖:其中做数据交换、配置、协议的那一批基本都直接或间接依赖 serde(纯底层设施如
libc、cfg-if、bitflags,以及 serde 自己的依赖syn/quote/proc-macro2则不依赖)。看前一类如何用 serde 是最好的"实战教材"。 - dtolnay 的个人主页:Serde 主作者,还维护 syn、quote、proc-macro2、cargo-expand、thiserror、anyhow 等半个 Rust 生态。他的代码和设计思路是整个 Rust 社区的标杆。
- 本系列的 Rust 后端专栏:《Rust 编译器与运行时揭秘》《Tokio 源码深度解析》《Hyper 与 Tower:工业级 HTTP 栈》《Axum 设计与实现》《SQLx 源码深度解析》《Rust + WebAssembly 全链路解析》。读完 Serde 只是这一系列的一块拼图——每一个都值得读。